タスクシステムを動かす
タスクシステムを動かすために呼び出すメンバ関数をまとめます。
プログラム | 実行する時期 |
TaskEx::InitTaskList(); | ゲーム起動時 |
Task::RunTask(); | フレームごと |
TaskEx::DrawTask(g_pSprite); | 描画する時 |
Task::ReleaseTaskList(); | ゲーム終了時 |
自機タスク
自機タスクを例に、タスクのクラス、およびタスクシステムの使い方を見てみましょう。
メンバ関数Draw
、Main
は必須です。これらを定義しないと抽象クラスになってしまいます。
#pragma once #include"TaskSystemEx.h" // 自機 class Player : public TaskEx { DWORD m_timer; public: Player(float cx,float cy,float speed,DWORD life); ~Player(void); void Draw(ID3DXSprite *pSprite); void Main(void); void Move(float add_x,float add_y); void Damage(DWORD damage); };
定数
タスクごとにファイルを分けるので、各タスクに固有の情報は定数として定義しておけば十分です。
#include"task.h" #define TEXFILENAME _T("player.png") #define TEXWIDTH 64 #define TEXHEIGHT 64 #define COLORKEY 0 const float W05=TEXWIDTH*0.5f; const float H05=TEXHEIGHT*0.5f; // 当たり領域 機体の中心からの距離 const float HIT_CW=TEXWIDTH*0.25f; const float HIT_CH=TEXHEIGHT*0.25f; #define MARGIN 0 // 画面外に移動できるピクセル数 #define SHOT_SPEED -5 // ショットの速度 #define SHOT_WAIT 250 // ショットの間隔[ms]
コンストラクタ
メンバ変数の初期化、画像の読み込み、などを行います。
Player::Player(float cx,float cy,float speed,DWORD life)
{
m_id=PLAYER;
m_cx=cx; m_cy=cy;
m_hit_cw=HIT_CW; m_hit_ch=HIT_CH;
m_speed=speed;
m_life=life; m_max_life=life;
m_timer=0;
D3DXCreateTextureFromFileEx(g_pD3DDevice,TEXFILENAME,
TEXWIDTH,TEXHEIGHT,1,0,
D3DFMT_A8R8G8B8,
D3DPOOL_MANAGED,D3DX_FILTER_LINEAR,D3DX_FILTER_LINEAR,
COLORKEY,NULL,NULL,&m_pTexture);
}
デストラクタ
メモリ領域の解放、画像の破棄、などを行います。
Player::~Player(void)
{
SAFE_RELEASE(m_pTexture);
}
描画関数
描画するだけにしましょう。
void Player::Draw(ID3DXSprite *pSprite)
{
RECT rect={0,0,TEXWIDTH,TEXHEIGHT};
D3DXVECTOR2 trans(m_cx-W05,m_cy-H05);
D3DXVECTOR2 rot_cen(W05,H05);
D3DCOLOR color=D3DCOLOR_ARGB(m_alpha,255,255,255);
pSprite->Draw(m_pTexture,&rect,NULL,&rot_cen,m_rot,&trans,color);
}
処理関数
状況によって処理を分岐させる場合は、switch
を用いる、または実際に処理するメンバ関数を別に複数用意し、メンバ変数に追加した関数ポインタを用いて呼び分ける、ことにより実現できます。
[SPACE]キーを押している時の処理を見てください。規定のショット間隔を経過している場合は「弾タスク」を生成・追加しています。優先度は省略しているので、デフォルト値0.5f
に設定されます。省略せず0.75f
に設定したい場合は
new(0.75f) MyShot(m_cx,m_cy,SHOT_SPEED);
と書きます。
void Player::Main(void) { BYTE state[256]; GetKeyboardState(state); if(state[VK_LEFT] & 0x80) { m_cx=(m_cx-m_speed >= -MARGIN)? m_cx-m_speed : -MARGIN; } if(state[VK_RIGHT] & 0x80) { m_cx=(m_cx+m_speed < WND_W+MARGIN)? m_cx+m_speed : WND_W+MARGIN; } if(state[VK_UP] & 0x80) { m_cy=(m_cy-m_speed >= -MARGIN)? m_cy-m_speed : -MARGIN; } if(state[VK_DOWN] & 0x80) { m_cy=(m_cy+m_speed < WND_H+MARGIN)? m_cy+m_speed : WND_H+MARGIN; } if(m_timer<SHOT_WAIT) m_timer+=SPF; if(state[VK_SPACE] & 0x80) { if(m_timer>=SHOT_WAIT) { m_timer=0; new MyShot(m_cx,m_cy,SHOT_SPEED); } } }
移動させる
外部から自機を移動させたい場合に呼び出します。
void Player::Move(float add_x,float add_y) { if(add_x<0) { m_cx=(m_cx+add_x >= -MARGIN)? m_cx+add_x : -MARGIN; } else if(add_x>0) { m_cx=(m_cx+add_x < WND_W+MARGIN)? m_cx+add_x : WND_W+MARGIN; } if(add_y<0) { m_cy=(m_cy+add_y >= -MARGIN)? m_cy+add_y : -MARGIN; } else if(add_y>0) { m_cy=(m_cy+add_y < WND_H+MARGIN)? m_cy+add_y : WND_H+MARGIN; } }
耐久力を減らす
外部から耐久力を減らしたい場合に呼び出します。アイテム取得によるパワーアップも同様のメンバ関数を追加することにより実現できます。なお当たり判定は「弾タスク」で行うことにしました。
耐久力が無くなった場合は「爆発タスク」を生成・追加し、自滅します。自滅した後は何もせず直ちに関数から抜けましょう。この様に、ゲームの流れを自然な形で表現できるのもタスクシステムの魅力です。
void Player::Damage(DWORD damage) { m_life=(damage<m_life)? m_life-damage : 0; if(m_life==0) { new Explosion(m_cx,m_cy); delete this; } }
まとめ
サンプルファイルのシューティングゲームでは、前述の「自機」の他に、「敵機」「自機の弾」「敵機の弾(狙い撃ち)」「爆発」「自機の制御」「敵の出現制御」「ステージ制御」「ライフバー管理」「スコア管理」「タイトル画面」「ゲームオーバー画面」をタスクとしました。これらがゲームを構成する要素の全てであり、他のプログラムは、メインループやDirectXの初期化といった定型処理に過ぎません。
本稿のタスクシステムは、メモリ効率を重視しています。どこが、というと、タスクに可変長のメモリを割り当てたり、ダミータスクを用いていないところが、です。一方、多くの参考文献では、タスクリストのためのメモリ領域を、タスクを要素とする配列として確保しています。配列の要素は固定長なため、無駄が生じてしまいます。私にはこれがどうしても納得できなかったため、本稿の仕様になったというわけです。プログラムは複雑になってしまいましたが、一度作ってしまえばそうそう触る部分ではないため、頑張っただけの価値はあります。
タスクシステムは、シューティングゲームやアクションゲームに特に相性が良いとされていますが、もちろん他のジャンルのゲームも作ることができるので、是非とも挑戦してください。
参考文献
- 『ゲームのためのタスクシステム』 松浦健一郎 著、C MAGAZINE 2004年12月号、pp.64-78、ソフトバンククリエイティブ
- 『C++によるタスクシステムの実現』 松浦健一郎 著、C MAGAZINE 2006年1月号、pp.135-144、ソフトバンククリエイティブ
- White Paper