ゲーム開発最前線『侍』はこうして作られた―アクワイア制作2課の660日戦争
前回は自機と弾を出してみたので、今回は敵も追加してみましょう。そして、背景のスクロールやゲーム内の「時間」に応じて敵を出すなどゲーム全体のシステムについても考えてみることにします。
今回は、32×32ピクセルであちこち動きながら弾を撃ってくるキャラクタを敵にしてみます。出現位置は、画面上端の中央付近、動きは一度画面中ほどまで降りてきてしばらくふらついてからまた下の方に移動して消えるようにしましょう。また、その間乱数で弾を撃つ、球を打つ瞬間はキャラクタの色を変える、といった要素も盛り込んでみます。
class CEnemy1:public CObject { public: CEnemy1(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 ~CEnemy1(); private: CGame *game; DWORD dwMask,dwMode,dwCount; LPDWORD lpBuf,lpPixels[2]; int iX,iY,iDx,iDy,iWidth,iHeight; virtual DWORD getPixel(int,int); };
CEnemy1クラスのコンストラクタでは、まずサイズを32×32に、キャラクタの座標(iX, iY)を画面上端の中央よりになるように設定します。また、行動を時間で変化させるためのカウンタdwCountの初期化も行うようにしました。
キャラクタのピクセル列は、通常時と弾発射時の2枚(通常時lpPixels[0]、弾発射時lpPixels[1])を用意し、それぞれ初期化しておきます。
CEnemy1::CEnemy1(CGame *gArg) { iWidth=32; iHeight=32; dwCount=0; game=gArg; iX=game->getWidth()/2; iY=-iHeight/2; /* キャラクタ用ピクセル列確保 */ lpBuf=(LPDWORD)GlobalAlloc(GPTR,iWidth*iHeight*4*2); lpPixels[0]=lpBuf; lpPixels[1]=lpBuf+iWidth*iHeight; for (int i=2;i<30;i++) /* ピクセル列を設定 */ for (int j=2;j<30;j++) { lpPixels[0][j+i*32]=0xcccccc; lpPixels[1][j+i*32]=0xff0000; } dwMask=0; }
敵の行動を定義するaction()は、移動と弾の発射を行うという点では自機と同様です。ただし、「乱数」で移動量や弾の発射を決定する点、一度行動するたびにカウンタ変数dwCountに加算して、その値(生成されてからの行動数=ゲーム内の時間)に応じて移動パターンを変える点が異なります。弾を発射したら、変数dwModeを1にして、描画時などに反映されるようにしておきます。
void CEnemy1::action() { iDx=-4+((double)rand()/(double)RAND_MAX)*8; if (iY<game->getHeight()/3 || dwCount++>100) iDy=((double)rand()/(double)RAND_MAX)*8; else iDy=0; iX+=iDx; iY+=iDy; if (iY<game->getHeight()-iHeight) game->adjest(this); if (((double)rand()/(double)RAND_MAX)<0.05) { /* 弾発射 */ game->fire(new CBullet(iX+12,iY+40,0,10,game)); dwMode=1; } else dwMode=0; }
描画を行うdraw()は、自機と同じです。キャラクタのピクセルを1ピクセルずつ「抜き色と同じでなければ」描画する、という処理を行います。ただし、キャラクタのピクセルを返すgetPixel()は、dwModeの値によって参照するピクセル列を変更します。これによって、「通常時のキャラクタ」と「弾発射時のキャラクタ」を描き分けるわけですね。
DWORD CEnemy1::getPixel(int iArg1,int iArg2) { return lpPixels[dwMode][iArg1+iArg2*iWidth]; } void CEnemy1::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); } }
このようにして作成した敵は、ゲーム中一定の周期で出すことにします。ゲームクラスCGameに変数dwCountを追加し、この変数をaction()の度に加算する事で「ゲーム中の時間」を計ることにしましょう。そして、このdwCountが400になったら敵を出しdwCountをクリアすることにします。これで400周期ごとに敵が出てくるようになるわけです。
if (dwCount++>400) { /* 400周期で敵を出す */ lObj.push_back(new CEnemy1(this)); dwCount=0; }
敵クラスCEnemy1はCObjectクラスの派生クラスなので、敵であるCEnemy1オブジェクトも自機や弾と同様CObject型のオブジェクトとして扱えます。つまり、前回自機と弾用に作ったシステムがそのまま使えるわけです。というわけで、ゲーム内のオブジェクトに関する処理は前回と同じ。
さらに、今回は背景のスクロールも追加しました。背景は、最初に
for (int i=0;i<iWidth*iHeight;i++) lpBG[i]=64+((double)rand() / (double)RAND_MAX)*128;
として背景用ピクセル列lpBGを全体に「青っぽい点」で埋め尽くしておきます。そして、ゲーム中はaction()で「ゲーム内の時間」を進める時に
/* 背景を1ピクセル下にずらす */ MoveMemory(lpBG+iWidth,lpBG,iWidth*(iHeight-1)*4); /* 背景の一番上のラインに青い点を描画 */ for (int j=0;j<iWidth;j++) lpBG[j]=64+((double)rand() / (double)RAND_MAX)*128;
としてスクロールさせるようにしました。
以上のことから、CGameクラスのaction()は以下のようにします。
void CGame::action() { if (dwCount++>400) { /* 400周期で敵を出す */ lObj.push_back(new CEnemy1(this)); dwCount=0; } 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)->getHeight()<0 || (*i)->getY()>iHeight) { delete *i; (*i)=NULL; } i++; } lObj.remove(NULL); i=lBul.begin(); while(i!=lBul.end()) lObj.push_back((*i++)); /* 背景を1ピクセル下にずらす */ MoveMemory(lpBG+iWidth,lpBG,iWidth*(iHeight-1)*4); /* 背景の一番上のラインに青い点を描画 */ for (int j=0;j<iWidth;j++) lpBG[j]=64+((double)rand() / (double)RAND_MAX)*128; updateScreen(); }
プログラムを実行すると自機が表示されるので、カーソルキーで移動したりスペースキーで弾を発射したりしてみてください。