STGつくろー!
その5 敵キャラを作る!
@ もうあっという間のはずです
敵キャラは多様ではありますが、とりあえず最低限入れたい事を考えます。敵キャラは移動します。弾も吐きます。そして弾に当たるとやっつけられます。耐久力もあります。これらを踏まえてToDoリストを作ります。
ToDoリスト ・ 敵の初期取得位置は(0, 0, 0)となる ・ 敵の位置変更後、変更した位置が取得できる ・ 敵は「打て」と命令されると弾を打つ ・ 敵は弾に当たると耐久力を減らす ・ 敵は耐久力が無くなると自分は破壊されたと宣言する
このうち、1段目から3段目まではCPlayerクラスとまったく同じです。よって、テストクラス、テストコードも類似してきます。同じ機能が入っているので、4段目に行く前にちょっとリファクタリングしてしまいます。
A 敵と自機を抽象化
敵と自機は「弾を打つ」という共通項があります。よってリファクタリングによって親クラスCShootingCharacterを作り、Shoot関数を設け、CPlayerと敵のクラスCEnemyはそのクラスを継承します。CShootingCharacterはCCharacterを親クラスに持ちます。コンパイラを通った後に、実行テストをしても問題は無いようです。すばらしい。
B 敵は弾に当たると耐久力を減らす
中ボスなどは一撃では倒れません。つまり耐久力があるわけです。ToDoリスト4段目では、弾に当たるということと、それによって耐久力が減るということの実装を目指します。テストコードを作るためには、耐久力を取得する関数も必要になります。結構大事です。
まずはテストコードを作ってしまいます。CEnemyTestクラスにtestClashBullet関数を作り、次のようなテストコードを作成します。
EnemyTest.h void testClashBullet(){
CEnemy Enemy1;
CBullet Bullet1(0, 0, 0);
Bullet1.SetAttackPower(50); // 弾の攻撃力を設定
Enemy1.SetHardiness(100); // 耐久力を設定
// 当てる(非破壊)
assertEquals(TRUE, Enemy1.Clash(&Bullet1)); // 弾に当たる(受ける)
assertEquals(100, Enemy1.GetHardiness()); // 敵の状態を取得
// もう一度弾を当てる(破壊)
assertEquals(FALSE, Enemy1.Clash(&Bullet1)); // 弾に当たる(受ける)
assertEquals(100, Enemy1.GetHardiness()); // 敵の状態を取得
}
大分複雑です。まず弾に攻撃力を与え、敵には耐久力を設定します。敵が弾に当たるというのを、オブジェクトをもらうことで対処することにしました。弾オブジェクトを貰い受けると、その攻撃力を得て、自分の耐久力を減らします。GetHardiness()関数で耐久力をチェックします。同じレベルの弾をもう一度当てて、さらに耐久力をチェックします(クロスチェックテスト)。
コンパイルすると、各種関数が無いというエラーがでます。これはいつもの通りです。まずは、CBullet::SetAttackPower関数から設定です。この関数内では、新しく設けたm_iAttackPowerメンバ変数に攻撃力を登録することにします。CEnemy::SetHardiness関数ではint型のm_iHardinessに耐久力を設定します。
CEnemy::Clash関数の内部では、引数のCBulletオブジェクトから攻撃力を貰い受けます。ということは、CBulletオブジェクトに攻撃力を取得できるGetAttackPower関数を追加する必要があります。この追加は簡単ですね。Clash関数の中はこうなります。
Enemy.cpp bool Enemy::Clash(CBullet *bullet)(){
// 耐久力を減らす
m_iHardiness -= bullet->GetAttackPower();
// 耐久力が0以下なら破壊
if(m_iHardiness <= 0)
return TRUE;
return FALSE;
}
CEnemy::GetHardiness関数は耐久力m_iHardinessを返すだけです。
以上の追加を行うと、コンパイラエラーはなくなります。この段階で実行するとレッドシグナルが灯りますので、CEnemyTest::testClashBullet関数内を、
EnemyTest.h void testClashBullet(){
CEnemy Enemy1;
CBullet Bullet1(0, 0, 0);
Bullet1.SetAttackPower(50); // 弾の攻撃力を設定
Enemy1.SetHardiness(100); // 耐久力を設定
// 当てる(非破壊)
assertEquals(FALSE, Enemy1.Clash(&Bullet1)); // 弾に当たる(受ける)
assertEquals(50, Enemy1.GetHardiness()); // 敵の状態を取得
// もう一度弾を当てる(破壊)
assertEquals(TRUE, Enemy1.Clash(&Bullet1)); // 弾に当たる(受ける)
assertEquals(0, Enemy1.GetHardiness()); // 敵の状態を取得
}
と太文字のように変更すれば、グリーンシグナルとなります。これで、弾を貰い受けて耐久力を減らすという、STGらしい部分が実装できました!ToDoリスト4段目終了。
C 敵は耐久力が無くなると自分は破壊されたと宣言する
ToDoリスト5段目は、破壊非破壊の判定関数の設定をしようというToDoリストです。これは、Clash関数でも仮取得は出来ていましたが、弾の判定を全部終わらせた後に倒したか倒していないかを判定する局面があるはずなので必要と判断しました。
やることはとても簡単です。CEnemyTestクラスにtestJudgeBroken関数を追加します。テスト内容は、弾で敵を攻撃して耐久力を減らしてもらってから、破壊か非破壊かの判定を行います。
EnemyTest.h void testJudgeBroken(){
CEnemy Enemy1;
CBullet Bullet1(0, 0, 0);
Bullet1.SetAttackPower(50); // 弾の攻撃力を設定
Enemy1.SetHardiness(100); // 耐久力を設定
// 当てる(非破壊)
Enemy1.Clash(&Bullet1);
assertEquals(FALSE, Enemy1.DidBroken()); // 破壊判定
// もう一度弾を当てる(破壊)
Enemy1.Clash(&Bullet1);
assertEquals(TRUE, Enemy1.DidBroken()); // 破壊判定
}
DidBroken関数は破壊されたかどうかを判定する関数として設定します。これは、m_iHardinessの値が0以下か否かを見ればよいだけですね。
Enemy.h bool Enemy::DidBroken(){
// 耐久力が0以下なら破壊
if(m_iHardiness <= 0)
return TRUE;
return FALSE;
}
このプログラム、どこかで見ました。そう、Enemy::Clash関数内です。重複があるということは、これはリファクタリング対象になりますね。今は、まだ手を付けずにテストを通すことだけ考えます。上のテストはレッドシグナル設定にしてありますので、TRUEとFALSEを入れ替えると、グリーンシグナルになります。これで、敵に関する今回のToDoリストは終了です。
D CEnemyクラスのリファクタリング
最後は楽しいリファクタリングです。つい先ほど出てきた破壊判定の重複部分をリファクタリングします。これは簡単で、Clash関数内の判定部分をDidBroken関数で置き換えるだけです。
Enemy.cpp bool Enemy::Clash(CBullet *bullet)(){
// 耐久力を減らす
m_iHardiness -= bullet->GetAttackPower();
// 破壊判定
return DidBroken();
}
一応実行テストをします。もちろんオールグリーンです。すばらしきかなTDD。
今の段階でリファクタリングすることは多分ありません。「でも、あれもしたい。これもしたい。」という先読みは厳禁です。それはYAGNIの原則に反してしまいます。この章までで、自機・敵を動かして、弾を発射させ、敵に当たった場合は耐久力を減らすところまでは実装できました。しかも、超シンプルです。次は、ちょっと大変そうな「当たり判定」と参りましょう!