#はじめに
例えばシューティングゲームを作る場合、敵を何十種類か実装して、それぞれ別の動きをさせたいでわけです。
しかしながらc++では(他の言語でも)そういう機能の実現方法がたくさんあり、最高の設計をするのは至難です。
この記事では敵の種類毎に別の動きをさせる方法について色々説明します。
それによって、どういう仕様の場合どうやって処理を分岐させるのが良いか考える助けになるかと思います。
サンプルを動くように実装するのは手間なので、そういう風にはしてません。
※マークダウンでは表示が狂いますが、識別子に日本語を使うのはg++以外では特に問題無いです。
#switchによる処理の分岐
プログラミングを始めたばかりの人はまずこんな感じにすると思われる。
あるいはif文でも似たような事したり。
enum class EnemyType
{
スライム,
ゴブリン,
ドラゴン
};
class Enemy
{
public:
EnemyType 種類;
int 体力;
int X座標;
int Y座標;
int 大きさ;
int 速度;
int 経過時間 = 0;
bool 死亡フラグ = false;
Enemy(EnemyType 種類,int X座標,int Y座標)
{
Init(種類,X座標,Y座標);
}
void Init(EnemyType 種類,int X座標,int Y座標);
void Update();
private:
void Initスライム();
void Initゴブリン();
void Initドラゴン();
void Updateスライム();
void Updateゴブリン();
void Updateドラゴン();
};
void Enemy::Init(EnemyType _種類,int _X座標,int _Y座標)
{
種類 = _種類;
X座標 = _X座標;
Y座標 = _Y座標;
switch(種類)
{
case EnemyType::スライム:
Initスライム();
break;
case EnemyType::ゴブリン:
Initゴブリン();
break;
case EnemyType::ドラゴン:
Initドラゴン();
break;
}
}
void Enemy::Update()
{
switch(種類)
{
case EnemyType::スライム:
Updateスライム();
break;
case EnemyType::ゴブリン:
Updateゴブリン();
break;
case EnemyType::ドラゴン:
Updateドラゴン();
break;
}
}
//以下Update~やInit~を実装
文法も簡単でそれなりに分かりやすく、実際の所この方法だけでもゲームは作れます。初学者はこの手法だけにしとけってのも分からんでも無い感じです。
この方法だとスライムもドラゴンもゴブリンも同じ型なので、一括して事前にメモリを確保しておくのも楽ですね。
ただ、switchには極力enum classを使うようにはしましょう。
int 種類;
const int スライム = 0;
const int ゴブリン = 1;
const int ドラゴン = 2;
みたいな定義は分かりにくいので止めよう、これは初心者でもやめよう。
大体これで良い感じはするのですが、わりと欠点はあります、拡張のしにくさです。
例えばドラゴンは三回ダメージを受けると反撃するみたいな処理を追加するとします。
するとEnemyクラスにダメージを受けた回数を記録する変数を追加する事になりますが、その変数はドラゴンしか使わないので無駄が多いです、敵が100種類いてそれぞれにそんな変数があると、かなり分かりにくくなります。かといって個別の変数だけメモリを動的に確保したり「int 汎用A」みたいな事を行うと可読性がガクッと落ちます。
また敵の種類を増やす場合、あちこちにあるswitch文を修正しないといけないのも手間がかかります。
サンプルでは初期化と更新処理だけですが、実際は描画処理だったり、ダメージ時の処理などもswitchで分岐する必要がありさらに大変。非実装にした種類を削除するとかもわりと面倒です。
(※switchが大量に増えるのは関数ポインタ使えば多少マシに出来ます。)
①switch文が何箇所も出てこない
②分岐の数が後から増える可能性が低いか、分岐の数自体が少ない
③敵種によって必要な変数が殆ど変わらない
こういう場合はswitch文で十分なんじゃないかと思います。
例えば、ドラゴンが残りHPや経過時間に応じて行動パターンを複数持つとか言う場合はifやswitch文を使いますし、ゴブリンはサブ種族にレッドとブルーがいて、動く早さだけ少し違うとかならInit関数で初期化する速度の値を変更するとかで良いでしょう。
実際にシューティングゲームを作る場合、CSVファイルとかで敵のデータ作って実行時に読み込んでプログラムを変更せずに処理を変えるみたいな事になると思うのでswitchやifで処理を書く事がなんやかんや多くなると思います
#継承と仮想関数による動的多態
所謂オブジェクト指向プログラミングにおける、ポリモーフィズム云々。
class IEnemy
{
public:
int 体力;
int X座標;
int Y座標;
int 大きさ;
int 経過時間 = 0;
bool 死亡フラグ = false;
IEnemy(int X座標,int Y座標):
X座標(X座標),
Y座標(Y座標)
{}
virtual ~IEnemy(){}
virtual void Update();
};
class スライム : public IEnemy
{
public:
スライム(int X座標,int Y座標):
IEnemy(X座標,Y座標)
{}
void Update() override
{
X座標 += 5;
}
};
class ドラゴン : public IEnemy
{
public:
int 速度 = 0;
ドラゴン(int X座標,int Y座標):
IEnemy(X座標,Y座標)
{}
void Update() override
{
X座標 += 速度;
++速度;
}
};
同じ型を継承したクラスはその型のポインタのコンテナに入るので、同じコンテナにドラゴンとスライムのポインタ入れる事が出来ます。
さらに仮想関数のオーバーライドによって処理は分岐してどうのこうのします。
newとかスマートポインタとか出てくるのでそれぐらいは理解する必要が出てきますが文法的にはそんなに分かりにくくないとは思いますし、実装もわりと簡単です。(※筆者の印象です)
あとは動的にメモリを確保する必要が出てくるので、速度が気になるかもしれません。
この方法だと新しい敵の種類を増やす度にswitchにcaseを追加していく作業が無くなり、新たにIEnemyを持つクラスを追加すれば良いだけなので、新しい仕様が増えても対応しやすいです。
switchの分岐に比べて色々改善されましたが、まだ欠点があります。
ボスキャラみたいなのしかいない場合は、全ての敵が全然違う動きをするのでこれで良いのですが、
実際は色んな種類の敵がいると、同じ事をしている事がよくあります。
例えば敵の移動コースが四種類あり、さらに等速,加速,一定まで加速,一定時間後停止の4つがあり、さらに弾の形が四角,丸,三角,十字の四種類あったとします、これをそれぞれ別のクラスにすると4×4×4で64個クラスを定義する必要があり、これは面倒です。
こういう仕様だと、それぞれ必要な変数が違うので(例えば円だと中心座標と半径、三角だと3つの座標になる)switchで分岐するのは分かりにくく、他のアプローチが必要になります。
#Strategyパターンによる動的な多態
というわけでStrategyパターンです。
//IShape等の定義は省略
class Enemy
{
public:
int 体力;
std::unique_ptr<IShape> 図形;
std::unique_ptr<IMotion> 移動方法;
std::unique_ptr<ISpeed> 移動速度;
int 経過時間 = 0;
bool 死亡フラグ = false;
IEnemy(IShape *図形,IMotion *移動方法,ISpeed *移動速度):
図形(図形),
移動方法(移動方法),
移動速度(移動速度)
{}
virtual void Update()
{
移動方法->Do(図形,移動速度.Update());
}
bool Hit(IEnemy *判定対象)
{
図形->Hit(判定対象->図形.get());
}
};
int main()
{
auto enemyA = IEnemy(new Rect(0,0,10,10),new Liner(),new Accel(0,0.1));
auto enemyB = IEnemy(new Line(0,0,10,10,5),new Curve(),new Fix(1));
}
文法的には新しい事は出てこないので、問題ないかと思います、詳しい解説はStrategyパターンで検索したら見つかると思います。
図形の型はダブルディスパッチとオーバーライドを組み合わせた当たり判定処理を書けばよいでしょう。
こんな感じにする事で組み合わせ毎にクラスを書く必要がなくなってかなり楽になりました。
独自で複雑な動きをする敵はクラスを作ってUpdateをオーバーライドし、ザコ敵や弾はコンストラクタの引数を変えてFactoryパターンで生成するなりすれば良さそうです。
ついでに、この方法だとIShape,IMotion,ISpeed等はIEnemyに依存しないのでソースコードの再利用性が高くなります。
ただ依存していないのは面倒な場合もあります、例えば壁にぶつかると向きを反転し、二回反転すると消滅するとか言うReflect型を定義したい場合、Reflect型のコンストラクタでIEnemyのポインタを変数として確保しておかないとIEnemyを消滅させる事が出来なくて云々みたい分かりにくくなる事がわりとあります。
他にも問題があります。
主人公の武器の切り替え等であれば動的に決めざるを得ないのですが、今回の場合、実行中に移動方法等を切り替えていません。
するとnewで動的にメモリ確保する回数が増えるのが気になるし、あちこちにnewが出てくるのが冗長に見えます。メモリプールとか使えば多少改善されますが出来ればnewは減らすため、コンパイル時に静的に解決したい。
(※ラムダ式や関数ポインタを使う方法もありますが、関数は状態を持てないので図形の形の差し替えとかに用いるのは無理です。)
#ジェネリックなPolicyパターンによる静的な多態
テンプレートを使ったジェネリックプログラミング。
class IEnemy
{
public:
int 体力;
IShape &図形;
int 経過時間 = 0;
bool 死亡フラグ = false;
IEnemy(const IShape &図形):
図形(図形)
{}
virtual void Update() = 0;
bool Hit(IEnemy *enemy)
{
図形.Hit(&enemy->図形);
}
};
template <class TShape,class TMotion,class TSpeed>
class Enemy:public IEnemy
{
private:
TShape shape;
TMotion motion;
TSpeed speed;
public:
Enemy(TShape &&shape,TMotion &&motion,TSpeed &&speed):
IEnemy(shape),
shape(shape),
motion(motion),
speed(speed)
{}
void Update() override
{
motion.Do(shape,speed.Update());
}
}
int main()
{
using スライム = Enemy<Rect,Liner,Accel>;
スライム enemyA({0,0,10,10},{0,0,300,300},{0,0.1});
auto enemyB = Enemy<Line,Curve,Fix>({0,0,10,10,5},{0,0,300,300},{1});
}
共通の型でないと同じコンテナに入らないため、IEnemyを継承したEnemyをクラステンプレートにします。
見ての通りnewで動的確保する回数が減るので、その分高速になりますし、初期化子リストも使えるので冗長性が下がります、この辺りが主なメリットでしょう。
組み合わせ毎にusing宣言しておけば、より分かりやすくなります。
文法的にはテンプレートが分かりにくいかもですが、テンプレートの使い方としては基礎的で変わったテクニックとか使ってないので大丈夫だとは思います。
shapeは衝突判定時にIEnemyから取得出来ないと困るのでIEnemyのメンバーにしていますが、motionとspeedについては継承関係が不要になるため仮想関数呼び出しのコストもかからなくなり誤差レベルで速度が改善します。
こういう実装の場合あまり意味はないのですが、継承元が違っていても、同じ引数で同じ名前の関数があれば動作するようにもなるので、依存性を少し下げる事が出来ます。
一応この設計でもISpeed型を内部に持つ型等を作れば、動的に扱う事も可能ですが、外部スクリプト等で敵の動作の組み合わせを決めたい場合とかは使いにくいかと思います。
あと引数を間違えた時とかにどこの行でtypoしたかすら良く分からないエラーメッセージを拝むことが出来るようになるかもしれません。
#部分的なスクリプト
ruaとかjava scriptとかrubyとかで一部の処理を実装して、読み込むスクリプトを動的に決めて再コンパイルせずに処理を変えれるようにするやつ、関数ポインタとやってる事は近い
複数人で開発する場合やMODによる拡張に対応したい場合、非プログラマーでもスクリプトなら出来るって人は多いので特に重要
メインコードの開発環境+スクリプトの開発環境を用意したり学習する手間
デバッガの機能面が劣る事がありコンパイル時では無く実行時エラーになるのでデバッグの手間が多少増える
処理速度の面で多少劣るなどなど、欠点もありますが利点も多いので活用しましょう
#まとめ
色んな処理の分岐方法があるけど、どの方法が良いか考えて使いましょう。
少しややこしいですが、テンプレートを使った方法は強力なので是非とも習得しよう。
文法には規格と言う正解があり、アルゴリズムの高速化は数値で良さが分かりますが、設計は「何を作るか?」「どれぐらい仕様変更に耐えるか?」「コードの再利用の予定はあるか?」等によって正しい方針が変わるのが難しい所なのかなと思います。
設計においては手法やデザインパターンのメリット/デメリットを理解するより、自分が何を作ろうとしているかを把握しておく事の方が重要なんじゃないかと思います。
[2つ目の方法の参考記事]
ゲームのオブジェクト管理システムについて考える
※2015/6/5
続き書きました
http://qiita.com/mrdagon/items/009dfad2413faee2a827