シューティングゲームには自機や敵機、弾などさまざまな「もの」がでてきます。そして、それらの「もの」がゲームの中でそれぞれの特性に応じた「動作」をし、他のものに影響を与えることでゲームが進んでいくわけです。このような「もの」を抽象化して、ゲーム内の「もの」それぞれに「各自に応じた動作」を行わせる仕組みがあれば、ゲームのシステム全体は非常に見通しの良いものになりますね。もののリストを保持して、あとはそのリスト内のものに「動作を起こせ」とメッセージを送るだけでゲームが「進行」するわけですから。
今回は、そんな抽象化された「もの」を中心とするゲームシステムを作ってみましょう。このような抽象化された「もの」を「オブジェクト」という単位にまとめ組み合わせていく設計手法は「オブジェクト指向」的な設計と呼ばれていますが、今回はC++のクラス機能を利用してゲーム内の「もの」をオブジェクトとして扱うオブジェクト指向「風」のゲームシステムを考えてみることにします。
まず、ゲーム内のオブジェクトを抽象化する方法を考えてみましょう。オブジェクト指向では、このような場合「オブジェクトに共通する一般的な動作のインターフェース」を定義し、それぞれのオブジェクトはそのインターフェースを継承する(インターフェースを継承したクラスに基づいて生成される)形で「インターフェースを通して渡されるメッセージ」を受け取る「オブジェクト」になります。
たとえば、シューティングゲーム内のオブジェクトであれば「actionというメッセージを受け取ると行動する」というインターフェースをまず定義し、自機や敵など具体的なオブジェクトはそのインターフェースを認識するようにしてactionというメッセージを受け取った時に「どう行動するか」を個別に定義していくわけです。これで、actionというメッセージを送れば相手が自機であれ敵であれ弾であれ、「行動」を起こします。そして、そのメッセージを送る方は相手がどんな動作を起こすか、それどころか相手が「何ものなのか」すら知る必要がなくなるわけです。必要なのは、「相手がactionというメッセージを受け取れること」なのですから。
今回は、オブジェクトとしてまず自機と弾を考えることにします。これらのオブジェクトが備えるべきインターフェース(「外」から見た機能の呼び出し形式)は、どのようなものでしょうか? まず、それぞれに固有の行動を起こすactionが必要でしょう。あと、現在位置を取得したり設定するインターフェース、またゲーム内での「大きさ」もわかると良いですね。また、キャラクタの描画についても指定したデバイスコンテキストに自分で描いてくれると楽です。これらの抽象化されたインターフェースを関数として定義した「ゲーム内のオブジェクトに共通のインターフェース」クラスCObjectは、以下のような感じになるでしょう。このクラスから派生させた具体的なオブジェクトを定義するクラスには動的なデータが含まれデストラクタが必要になるので、仮想デストラクタも定義しておきます。
class CObject { public: virtual void action()=0; virtual int getX()=0; virtual int getY()=0; virtual void setX(int)=0; virtual void setY(int)=0; virtual int getWidth()=0; virtual int getHeight()=0; virtual void draw(int,int,LPDWORD)=0; virtual ~CObject() {} };
以上のクラスでゲーム内のオブジェクトが受け取るメッセージを抽象的に定義できました。あとは、この抽象クラスの純粋仮想関数を具体的に定義した各オブジェクト用のクラスを定義すれば良いわけです。そうすれば、その派生クラスのオブジェクトを「CObject(実装上はCObject型ポインタ)として」扱えるようになります。
ただ、実際のゲームは自機などのオブジェクトだけでは成立しません。オブジェクト同士を関連付け、ゲーム内のオブジェクトに行動を起こさせてゲームを進行させるゲームの「世界」を定義するクラスが必要だからです。この「ゲームクラス」CGameは、メインプログラムでそのオブジェクトを作成しメインプログラムによって駆動されることで実際のゲームを動作させることになります。
今回定義するクラス | |
---|---|
ゲーム本体 | CGame |
ゲーム内のオブジェクトの抽象的定義 | CObject |
自機 | CChr : CObject |
弾 | CBullet : CObject |
ゲームクラスでは、ゲーム内のオブジェクトをリストとして保持し、リスト内のオブジェクトに順次メッセージを送っていくことでゲームの時間を進めるようにしましょう。具体的には、保持しているオブジェクトに順次行動を起こさせるaction、そして現在のゲームの状態を描画するdrawを順次呼び出してゲームを進行させるようにしました。また、他にゲームの世界に新たなオブジェクトを追加するaddObjectも用意し、これらの関数が「ゲームを作成・実行するメインプログラム」とのインターフェースになります。
さらに、ゲーム内のオブジェクトから受け取るメッセージとしてゲーム世界に弾を発射するfire、移動後に自分の位置を調整するadjestも定義しました。
class CGame { public: CGame(int,int); void addObject(CObject *); void action(); void fire(CObject *); void adjest(CObject *); void draw(HDC,int,int); ~CGame(); private: int iWidth,iHeight; LPBYTE lpBuf; LPDWORD lpScreen,lpBG; LPBITMAPINFO lpbiScreen; listlObj,lBul; void updateScreen(); };
このゲームクラスはメインプログラムの方でオブジェクトを作成し、メインループからactionを呼び出してゲームを進めます。メインプログラムは、ゲームを描画するウインドウhwMainとゲームクラスのオブジェクトlpGameを作成したら、以下のようなメインループに入るようにしました。
while (1) { /* メインループ */ if (PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) { if (!GetMessage(&msg,NULL,0,0)) /* メッセージ処理 */ return msg.wParam ; TranslateMessage(&msg); DispatchMessage(&msg); } else { /* ゲームを1ステップ進める */ lpGame->action(); InvalidateRect(hwMain,NULL,FALSE); UpdateWindow(hwMain); Sleep(5); } }
メインループでウインドウがUpdateWindow()されると、ウインドウのWM_PAINTメッセージ処理でlpGame->draw()を呼び出してゲームの描画を行います。
case WM_PAINT: hdc=BeginPaint(hwnd,&ps); lpGame->draw(hdc,2,2); /* ゲームの状態を描画 */ EndPaint(hwnd,&ps);
CGameクラスのaction()は、以下のようにしました。まずゲーム内オブジェクトのポインタを保持しているリストlObjのイテレーターで順次オブジェクトのaction()を呼び出し、actionの結果ゲーム領域の「外」に出たオブジェクトを削除します。続いて各オブジェクトのactionの処理中に発射された弾丸をlBulに保持しておいて全オブジェクトのaction終了後にまとめて新規のオブジェクトとしてlObjに追加、という感じですね。その後、updateScreen()でゲームの状態をビットマップに描画します。
void CGame::action() { list<CObject *>::iterator i=lObj.begin(); lBul.clear(); while(i!=lObj.end()) { (*i)->action(); if ((*i)->getX()+(*i)->getWidth()<0 || (*i)->getX()>iWidth || (*i)->getY()+(*i)-%gt;getHeight()<0 || (*i)->getY()>iHeight) { delete *i; (*i)=NULL; } i++; } lObj.remove(NULL); i=lBul.begin(); while(i!=lBul.end()) lObj.push_back((*i++)); updateScreen(); }
ゲームを描画するupdateScreen()では、まず表示用DIBピクセル列lpScreenに背景用DIBピクセル列lpBGをコピーしてからゲーム内オブジェクトのdraw()を呼び出し、各オブジェクトに自分自身を描かせるようにします。
void CGame::updateScreen() { CopyMemory(lpScreen,lpBG,iWidth*iHeight*4); list<CObject *>::iterator i=lObj.begin(); while (i!=lObj.end()) { (*i)->l;draw(iWidth,iHeight,lpScreen); i++; } }
なお、弾丸のリストlBulには、オブジェクトがCGameのfire()を呼び出した段階で新規の弾丸オブジェクトのポインタが追加されます。fireは各オブジェクトのaction()内から呼び出され、各オブジェクトから渡された弾丸オブジェクトをlBulリストの末尾に追加します。
void CGame::fire(CObject *oArg) { lBul.push_back(oArg); }
以上で、ゲームの世界とその世界を利用するメインプログラムという「ゲーム内のオブジェクトを扱う仕組み」ができました。次は、ゲーム内のオブジェクトを具体化させていきましょう。
まず、自機は以下のようなCChrクラスで定義します。このクラスでは「カーソルキーで移動、スペースで弾を撃つ32×32ピクセルのキャラクタ」を自機として定義し、CObjectのインターフェースを実装します。また、その他に初期位置とゲーム本体のCGameを渡して初期設定を行うコンストラクタも用意しましょう。
class CChr:public CObject { public: CChr(int,int,CGame *); virtual void action(); virtual int getX(); virtual int getY(); virtual void setX(int); virtual void setY(int); virtual int getWidth(); virtual int getHeight(); virtual void draw(int,int,LPDWORD); virtual ~CChr(); private: CGame *game; DWORD dwMask; LPDWORD lpPixels; int iX,iY,iDx,iDy,iWidth,iHeight; virtual DWORD getPixel(int,int); };
コンストラクタでは、32×32ピクセルの32ビットDIBバッファを用意して適当なキャラクタを描いておきます。
CChr::CChr(int iArg1,int iArg2,CGame *gArg) { iWidth=32; iHeight=32; iX=iArg1; iY=iArg2; game=gArg; /* キャラクタ用ピクセル列確保 */ lpPixels=(LPDWORD)GlobalAlloc(GPTR,iWidth*iHeight*4); for (int i=2;i<30;i++) /* ピクセル列を設定 */ for (int j=2;j<30;j++) lpPixels[j+i*32]=0xff00; dwMask=0; }
次いでCObjectのaction()をオーバーライドして、具体的な「行動」を決めましょう。とりあえず、今回はカーソルキーで移動後、自機の位置を調整するためCGameオブジェクトのadjest()を呼び出します。さらに、スペースキーが押されていたら弾オブジェクトを生成し、そのポインタをCGameオブジェクトに渡すようにしました。
void CChr::action() { if (GetAsyncKeyState(VK_UP)<0) iY-=2; if (GetAsyncKeyState(VK_RIGHT)<0) iX+=2; if (GetAsyncKeyState(VK_DOWN)<0) iY+=2; if (GetAsyncKeyState(VK_LEFT)<0) iX-=2; game->adjest(this); if (GetAsyncKeyState(VK_SPACE)<0) /* 弾発射 */ game->fire(new CBullet(iX+12,iY-6,0,-10,game)); }
自機の描画処理を行うdrawでは、ゲームオブジェクトからゲームを描画する32ビットDIBピクセル列のポインタと幅・高さを受け取って自分自身のピクセル列をdwMaskを抜き色として透過描画します。
void CChr::draw(int iArg1,int iArg2,LPDWORD lpBMP) { int x,y; for (int i=0;i<iHeight;i++) for (int j=0;j<iWidth;j++) { x=iX+j; y=iY+i; if (x>=0 && x<iArg1 && y>=0 && y<iArg2 && getPixel(j,i)!=dwMask) lpBMP[x+y*iArg1]=getPixel(j,i); } }
弾クラスCBulletは、8×8ピクセルのキャラクタが「上」に向かって移動するようにしました。こちらはキャラクタを長方形にして透過処理なしで描画できるようにしてあります。
プログラムを実行すると自機が表示されるので、カーソルキーで移動したりスペースキーで弾を発射したりしてみてください。