<戻る

STGつくろー
その10 敵キャラの自動的な動きをYAGNIで


@ まずは難しく考えずにいこう

 STGでは、敵キャラが自動的に動きます。予定行動ではあるんですが、とにかく自動的に動きます。敵キャラごとに動きを定義したいんですが、クラスの派生でそれを行ってしまうと、すべての敵キャラのクラスを作る必要が出てきます。それは・・・さすがにやめたいところです。「次の行動位置を決める」部分は分離可能ですから、そこの部分だけを他のクラスCPosAssignerにやってもらって、敵キャラはそれをコンポジションとして持つことで移動を決めるとしましょう。これはクラスのある一機能を他のクラスに委譲するという「Bridgeパターン」です。こんなところで出てきました。

 やりたいことリストであるToDoリストは次のようになります。

ToDoリスト
・ 位置指定人(PosAssigner)はデフォルトで指定された移動距離と角度の差分位置を返す
・ 敵キャラは位置指定人からの指示に従って次の位置に移動する

さっそくCPosAssignerTestクラスを定義して、テストコード作成します。

PosAssignerTest.h
void testGoNext()
{
   // 位置指定人を生成
   sp<CPosAssigner> spPosAssigner(new CPosAssigner);

   // 初期設定
   spPosAssigner->SetMoveUnit(10.0);     // 単位移動距離
   spPosAssigner->SetDirectionZY(45.0, 45.0);   // 移動角度

   // 次の差分位置を出力
   assertEquals(0, spPosAssigner->GetNextPos().x);  // x座標
   assertEquals(0, spPosAssigner->GetNextPos().y);  // y座標
   assertEquals(0, spPosAssigner->GetNextPos().z);  // z座標
}

 初期設定として単位移動距離と初期移動角度を設定すると、GetNextPos関数によって次の差分位置を取得できる仕組みにする予定です。移動角度を変えれば、同じような行動定義を色々な角度で再現できる仕様にします。以下はそのイメージです。

 コンパイラエラーを出した後、まずはSetMoveUnit関数および、SetDirectionZY関数を定義します。これらの関数はCCharacterクラスですでに実装されていますね。それを流用してしまいます。GetNextPos関数は、

PosAssigner.cpp
POSITION GetNextPos()
{
   POSITION NextDifPos;

   // 次の位置の計算
   double Rad = 3.1415926535/180;
   NextDifPos.x = m_dMoveUnit
                           * cos(Rad * m_dZAxisDirect) * cos(Rad * m_dYAxisDirect);
   NextDifPos.y = m_dMoveUnit
                           * sin(Rad * m_dZAxisDirect) * cos(Rad * m_dYAxisDirect);
   NextDifPos.z = m_dMoveUnit
                           * sin(Rad * m_dYAxisDirect);

   return NextDifPos;
}

となります。これは、CCharacterクラスのSetDirectionZY関数内で定義されていた計算式です。何となくこの計算式の置き場所は変だと感じていましたが、これで多少すっきりしました。ここでは、次の位置さえ計算できれば良いので、差分座標は保持しません。
 これまで上記の計算をしていたCPosAssigner::SetDirectionZY関数には、単に角度を設定することに徹してもらいましょう。

 3つの関数および必要変数を定義すると、コンパイラエラーはなくなります。レッドシグナル、グリーンシグナルとも正常でした。ToDoリスト1段目終了です。



A敵キャラは位置指定人からの指示に従って次の位置に移動する

 ToDoリスト2段目です。ここでは、敵キャラであるCEnemyにCPosAssignerクラスを組み込む変更を行います。やることはいくつかあります。まず、CPosAssignerクラスを登録するSetPosAssigner関数を作ります。内部で動的に作成する手段もありますが、ここでは外部から与える方式にしました。また、敵キャラ以外も自動移動はするだろうとも思いますが、それはリファクタリングで検討します。

 次に、CEnemyクラスが持っているSetMoveUnit関数、SetDirectionZY関数の内部でCPosAssignerクラスの対応関数を呼び出すことで仕事を委譲し、またGoNext関数も少し修正します。

Enemy.cpp
void CEnemy::SetPosAssigner(sp<CPosAssigner> spPA){
   m_spPosAssigner = spPA;   // スマートポインタなので登録をすぐ変更できる
}

void CEnemy::SetMoveUnit(double unit){
   m_spPosAssigner->SetMoveUnit(unit);   // 委譲
}

void CEnemy::SetDirectionZY(double z_Axis_direct, double y_Axis_direct){
   m_spPosAssigner->SetDirectionZY(z_Axis_direct, y_Axis_direct);   // 委譲
}

void CEnemy::GoNext(){
   POSITION NextDifPos = m_spPosAssigner->GetNextPos();
   POSITION MyPos = GetPosition();

   SetPosition(
      MyPos.x + NextDifPos.x,
      MyPos.y + NextDifPos.y,
      MyPos.z + NextDifPos.z
   );
}

これでGoNext関数を呼ぶ度にm_PosAssignerの働きによって次の位置が取り出せます。最後に「CCharacterオブジェクトで同一のメンバ関数を仮想関数にする」ことを忘れてはなりません。

 この変更をこれまでのテストコードに通してみると、一発オールグリーンでした。少なくとも、これまでの結果と変わらない状態で変更が出来たことになります。これが、テストの自動化の威力ですね。



B リファクタリング

 この変更をさらにリファクタリングします。・・・しかし、今の段階でこれと言ったリファクタリングはありません。強いて候補があるとすれば、弾クラスであるCBulletも自動移動するものなので、同じ実装が出てくるはずです。ただ、それは弾クラスをさらに検討する時にリファクタリングすることにします。

 CPosAssignerクラスはまだ不十分な点があります。移動のパターンは外部から読み込むべきなのですが、その関数もまだ定義していません。ただ、それは必要になった時に実装します。それがYAGNIの原則です。

 ここまでのクラス図は以下のようになります。