はじめに
タスクとは「Windowsはマルチタスクだ」という時のタスクに同義ですが、プログラム的にはオブジェクトに近いです。シューティングゲームを作る場合は「自機」「敵」「敵出現制御」「得点管理」「タイトル画面」など、ゲームを構成する全ての要素をタスクとします。
タスクシステムとは、これら複数のタスクを管理する仕組みであり、現在でもプロの現場で用いられています。長所は次の通りです。
- ジャンルを問わず様々なゲームに適用できる
- 並列処理をうまい具合に実現できる
- ゲームの流れを自然な形で表現できる
- 大規模なゲームも開発できる
- タスクごとに独立しているため、複数人で開発できる
一方の短所は、タスクシステムの歴史が古いことに起因する、高すぎる自由度です。さまざまな実装方法があり、またオブジェクト指向が一般的ではなかった時代の手法なためか、スパゲティプログラムや、データの隠蔽化が不十分なプログラムとなっている例も見かけます。
本稿で実装するタスクシステムは、クラスを用いてオブジェクト指向を取り入れ、これらの問題を解決します。また、タスクシステムだけを実装しても、その良さが実感しづらいため、タスクシステムを用いたシューティングゲームも作ります。
ゲームプログラマを目指す方は必読です。
対象読者
タスクシステムの入門者を対象に、その基礎および使い方を解説します。C++言語において「クラス」「継承」「静的関数」「純粋仮想関数」そして「双方向リスト」を理解している人。またシューティングゲームを作るためにDirectX9 Graphicsも使用しますが、DirectXに依存した部分のプログラムは解説しません。
必要な環境
シューティングゲームはDirectX 9に対応した環境でのみ動作します。開発はVisual C++ .NET 2002とDirectX 9b SDK、動作確認はWindows XP SP2とGeForceFX 5500を用いて行いました。
タスクシステムの設計
タスクに必要な情報は次の通りです。
- 処理関数
- 描画関数
- 前のタスクを指すポインタ
- 次のタスクを指すポインタ
「前のタスクを指すポインタ」と「次のタスクを指すポインタ」は、タスクを要素とする双方向リストの「タスクリスト」を構築するために使います。「前のタスクを指すポインタ」はあまり使わないため、これを取り除いて単方向リストとしても構いません。
そして「タスクリスト」を管理し、操作する手段を提供するのが「タスクシステム」です。
タスクシステムでは、タスクのためのメモリ領域を、タスクリストの初期化時にまとめて確保し、タスクを生成・追加する時には、既存のメモリ領域の一部を割り当てます。タスクを削除する時には、そのタスクの使用中フラグを偽にするだけです。もちろん、タスクリストへの挿入、タスクリストからの除外、という処理は行いますが。このようにタスクの削除にメモリ領域の解放を伴わないことによって自滅も可能となり、プログラムの見通しが良くなる、という利点があります。図2を見てください。1つの大きなメモリ領域を、3つのタスクに必要なサイズだけ割り当てています。タスクごとにサイズが異なるのは、タスク固有のデータ(クラスのメンバ関数とメンバ変数)が異なるためです。
タスクシステムに必要な情報は次の通りです(図3)。
- タスクおよびタスクリストを操作するための関数
- 最初に実行するタスクを指すポインタ(
m_active
) - 新しいタスクを追加する未使用領域(FreeArea)の先頭を指すポインタ(
m_free
) - メモリ領域の先頭を指すポインタ(
g_buf
)
使わなくなったタスク(DeadTask)は、タスクリストから除外するだけで、メモリ領域の解放などは行いません。また、タスクが持つ情報はデフラグに必要となるため、その時まで消してはいけません。
新しいタスクは、それより前方に使わなくなったタスクの領域があったとしても、未使用領域の先頭に追加します。前述のこともありますが、未使用の連続するメモリ領域のバイト数を簡単に計算するためでもあります。
未使用領域の残量が不足した場合は、デフラグを行い、使わなくなったタスクの領域を詰めます(図4)。
デフラグを行っても、残量不足が解消しない場合は、タスクを追加できません。ゲームは割り当てられるメモリサイズが決まっているため、他の部分のメモリ効率を見直す必要があるでしょう。
図2、3、4に示したタスクの番号は、実行する順番です。図3、4のタスクがメモリ領域の先頭から順に並んでいないのは、タスクに優先度を設定し、実行する順番を制御しているためです。他に、グループ分けの機能も実装しますが、詳しくは後述します。