yaneSDKによるゲーム製作メモ:第3回「キャラの管理」

一回書いたやつが更新時のサーバーエラーで全部消えた。鬱だ。
まあフォームに直に書いてた自分が悪いといえばそうなんだけど…。

さて、ゲームには多くのキャラクタが登場する。
ここで言うキャラクタとは大雑把に言うと、画像を持ち、
画面上に表示されるもので背景以外のもの、といった感じか。
具体的にはシューティングの自機や弾、敵機。場合によってはスコアやパワーアップなどの
情報の表示もすべてキャラとして扱うかもしれない。
細かい定義は製作者に依存するが、ここではキャラの管理について説明するため、
比較的不定期に複数が生成、削除を繰り返す敵機、弾などを主に考える。

一般的なシューティングでは、敵がひょこひょこ出てきてわらわら弾を吐いていくし、
自機も一度に大量の弾を撃てることが多いだろう。
これらの敵機や弾をいちいち個別に扱っていたのではとてもコーディングが終わらないし、
そもそも「何発目の弾が今どこにあって」という瑣末な情報が必要になることはまずないだろう。
(敵機の場合情報が必要になることもあるかもしれないが)
そこで、これらのオブジェクトについて、「生成、移動、描画、解体」を自動化する管理クラス
(キャラクタコントローラ)を作る。

コントローラは、各キャラオブジェクトへのポインタをまとめたチェイン
(実体はstd::listなどの可変長配列)を持ち、
生成時にキャラをチェインに追加し、解体時にチェインから削除する。
具体的な実装は、まずCreate()関数が(パラメータ付きで)呼ばれると、
以前やったパラメタライズドファクトリーによって対応するキャラが生成され、
ポインタがチェインに組み入れられる。
移動、描画については、iterator(要素を順繰りに指すポインタのようなもの)を用いた
ループによって、チェインの先頭から各キャラの移動、描画関数を呼び出す。
この過程で、削除フラグが立っているオブジェクトがあると、ポインタがチェインから消される
(同時にオブジェクトを解体する)。
STLの配列やiteratorを知ってないとよくわからないかもしれないが、
実のところ仕組みは全然難しくない。リストを持っておいて順番に呼んでいるだけだ。

で、自分の実装はこんな感じ。(鵜呑みにしないでねw)

//グローバル
//キャラの種類
enum EChara {
eEnemy001,
eEnemy002,
eMyship,
eShot,
eBullet
};

//キャラクインスタンスをリストで管理するコントローラ

class CCharaControl{
public:
//チェインを得る
list<CChara*>* GetList(){ return& m_chara_list; }

/*生成
nChara : キャラの種類
x,y : 座標
*/
smart_ptr<CChara> Create(int nChara, int x, int y){

//パラメタライズドファクトリー
CChara* pChara = NULL;
switch(nChara){
case eMyship :
pChara = new CMyship(x,y);
break;
case eEnemy001 :
pChara = new CEnemy(1,x,y);
break;
case eEnemy002 :
pChara = new CEnemy(2,x,y);
break;
case eShot :
pChara = new CShot(x,y);
break;
case eBullet :
pChara = new CBullet(x,y);
//などなど…
}

smart_ptr<function_callback> fn(
function_callback_v::Create(&CCharaControl::DeleteChain,this,pChara)
);

smart_ptr<CChara> p(
pChara,
new nonarray_callback_ref_object<CChara>(pChara,fn)
);

//チェインに追加する
GetList()->insert(pChara);

return p;
}

//解体(自動的にコールバックされる)
//チェインそのものが先に削除されるとき、中身をすべて消去する
//明示的に呼ぶ必要はない
void DeleteChain(CChara* p){
list<CChara*>::iterator it = GetList()->begin();
while (it!=GetList()->end()){
if ((*it) == p){
it = GetList()->erase(it);
}else{
it++;
}
}
}

//移動。各インスタンスを走査し、isValid()==trueならば各移動メソッドを呼び出す。
//そうでなければチェインから削除する。
void OnMove(const CKey2* k){
list<CChara*>::iterator it = GetList()->begin();
while (it!=GetList()->end()){
if )((*it)->isValid())({
(*it)->OnMove(k);
it++;
}else{
it = GetList()->erase(it);
}
}
//↑の意味ね(list_chainならこう書ける)
//GetList()->for_each_valid(CChara::OnMove,k);
}

//描画。各インスタンスを走査し、isValid()==trueならば各描画メソッドを呼び出す。
void OnDraw(const smart_ptr<ISurface>& lp){
list<CChara*>::iterator it = GetList()->begin();
while (it!=GetList()->end()){
if )((*it)->isValid())({
(*it)->OnDraw(lp);
}
it++;
}
}

private:
list<CChara*> m_list_chain; //チェイン
};

先に注釈を。
やね本1にはチェインにlist_chainを使えと書いてあるが、後々描画の重ね順を考えるときに、
任意の位置にinsertできないのでここでは普通にstd::listを使っている。
それと、ここでは自機もチェインに入れているが、実際は自機は単体でかつ特別な存在で、
自機の情報を使って敵や弾を動かしたりするかもしれないので、チェインに入れずに個別に
扱うほうがいいかもしれない。

では解説。まずCreate()関数について。
本来はもっといろいろパラメータを渡すかもしれないが、ここでは簡単のためキャラの種類と
座標だけにしている。あとはシーンのときと一緒。

その下のfunction_callback云々のくだりは、チェインの中身がまだ残っている状態で先に
コントローラが解体されるときに、自動的にDeleteChain()を呼び、チェインをカラにする
仕組み…と思われる。
このように自動的に関数を呼ぶようにする仕掛けを関数コールバックと言うようだが、
コレはまだ自分もよくわかってない(ぇ

次にOnMove()関数。
iteratorはインクリメントすると次の要素を指すので、これを使ったループで
全要素のOnMove()を呼び出す。isValid()がfalseを返したときは、(画面外に出たりしたから)
削除してくださいという知らせなのでeraceする。erace後のiteratorは自動的に次の要素を
指すので、インクリメントは必要ない。

補足。やね本1にもある通り、各キャラのOnMove()内でdelete thisして削除する方法を使った場合、
それを通知しないとiteratorが不正な値を指してしまうので、削除フラグだけ立てておいて、
次のOnMove()でiterator経由で削除する。

最後にOnDraw()関数について。
これはOnMove()とほぼ同じ。削除は行う必要はない。

結局、使い方は簡単で、シーンクラスにこれを持たせ(コンポジションでもいいし、
コンストラクタでの動的生成でもいい)、シーンの初期化やOnMove()の中で適宜Create()し、
シーンのOnMove()、OnDraw()でコントローラのOnMove()、OnDraw()を呼べばよい。
解体については気にする必要はない。

またまた注意点だが、この実装だと、Create()によって返るsmart_ptrを受け取っておかないと、
生成と同時に解体され、チェインが不正になる。
自機の場合は普通にsmart_ptrで受けて後々いじるのに使い、
敵機や弾などの不特定多数オブジェクトはsmart_vector_ptrにinsertしておけばよいだろう。

//こんな感じ
CCharaControl cc;
smart_ptr<CChara> m_spMyship;
smart_vector_ptr<CChara> m_svpEnemy;

m_spMyship = cc.Create(eMyship,0,0);
m_svpEnemy.insert(cc.Create(eEnemy001,10,10));

ところで、OnMove()の引数でキー入力を渡しているのに気づいただろうか。
yaneSDKのシーン基底クラスではOnDraw()と同じく描画サーフェースを渡しているのだが、
役割ははっきり分けたほうがいいだろうと。
シーンからならouter.GetKey()できるので、これで自機を操作できる。
さらに、自機と反対に動く敵なんかも作れそう。

基本はこんなところ。
自分はコントローラに他に当たり判定チェックなんかも入れているが、それはまた後日。

次回はこれに扱われるキャラクタクラスを書こう。(順番逆?)