ホーム < ゲームつくろー! < ゲーム製作技術編 < タスクシステムと「ゆるゆる」なキャラクタ
その3 タスクシステムと「ゆるゆる」なキャラクタ
タスクとはゲームの細かな制御をしてくれる小さなプログラム(オブジェクト)です。タスクを一度用意すると、システムがタスクを更新していくだけでゲームが進行していきます。タスクの利点は後付けで機能を追加していける点です。仕様の変更があったとしても、タスクを追加したり削除したりする事で、その変更に柔軟に対処できます。
タスクは特定のオブジェクト間の関係を結ぶ「ストラテジ(Strategy)」の位置付けとなるため、突き詰めていくとタスクによる「ゆるいキャラクタ」が見えてきます。ゆるいキャラクタとは幾つかのタスクによってもやっと形作られるくくりの無いキャラクタの事です。ゆるいキャラクタは、制御がなかなか面倒ではありますが、ゲームを後付け発展させるのに極めて効果的でもあります。
この章ではそんなタスクとゆるいキャラクタの作り方について、あれこれと試行錯誤してみることにします。
@ タスクを導入するとオブジェクトはこう変わります
具体的な例で始めましょう。
STGを作っていて、ある程度システムが固まった時に「ん〜自機ウェポンのパワーメーターが欲しいな」と思うことは良くあります(あるんですって(^-^;)。やる事は自機が持つウェポンのパワー情報を得て、それをパワーメータに反映させるだけです。一番シンプルな状態は以下のようなクラス設計ではないでしょうか:
シンプルではありますが・・・
ステージシーンクラスが自機とパワーメータを保持するスタイルです。しかし・・・パワーメーターの追加によりステージシーンクラスはパワーメータークラスと切っても切れない関係になってしまいます。それはまぁこのクラスを使い捨てと割り切れば大きな問題ではありません。それよりも、ステージシーンクラスの内部実装がだんだんごちゃごちゃになってくる点が問題です。クラス内に管理するオブジェクトが増えてくると、色々なオブジェクトのソースとが入り乱れてきます。可読性も低下しますし、何よりも「修正」が難しくなってきます。ステージシーンクラスに一度設定したパワーメーターがやっぱりいらないとなった時、該当箇所を削除し入り乱れたソースの糸を元に戻すのはちょっと勇気がいります。
そこで、次のように考えてみます。「システムが自機とパワーメーターの関係を管理する事で混乱しちゃうのならば、両者の関係を外部に出してしまおう」。そうすれば、パワーメーターが要らなくなった時は、その委託をやめれば良いだけになります。そう、この仕事の委託先が「タスク」です:
複雑にはなりますよ(^-^;
ステージシーンはパワーメーターの変わりにタスクオブジェクトを保持し、その更新メソッドを呼び出します。すると、内部に保持されたパワーメーターが更新されるわけです。この形態を作り上げると、ステージシーンはいつもと同じ仕事をしながら、気が付くと画面にパワーメーターが追加される状態になります。
A そしてステージシーンクラスはうっすら消えていく
@の例で示した図で、ステージシーン内に「自機OBJ」がありますが、良く考えてみるとこれとパワーメーターOBJはステージシーンクラスにとってみれば同じ扱うオブジェクトです。となりますと、パワーメーターと同じように思い切ってこれを外に出した設計もできそうです:
各オブジェクトの実体はメモリのどこかに置かれ、タスクはそれを完全に参照する形となります。自分の必要とするオブジェクトとの連携を確立したタスクは、ステージシーンクラスに引き渡されます。後はステージシーンがタスクを順番に更新さえすれば、ステージ全体が1ステップずつ進行していくことになります。
こうなりますと左記のステージシーンクラスは、単にタスクと描画を行うタスク描画管理クラスに変わっています。タスクに仕事を置き換えると、ステージシーンクラスはうっすらと消えていくんです。
B タスククラスをどう初期化するか?
昔タスクは関数ポインタでしたが、今はクラスがその変わりに役立っています。タスクは誰かが生成します。そして作られるとほぼ同時に初期化されます。
初期化には、「外部がタスクのメソッドを用いて初期化する」形態と、「タスクの内部で自動的に初期化される」形態とがあります。どちらが好ましいでしょうか?私は後者だと思っています。前者の場合、タスクの生成者がタスクの初期化方法について知っている必要があります。しかし、何十種類できかないタスク1つ1つの初期化を1つのクラスが担当するのはあまりにも大変すぎるんです。一方、タスク自身が自分の初期化方法を知っているならば、生成者はタスクオブジェクトを生成してする事に集中できます。
ではどのように自分自身で初期化するか?下図をご覧下さい:
あるタスクは、タスクが固有に使う値(Task固有値1、Task固有値2)とオブジェクトへのポインタが3つ(操作OBJ1、操作OBJ2、操作OBJ3)必要だとしましょう(左側の状態)。固有の値は数値や文字列でのみ表される単純な値なので、与えるのは極めて簡単です。
オブジェクトはタスクが生成しても良さそうな気がしますが、実はここが大変なんです。タスクが扱うオブジェクトは「共有」されると絶大な威力を発揮し出します。しかし、タスク内部でオブジェクトが生成されると、それを外に出さなければ共有できません。この仕組みの確立が以後の話しのメインになります。
今回採用したいのは「オブジェクトを生成者に作ってもらう」スタイルです。そして、タスクの代わりに生成者がオブジェクトを管理します。こうすると、オブジェクトをクラスと番号で指定できるようになるんです。上の例ですと、タスクは数値として渡されたオブジェクトIDをある生成者(ファクトリクラス)に渡します。生成者は内部で新しいオブジェクトを生成し、それにオブジェクトIDを振って自身に保持します。タスクはそのポインタのみを受け取ります。別のタスクが同じIDを生成者に要求した時には「共有」とみなし、生成者は同じオブジェクトを渡します。こうすることで、オブジェクトの共有がはかられます。
下の図はそのフローを表した物です:
Task1は生成者Aに対して「ID0002のA01のオブジェクトを下さい」と要求します。生成者Aは自分が保持しているオブジェクトストックを見て、もしID0002のA01が無ければ、新規に作成します。あればそこに格納されている「ID0002のA01」を引っ張り出してTask1に渡します。同様の事を生成者Bに対しても要求しID0001B01を得ます。こうしてTask1は自分が望むオブジェクトを取得できます。Task2についても同様の作業を行います。
生成者はあまり具体的な初期化をしません。ですから渡してもらったオブジェクトの初期化はTaskが担当します。初期化後、オブジェクト同士はタスクによってその関連を作られます。後はタスクを更新するだけです。
D タスクのプライオリティ
Aで示した図で、自機を表す(操作するかな)タスク(Shipタスク)と自機からウェポンのパワー値を貰ってパワーメーターに反映させるタスク(PMタスクと呼んでおきます)は、個々が独立して働きます。しかし、その「働く順番」によっては誤動作する場合があります。例えば、PMタスクが先に働くと、自機がウェポンの状態を更新する前の値が反映されてしまいます。
これを解決するために、タスクに優先順位をつけます。これは「タスクプライオリティ(priority:優先順位)」と呼ばれています。タスクに優先順位を付けて、タスクを更新する側がそれを制御すれば、多くの場合問題は生じません。
E タスクは描画を意識するか?
タスクはオブジェクトを保持します。それは描画対象もあれば、そうでない物もあります。では、タスクは描画を意識するでしょうか?つまり、タスクが描画メソッドを持つか否かという話です。
タスクが描画メソッドを持たないとすると、描画対象オブジェクトを別に登録する必要が出てきます。しかし、タスクが気まぐれで生成した描画対象を知る術がありません。これはやっぱりいかんのです。
タスクが描画メソッドを持つと、システムはタスクの描画メソッドを呼び出す事で描画も委譲できます。何をどう描画するかをタスクに任せてしまうわけです。しかし、描画する必要の無いタスクも当然沢山あります。それらが空定義された描画メソッドを持つのはパフォーマンスから見てもちょっとどうかと思います。そこで、描画機能を持たないタスクをITaskBase、持つタスクをIDrawTask(ITaskBaseの派生インターフェイス)とし、登録する時に動的型判定で判別してしまいましょう:
左側が描画機能の無いタスク、右側が描画担当タスクです。右側は描画にのみ力を入れても良いですし、何か仕事(更新)を持っても良いでしょう。システムは両者を適切に振り分けて、更新と描画ループを回します。
タスクが描画機能も持つ場合、描画のプライオリティも存在する事になります。タスクの更新順序と描画順序は全くの別物です。
F そしてキャラクタも「ゆるゆる」になる
さて、ここまでの話から、よくある「キャラクタクラス」とタスクによるゆるいキャラクタを比較してみましょう:
左側はいわゆるキャラクタクラスという枠組みです。位置の情報、描画の情報、そしてアニメーションの情報を持っています。もしここに別の機能を盛り込みたい場合、クラスが持てるコンポーネントオブジェクトはもう決まっていますから、派生クラスを作るしかありません。
一方右側はタスクによるキャラクタの定義です。キャラクタはIDに置き換わり、位置制御タスク、描画制御タスク、アニメーションタスクがキャラクタに該当するコンポーネントを触ります。新しい機能を付けたい場合は、ID0001である他のオブジェクトを扱うタスクを追加するだけです。クラスよりもオープンな分、制御はちょっと難しくなりますが、その代わりに強烈な柔軟性を手に入れることができます。
F まとめ
クラス無いにコンポーネントを置くと、クラスはそのコンポーネントと一蓮托生になります。これは追加や削除が難しくなる事を暗示します。そこで、コンポーネントを思い切って外に出し、その関連をタスクに任せると、柔軟でゆるゆるなキャラクタが形作られることがわかります。その代償として制御が結構難しくなるのですが、「この機能を付けたいのにプログラム上出来ない!」という閉じた悲しい状態になるよりはマシです。
この章はタスクについて説明しただけなので、サンプルプログラムはありません。次章からタスクについて各論に入っていきます。まずは、実はかなり大変な「生成者」を取り上げます。