今回は、当たり判定とゲーム内の時間に応じて敵を出すなど一定の処理を行うコマンド(スクリプト)機能を追加します。これで、ゲームシステムの基本的な部分はほぼ完成ですね。
当たり判定の前に、クラス階層を整理しておきましょう。前回までCObjectクラスでは「実装を持たないインターフェース(純粋仮想関数と仮想デストラクタ)」のみを定義していましたが、これを改めオブジェクトに共通する基本的な属性と機能を実装します。ただし、action()などの純粋仮想関数が残っているため、CObjectクラスは依然インスタンスを直接生成できない抽象クラスです。属性はprivate変数に、属性にアクセスする関数群はpublicあるいはprotectedな仮想関数になっているので、やろうと思えばCObjectクラスの派生クラスでCObjectクラスの実装とは異なる独自の属性・処理を定義することも可能でしょう。
あと、当たり判定の結果キャラクタを「破壊」するためにCObjectクラスに耐久力、各キャラクタの当たり判定の際に相手に与えるダメージ、キャラクタタイプを属性として追加しましょう。この内、キャラクタタイプは「味方と敵」「機体と弾」という種別を設定しておきます。値は、自機は1、自機の弾は2、敵機はー1、敵の弾はー2としました。各キャラクタは特定のキャラクタタイプのオブジェクトとのみ衝突を起こすようにして、その判定を関数checkType()で行うようにしましょう。たとえば、「敵の機体や弾」(敵方すべて)と衝突を起こす自機(CChrクラス)のcheckType()は以下のようになります。
bool CChr::checkType(int iArg) { return iArg<0; }
当たり判定は、まず以下のような「ある点がキャラクタ領域に含まれるか」を判定する関数inside()を定義します。キャラクタ領域は(getX(), getY())-(getX()+getWidth()-1, getY()+getHeight()-1)までの長方形領域なので、引数で渡された座標がこの領域に入っているか、判定するようにしました。
bool CObject::inside(int iArg1,int iArg2) { return (getX()<=iArg1 && getX()+getWidth()>iArg1) && (getY()<=iArg2 && getY()+getHeight()>iArg2); }
当たり判定を行う関数collision()は、このinside()を使って引数で指定されたCObjectオブジェクトとの当たり判定を行います。今回は、「自分の領域の四隅の内一つでも相手の領域に入っていれば」当たりと判定し、この判定を自分対相手、相手対自分に対して行うようにしました(片方だけだと一方が相手の「内部」に含まれる場合の判定ができない)。当たったと判定された場合は、相手からのダメージを受ける(相手のgetDamage()を自分の耐久力から引く)処理を行います。
bool CObject::collision(CObject *oArg) { /* oArgとの当たり判定 */ if (oArg==NULL || oArg==this || !checkType(oArg->getType())) return false; if ((inside(oArg->getX(),oArg->getY()) || inside(oArg->getX()+oArg->getWidth()-1,oArg->getY()) || inside(oArg->getX()+oArg->getWidth()-1,oArg->getY()+oArg->getHeight()-1) || inside(oArg->getX()+oArg->getWidth(),oArg->getY()+oArg->getHeight()-1)) || (oArg->inside(getX(),getY()) || oArg->inside(getX()+getWidth()-1,getY()) || oArg->inside(getX()+getWidth()-1,getY()+getHeight()-1) || oArg->inside(getX()+getWidth()-1,getY()+getHeight()-1))) { oArg->addLife(-getDamage()); return true; } return false; }
次に、ゲーム中に敵を出すなどあらかじめ決められた処理を行うスクリプトシステムを作ってみましょう。今回作るスクリプトは、以下のようなCCommandクラスで定義されるコマンドオブジェクトを実行するシステムにします。
class CCommand { public: CCommand(DWORD dwArg1,DWORD dwArg2,DWORD dwArg3) { setCount(dwArg1); setCode(dwArg2); setArg(dwArg3); } DWORD getCount() { return dwCount; } DWORD getCode() { return dwCode; } DWORD getArg() { return dwArgu; } void setTime(DWORD dwArg) { dwTime=dwArg; } void setCode(DWORD dwArg) { dwCode=dwArg; } void setArg(DWORD dwArg) { dwArgu=dwArg; } private: DWORD dwCount,dwCode,dwArgu; };
コマンドオブジェクトには、実行するゲーム内時間(dwCount)、コード(dwCode)、引数(dwArgu)の3つの属性を持たせます。今回は、コードとしてCEnemy1クラスのインスタンスを敵として生成するコード1を定義し、引数には上位16ビットに登場時のX座標、下位16ビットにY座標を設定するようにしましょう。ただし、コマンドオブジェクトが保持しているのはコードや引数などの「値」だけで、実際にその値を解釈して実行するのはCGameオブジェクトです。
続いてこのコマンドオブジェクトを保持するためにCCommandListクラスを定義します。このクラスは、addCommand()で追加されたコマンドオブジェクトをvectorに順次格納し、getCommandで指定されたゲーム内時間に対応するコマンドオブジェクトを取り出せるようにします。今回は、「コマンドは、ゲーム内時間の順序に従って追加する」「ゲーム内で時間を進める時には必ず現時間に対応するコマンドをgetCommand()で取り出す」という条件をつけることにしましょう。この条件によって、getCommand()は「現在の参照位置に指定された時間のコマンドがあるか」調べ、あればそのコマンドを返して参照位置を一つ進めるだけで良くなります。「指定される時間は一つずつ進み後戻りしない」「指定された時間までのコマンドはすべて確実に実行済みで参照位置もそれらのコマンドの後にある」ため「現在の参照位置のコマンドの時間(getCount()の値)が指定された時間でなければ、そのコマンドの時間は指定された時間以降のものである」ことが保障されるからです。
class CCommandList { public: CCommandList() { dwIndex=0; } void addCommand(DWORD dwArg1,DWORD dwArg2,DWORD dwArg3) { vtCmd.push_back(new CCommand(dwArg1,dwArg2,dwArg3)); } CCommand * getCommand(DWORD dwArg) { if (dwIndex==vtCmd.size()) return NULL; if (vtCmd[dwIndex]->getCount()==dwArg) { dwIndex++; return vtCmd[dwIndex-1]; } return NULL; } private: vectorvtCmd; DWORD dwIndex; };
プログラムでは、最初にCCommandオブジェクトを生成しコマンドを追加してからそのCCommandListオブジェクトをCGameに渡します。
CCommandList *cmTmp=new CCommandList(); cmTmp->addCommand(10,1,0x00000000); cmTmp->addCommand(10,1,0x01a00000); cmTmp->addCommand(200,1,0x00800000); cmTmp->addCommand(500,1,0x00800000); cmTmp->addCommand(1000,1,0x00000000); cmTmp->addCommand(1000,1,0x00800000); lpGame=new CGame(512,512,cmTmp);
実際のコマンド実行は、CGame::action()内で行います。
CCommand *cmCmd; while((cmCmd=cmList->getCommand(dwCount))!=NULL) { switch (cmCmd->getCode()) { case 1: int iX=cmCmd->getArg() >> 16; int iY=cmCmd->getArg() & 0xffff; lObj.push_back(new CEnemy1(this,iX,iY)); break; } }
プログラムを実行すると自機が表示されるので、カーソルキーで移動したりスペースキーで弾を発射したりしてみてください。自機の耐久力は3、敵機や敵弾のダメージは1に設定してあるので、敵機や敵弾に3回当たると自機が消滅します。
今回は、ゲーム内の時間(CGame::action()の実行回数)とフレームレートも表示するようにしてみました。私の環境ではだいたい50FPS程度ですね。