日別アーカイブ: 2007年9月4日

static タスクシステム

ほとんどのゲーム開発では、一般的にタスクシステムと呼ばれる
独自のフレームワークを使用しています。
あまりドキュメント化されたり表に出ることが無いものの、
プロジェクトを通じてプログラマ間に秘伝的に伝播していくので
実に多くのバリエーションがあります。

プロジェクトの数だけ存在するといっても良いかもしれません。
基本構造と目的はほぼ似通っており、セガタスクと呼ばれる
こともあります。

OS のタスク同様、処理ごとに CPU 時間を細かく分割していく
仕組みです。ゲーム開発で特徴的なのは、すべての処理を
フレーム単位で管理することです。
1フレームの時間は約 16.7~33.3msec と決まっているので、
この中でそのフレームに必要なタスクを、全部一巡しなければ
なりません。

タスクシステムの利点、またはその利用目的は、大きく分けて
2種類あると考えています

 ・タスク数の動的な増減への対処
 ・モジュール分割や作業分担といった開発時の利便性のため

ゲーム中必要な処理は能動的に変化します。例えばオブジェクト
数の変化 – 追加や削除など、動的な処理単位の変化を効率よく
管理することが求められます。

オブジェクトだけでなく、ゲームやシステム全体の動作自身も
タスク化することができます。各種モジュールやサービスなども、
ゲーム進行やモードの遷移にあわせて組み換えが可能となる
わけです。

オブジェクトのような細かい単位と、ゲームシーン等の大きな
処理単位では、規模も目的も異なります。

これらの処理単位を同じ枠組みを使って同一に扱うこともあれば、
グループ毎に別の管理機構を用いることもあります。親子関係、
階層構造を持たせて凝った構造にしているものもあります。
この辺の考え方や設計はそのプロジェクトの方針に依存します。

ゲームの開発規模も、今ではプログラマで10数人規模とかなり
大きくなっています。オブジェクトの増減といったプログラム
処理上の都合だけでなく、分業化におけるフレームワーク
としても非常に重要な役割を持っています。
ある意味人間も管理するわけです。

各タスクは独立しているため作業分担が比較的容易で、システム
がきちんとしていれば、お互いの干渉衝突を最小限にしつつ
開発を進められることになります。
(でも終盤はそうも言っていられません)

上で述べたように、1フレームの処理を切り分けながら処理を
組み立てていきます。各タスクは1フレーム内で順番依存を
持っており、実行順をある程度明確にしなければなりません。

さまざまな手法がありますが、例えば各タスクに割り当てた
プライオリティ値を元に、実行順番を決めるものもあります。

システム系モジュールや作業分担時のタスクは、動的な追加
削除はあっても、動的にプライオリティが変わることが
ほとんどありません。

そういう enum や define 値で順番を決められたタスクも、
動的なリストで追加時にソートされ、リストをたどって仮想関数
呼び出しが行われています。
これを static にできないか考えてみました。
(ちなみに筆者は script ベースのまた違う手法を使っています。)

いわゆる TypeList です。
今回はソートのために Binary Tree を作ってみました。

各 Task は任意の priority 値と呼び出し口 Func() だけ定義
すればよく、Func() が static なら基底クラスも不要です。
各 task は別ソースだとして

class DrawBegin_Task {
public:
    enum {
        priority= 3000,
    };
    static void Func()
    {
    }
};

class DrawEnd_Task {
public:
    enum {
        priority= 5000,
    };
    static void Func()
    {
    }
};

class Input_Task {
public:
    enum {
        priority= 1000,
    };
    static void Func()
    {
        //..
    }
};

こんな形で呼び出します。

template
struct task {
    enum {
        value= A::priority,
    };
    static void	Exec()
    {
        A::Func();
    }
};

typedef	TS::Array<
            task,
            task,
            task
        > TaskList;

void MainLoop
{
    TS::ExecTask::result>::Exec();
}

実際の実行順は TaskList 上の順番とは異なり、priority 番号
の小さい順に並び替えられます。

そのため一度 TaskList に登録してしまえば、各処理ごとに
priority を自由にいじって処理の挿入先を変更することができます。
これはコンパイル時に決定します。
画面をクリアするだけとか Flip するだけの単純な task は
inline 展開が可能です。(__forceinline が必要でした)

とはいえ、もともと 1フレームに数回程度、非常に実行頻度の低い
部分なので static 化したとしてもパフォーマンス的には全く
変わらないでしょう。

動的な変更を行うオブジェクト管理は、これらの下に設けることに
なります。

上の例では static な関数しか呼べませんが、各タスクの
インスタンスも管理したいならこんな感じになるでしょうか。
それぞれ _TaskBase を継承しておきます。
ついでに priority も外部からも指定できるようにしておきます。

class _TaskBase {
};

static _TaskBase*  _InstanceTable[ TS::Length::value ];

template
struct itask {
    enum {
        value= pri,
    };
    static void	Exec();
};

template
RegisterInstance( A* _instance )
{
    _InstanceTable[TS::Index >::value]= _instance;
}

template
void  itask::Exec()
{
    _TaskBase* _inst= _InstanceTable[TS::Index >::value];
    reinterpret_cast(_inst)->Func();
}

class Camera_Task : public _TaskBase {
public:
    enum {
        priority= 2000,
    };
    void Func()
    {
        //..
    }
};

使い方は TaskList に登録して、RegisterInstance() を
呼び出すだけです。

typedef	TS::Array<
            itask,
            itask,
            itask,
            itask
        >    TaskList;

void MainLoop
{
    RegisterInstance( new Camera_Task );

    TS::ExecTask::result>::Exec();
}

あとは MainLoop から priority 順に呼び出してくれます。
この場合も仮想関数は不要で、呼び出しは static の例と同じように
静的に行われます。
またインスタンスを持たないものは RegisterInstance() する必要が
無く、Func() も static のままで構いません。
(ここは本当は何らかのエラー判定が欲しい部分です。)

一応実際に試したのでソースをのせてみます。
(確認はVisualStudio2005のみ)

StaticTaskSystem.h

wheelhandle_ss01t.zip 実際に使用した例のアーカイブ

本当はもっといろいろ改良が必要だと思います。