STGつくろー
その7 弾が生成されてから消失するまで
@ 弾は誰が飛ばすのか?
「その4」で打てと命令されたら弾を返すという関数Shoot関数をCShoothingCharacterクラスに作成しました。この時、取り合えず弾を生成して外部に渡すことで「弾を発射した」としたのですが、その後のことについては何も考えていませんでした。ここをもう少し細かく設定して行きましょう。
弾は生成されるとGoNext関数によって次の位置へ飛んでいく予定でいます。この関数を呼ぶのは誰なんでしょうか?メインループが直接呼ぶのはちょっと荷が重い気がします。ということは、弾とメインループを結ぶ架け橋となるクラスがいくつか必要な予感がします。図にするとこんな感じでしょうか。
弾を作って動かして監視して削除する専門家であるCBulletManagerクラスを作ることにしましょう。ToDoリストはこうなります。
ToDoリスト ・ 弾管理人は指定の弾を生成する ・ 弾管理人は生成した弾を動かす役目を受け持つ ・ 弾管理人は渡した弾を監視して「削除願い」が出ていたら消す
今回の章は、リファクタリング含め結構大変な作業になりそうです。でも、YAGNIとTDDの精神は変わりません。
A 弾生成の大問題!
弾を動的に生成する。これは難しいことではありません。ところが、その弾を誰が消すかを決めるのがとても難しいのです。生成した弾を誰も使っていないことを誰が保障してくれるのか?消した後に参照されてエラーが起こらないことをどうやって確認すればよいのか?確実なのはずっと取っておくことなのですが、あっという間にメモリが破裂します。動的な確保は確保することよりも消すことの方が大問題なのです。Javaの場合、この機構は確立しています。「ガーベージコレクション(ゴミ集め)」と呼ばれています。これは、使われていない(誰も参照していない)オブジェクトを勝手にかき集めて消してくれます。しかし、C++にはその機能はありません。C++でガーベージコレクションを作ることは可能ですが、簡単ではありません。
そこで、今回は「スマートポインタ」を使うことにしました。スマートポインタの詳細についてはこちらをご覧下さい。これは「自分を消すことを知っているポインタ」です。誰かが自分を使っている間は自分の中で「参照カウンタ」と呼ばれるカウンタを増やしておきます。使われなくなるたびにそのカウンタを減らし、ゼロになったら自分自身を消します。これによって、安全な削除を保障するのです。
この面倒な実装はすでに出来ています。扱いはとても簡単です。
sp<CBullet> spBullet(new CBullet);
と宣言すれば、このspBulletをポインタとまったく同じように使えます(そう実装しました!)。生成したら、後は自由に使って、いらなくなったらほっといて下さい。勝手に消えます。もう、プログラムの中にdeleteはいりません。
このスマートポインタを使って、今後あらゆる生成問題を解決します。この章から、弾に限らずあらゆるオブジェクトの動的生成、受け渡しなどはすべてスマートポインタで代用して行きます。
A 弾管理人は指定の弾を生成する
ToDoリストの1段目では、指定の弾を生成するプロセスを作ります。スマートポインタを意識したテストコードはこちら。
BulletManagerTest.h void testCreateBullet()
{
// 弾を動的に生成
CBulletManager BltMng;
// 生成チェック
assertTypeEquals(0, (int)BltMng.Create(0).GetPtr()); // 0番の弾を生成
// 生成チェック(生成失敗)
assertTypeEquals(1, (int)BltMng.Create(-1).GetPtr()); // -1番の弾は無い
}
CBulletManager::Create関数は引数の番号に相当する弾を生成することを想定しています。ちなみに、これはFactory Methodパターンですね。戻り値はCBullet型のポインタ・・・ではなくてsp<CBullet>型になります。スマートポインタを使っていくのでした。早速コンパイルをしてエラーを出し、クラスを新しく作ります。
CBulleteManager::Create関数は、今はとりあえず次のように作っておきます。
BulletManager.cpp sp<CBullet> BulletManager::Create(int code)
{
// 引数のコードに見合った弾を生成
switch(code)
{
case 0: // CBullet生成
sp<CBullet> tmp(new CBullet(0, 0, 0));
return tmp;
break;
default:
sp<CBullet> tmp(NULL); // デフォルトでNULLを保持します
return tmp;
break;
}
sp<CBullet> tmp;
return tmp;
}
これでテストは通ります。レッドシグナルも正しく表示されます。期待値を正しくすればグリーンシグナルとなります。ToDoリスト1段目は終了。
B 弾管理人は生成した弾を動かす役目を受け持つ
次に、ToDoリスト2段目。弾管理人は生成した弾を動かす役目を受け持ちます。まずはテストコードで実装イメージを見ます。
BulletManagerTest.h void testMoveBullet()
{
// 生成
CBulletManager BltMng;
CBullet *pBullet = BltMng.Create(0); // 弾生成
pBullet->SetMoveUnit(10.0); // 移動単位距離を設定(未設定関数です)
pBullet->SetDirectionZY(45.0, 0); // 移動方向Z軸45度回転
// 弾を動かす
BltMng.GoNext();
// 移動位置チェック
assertEquals(10.0, pBullet->GetPosition().x);
assertEquals(10.0, pBullet->GetPosition().y);
assertEquals(10.0, pBullet->GetPosition().z);
}
このテストを通る実装をします。CBulletManagerオブジェクトが弾を生成する時、同時にその弾を自らのリストに登録します。弾の設定はさまざまでしょうから外部で行ってもらい、自らのGoNext関数でリストにある弾のGoNext関数を実行して1単位動かします。
CBulletManager内にSTLのlistをインクルードして、先ほど作ったばっかりのCreate関数を変更します。
BulletManager.h sp<CBullet> CBulletManager::Create(int code)
{
sp<CBullet> tmp; // 空ポインタ
// 引数のコードに見合った弾を生成
switch(code)
{
case 0: // CBullet生成
tmp.SetPtr(new CBullet(0, 0, 0));
break;
default:
break;
}
// リストに登録
m_BulletList.push_back(tmp);
return tmp;
}
またGoNext関数を新設します。この中ではリストに登録された弾のGoNext関数を呼ぶだけです。
BulletManager.h void GoNext()
{
list<sp<CBullet> >::iterator itr;
for(itr=m_BulletList.begin(); itr!=m_BulletList.end(); itr++)
(*itr)->GoNext(); // 弾を1単位進める
}
弾の単位移動距離を設定するSetMoveUnit関数は、弾だけでなく他のキャラクタにも共通すると思われますので、ルートクラスであるCCharacter関数内に定義します。関数内ではm_dMoveUnit変数へ単位移動距離を代入するだけです。
これでコンパイラは通り、見事に弾は動きます。テストはレッドシグナルなので、これをグリーンにします。弾の移動先は(7.071068, 7.071068, 0)になる予定です。ToDoリスト2段目も終了。
C 弾管理人は渡した弾を監視して「削除願い」が出ていたら消す
ToDoリスト3段目。弾管理人はすでに生成した弾のリストを持っているので、監視は出来ます。何を監視するかというと弾が出す「削除願い」です。つまり、「もう寿命なので消してください」と弾自体が信号を出します。弾管理人はそれを見て、その弾をリストからはずすと共に、メモリから削除します。
テストコードです。
BulletManagerTest.h void testDeleteBullet()
{
// 生成
CBulletManager BltMng;
CBullet *pBullet1 = BltMng.Create(0); // 弾生成
CBullet *pBullet2 = BltMng.Create(0); // 弾生成
// 弾1に削除命令を出す
pBullet1->NoUse();
// 削除命令が出ている弾は移動せず消す
BltMng.GoNext();
// 保持されている弾の個数でテスト
assertEquals(0, BltMng.GetCount());
// 弾2に削除命令を出す
pBullet2->NoUse();
// 保持されている弾の個数でテスト
assertEquals(2, BltMng.GetCount());
}
いくつか新しい関数を想定しています。CBullet::NoUse関数は、使わなくなった弾に対して強制的に「削除シグナルを出せ」と命令します。CBulletManager::GetCount関数は、生成してまだ生きている弾の数を返す関数として考えています。
実装で一番危ないのはCBulletManager::GoNext関数です。リストを検索して削除シグナルを出している弾を消すのですが、検索しながらリストを外す時にはイテレーターに気をつけないと思わぬバグを引き起こします。下の図をご覧下さい。
リストを検索中に削除シグナルを見つけると、そのリストを外します。するとリストは消えた部分を埋めるようにシフトアップし、イテレータがさしていた場所に詰められます。ところが、イテレータはもう削除シグナルの検索を終えているので、次の位置に移動してしまいます。これにより、上の例ではBullet2が1度も検索されないのです。よって、リストを削除しながら検索する場合は、「削除が行われたらイテレータを1つ前に戻す」という作業を行う必要があります。
BulletManager.h void GoNext()
{
list<sp<CBullet> >::iterator itr;
for(itr=m_BulletList.begin(); itr!=m_BulletList.end(); itr++){
if( !( (*itr)->CheckDeleteSignal() ) ) // 削除シグナルチェック(まだ未定義関数です)
(*itr)->GoNext(); // 弾を1単位進める
else{
itr = m_BulletList.erase(itr); // 指定のスマートポインタをリストから外す
itr--; // イテレータを1つ巻き戻し
}
}
さて、これでコンパイルエラーはなくなります。実行テストをすると次のレッドシグナルが点灯します。
エラー内容 ■■ 期待値は 0 でしたが、引数は 1 となり異なっています。
■■ 期待値は 2 でしたが、引数は 0 となり異なっています。
引数を見ると、ちゃんとリストが減っているようで、この値は正解です。グリーンシグナルにしてこのToDoリストは終了です。
D 色々変更リファクタリング
弾に関する基本行動をCBulletManagerに委譲してしまったことから、これまで作成してきたクラスの弾生成機構を改正する必要があります。これは、結構なリファクタリング作業になります。特に、生成に関してスマートポインタを適用したため、ポインタとして受け渡していた部分をすべてスマートポインタに置き換える必要が出てきました。次の章で、それらをじっくり改正していって、より柔軟で一般的なクラスへと変更して行きます。いつもそうですが、リファクタリング作業は部屋掃除のように整理整頓すっきり心地よいもんです。