こちらは鈴鹿高専Advent Calendar 2023 25日目の記事です。
はじめに
学校で習ったC++で何か応用できないかと思い、ターミナルで動くRPGを制作してみました。
その過程で蓄積した設計の知識をまとめたので是非ご覧になってください。
目次
タイトル | 内容 |
---|---|
作品について | 制作したCUIのRPGについて深堀りします。 |
制作で役に立った書籍 | 設計において役立った書籍を紹介します。 |
インクルードガード | C++における重複インクルードへの対処法を紹介します。 |
不変化 | 可変性がもたらす危険性について解説します。 |
高速化 | const参照について解説します。 |
名前設計 | 変数や関数の命名において優先すべきことややってはいけないことを紹介します。 |
変数一つ一つに意味を持たせる | 可読性の高いコードの書き方を紹介します。 |
英文のように読めるコードを書く | 可読性の高いコードの書き方を紹介します。 |
さいごに |
作品について
作品名はHoneyRPGです。
作品のリポジトリを置いておきます。
元々は国語の授業で発表をする機会があり、「プログラミングの楽しさを教えたい」という思いでこの作品を制作し始めました。制作していくうちに設計の良し悪しを理解してきて勉強にもなったと思います。
HoneyRPGは巷で話題のGenshin ImpactというmiHoYoさんのオープンワールド3Dゲームをテーマに制作しました。元素による反応もそちらから借りています。
制作で役に立った書籍
作品を制作する過程で大いに勉強になった書籍はミノ駆動さんの「良いコード/悪いコードで学ぶ設計入門」です。もはや設計における大定番となった書籍、皆さんも是非手に取ってみてください。
インクルードガード
RPGではもちろん味方と敵が戦ってもらわなければなりませんので、FighterはMonsterを、MonsterはFighterを攻撃する関数を持つことになります。
attack()
関数は引数に相手の型を使うので、相手側のファイルをインクルードしなければなりません。
#include "Monster.hpp"
class Fighter {
public:
void attack(const Monster& monster);
};
#include "Fighter.hpp"
class Monster {
public:
void attack(const Fighter& fighter);
};
しかし、これではFighter.hppがインクルードしているMonster.hppはFighter自身をインクルードしているため、重複インクルードとなり無限ループが発生します。
これを防ぐために、C++はインクルードガードと呼ばれる環境を提供しています。
インクルードガードは主に2つあります。
1つ目は従来型のインクルードガードです。
#ifndef
や#define
のあとの文字列は拡張子含むファイル名を大文字のスネークケースで書くという慣習があります。
#ifndef FIGHTER_HPP
#define FIGHTER_HPP
#include "Monster.hpp"
class Fighter {
public:
void attack(const Monster& monster);
};
#endif // FIGHTER_HPP
2つ目は#pragma once
を使う方法です。ファイルの先頭に一行書くだけなので楽ではありますが、1つ目に紹介した方法より新しいので互換性のないコンパイラも存在するかと思います。
#pragma once
#include "Fighter.hpp"
class Monster {
public:
void attack(const Fighter& fighter);
};
不変化
変数はそれぞれ意味を持ちます。しかし、その内容が変更されてしまえば、意味は失われてしまい、バグにつながってしまいます。
可変は破壊的と言っても過言ではありません。
修飾詞constを用いて、変数は基本的に不変にしましょう。
void Fighter :: buildAttackMethod() {
const int power = atk; // Read-only
const ElementalAttributeList fighterElement = elementalAttribute.getElement(); // Read-only
attackMethod[0].setParams("punchAttack", ElementalAttributeList::None, power);
attackMethod[1].setParams("kickAttack", ElementalAttributeList::None, power*1.5);
attackMethod[2].setParams("elementalAttack", fighterElement, power*0.8);
attackMethod[3].setParams("elementalStorm", fighterElement, power*2);
}
以下は見つけてしまった悪いコード。
void Monster :: initAttackMethod() {
int power = this->atk; // 可変であり、危険
this->attackMethod[0].setParams("normalAttack", ElementalAttributeList::None, power);
this->attackMethod[1].setParams("chargeAttack", ElementalAttributeList::None, power*1.5);
this->attackMethod[2].setParams("elementalAttack", this->elementalAttribute.getElement(), power*0.8);
this->attackMethod[3].setParams("elementalStorm", this->elementalAttribute.getElement(), power*2);
}
高速化
C++でユーザー定義型(クラス)を実装し、それをある関数の引数の型に使う場合、const参照を用いるのが一般的です。
値渡しをする場合、コピーによって関数に値を渡すため、ユーザー定義型の大きさによっては実行のパフォーマンスを大幅に下げてしまうからです。
以下がconst参照の使用例です。
void Fighter :: attack(const Monster& monster) {
std::cout << "Select how to " << name << " attack." << std::endl;
displayAttackMethods();
int method = attackMethod.size()+1;
std::cout << "> ";
while(attackMethod.size() < method) {
std::cin >> method;
if(attackMethod.size() < method) {
std::cout << "Select How to " << name << " attack." << std::endl;
std::cout << "> ";
}
}
method--;
selectedAttackMethod = attackMethod[method];
std::cout << name << "'s " << selectedAttackMethod.getName() << "!!!" << std::endl;
selectedAttackMethod.action(*this, monster);
}
void Monster :: randomAttack(const Fighter& fighter) {
int randomSeed = GeneralMethod::generateRandomSeed(0, 3);
selectedAttackMethod = this->attackMethod[randomSeed];
std::cout << randomSeed << std::endl;
std::cout << this->name << "'s " << selectedAttackMethod.getName() << "!!!" << std::endl;
selectedAttackMethod.action(*this, fighter);
}
Javaではこの参照がユーザー定義型においてデフォルトになっているのだとか。
(Javaではconst参照ではなく、参照がデフォルトになっています)
まだまだ勉強が足りません。
名前設計
何気に設計の中で一番重要な要素かもしれません。
では「やってはいけない命名」を紹介します。
- 連番
番号やアルファベットなどを利用した命名。
何を表しているのかわからず、可読性が落ちる。
// 半径から円の面積を求める
double calcAreaOfCircle(const double value01) {
const double a = 3.14;
const double value02 = a * value01 * value01;
return value02;
}
- 技術駆動命名
技術用語などを使用した命名。
void updateFlag() {
flag = !flag;
}
- 適当な変数名
const int hoge_ = hoge;
const int hoge__ = huga;
const int hoge___ = hoge_ + hoge__;
const int tmp;
変数名はどれだけ長くてもいいので、とにかく理解しやすい意味のある命名にするべきです。
変数名のつけ方の例はこの項の後にたくさん出てくるのでよく見ておくとよいかもしれません。
変数一つ一つに意味を持たせる
いくらコードを短くしても、意味がわからなければそれだけ時間の無駄が発生します。
そのため、優先的に変数に意味を持たせ、可読性を保つべきです。
これは攻撃を受けた時のダメージの計算をする関数です。
関数内の最初の3行をお読みください。
void Character :: damagedBy(Character& attacker) {
const int baseDamageAmount = attacker.selectedAttackMethod.getPower();
const double damageMagnification = calcDamageMagnification(attacker);
const int totalDamageAmount = baseDamageAmount * damageMagnification;
hp = hp - totalDamageAmount;
this->damagedMessage(totalDamageAmount);
if(0 < hp) return;
this->dead();
}
まず、baseDamageAmount
は基本ダメージです。ここでは、攻撃側の攻撃力を参照しています。
次にdamageMagnification
で、これはダメージの増幅率を属性の相性などをもとに計算し、取得します。
最後に、totalDamageAmount
ですが、これは基本ダメージに増幅率をかけたものを取得しています。これが最終的なダメージになります。
この3行は
const int totalDamageAmount = attacker.selectedAttackMethod.getPower() * calcDamageMagnification(attacker);
と1行にまとめてしまうこともできますが、これでは他の誰かがこのコードを見たときに、「このコードは何を計算しているんだ?」となります。
長くなってしまってもいいので、なるべく変数一つ一つに意味を持たせるコードを書く意識をしましょう。
Tips
ここではお話ししませんでしたが、特にマジックナンバーはコードの可読性を著しく落としてしまうため、これも避けるべき要素です。
英文のように読めるコードを書く
ここでは、「英文のように読めるコード」を紹介します。
コードを読んだほうが話が早いと思います。
以下のコードは特定の元素反応が起きるか否かを判定する関数です。
ElementalReaction ElementalAttribute :: ElementalReact(ElementalAttributeList otherElement) {
if(this->canNoneReactBy(otherElement)) return NoneReact;
if(this->canSaturateBy(otherElement)) return Saturate;
if(this->canEvaporateBy(otherElement)) return Evaporate;
if(this->canMeltBy(otherElement)) return Melt;
if(this->canElectrocuteBy(otherElement)) return Electrocute;
if(this->canBloomBy(otherElement)) return Bloom;
return NoneReact;
}
例えば、if(this->canMelBy(otherElement)) return Melt;
という1行がありますが、これは"if this can melt by other element"という英文に解釈することができます。日本語に訳すと、「もし、この属性が相手の属性によって溶解反応が起きるなら」という意味になります。
これは、関数の接頭辞にcanを使っているからこそ叶うことです。意識しなければこのようなコードは書けません。
さいごに
ここまで御覧いただきありがとうございます。指摘があればお願いします。勉強の足しにします。