ゲームのタスクシステム設計あれこれ

ゲームを進行させていくにあたって重要になるのが複数タスクの管理です。
俗にタスクシステムと呼ばれているものです。
タスクシステムの明確な定義はありませんが、たとえばキャラクタを移動させたり当たり判定させたり描画させたりといった処理の進行を管理するものだと解釈して差し支えないと思います。
これらのタスクを管理するためのクラス設計について考えてみました。

ゲームの進行で必要になる代表的な処理は以下のようになると思います。

 1.キャラクタの移動、回転
 2.キャラクタ同士との当たり判定
 3.キャラクタの描画
 4.効果音再生
 5.ファイル等の外部との非同期I/O処理

上記のうち、1~3はタイマーイベントで逐次呼び出される可能性があります。
4~5は1~3ほど頻度は高くないですが、不特定なタイミングで呼び出される可能性があります。
いずれにせよ、これらの処理は見かけ上平行して行う必要が出てくるでしょう。

また、処理の順番も重要で、キャラクタを移動してから描画するのと、描画してから移動するのではキャラクタの動きが異なってきます。
因みに、この順番は一般に前者が正しいです。
後者だと1フレームの遅れが出てしまうからです。

1~3の処理は1→2→3の順番で行う必要があります。
また、どれもタイマーイベントが発生したときに行う点で共通しているため、同じ基底クラスからの派生クラスとして実装すればよいでしょう。
したがって、1~3のタスクは以下のようなクラスになるでしょう。

//--------------------------------------
// タスククラス
//--------------------------------------
class Task {
public:
    virtual ~Task() {}
    virtual void Run(int interval) = 0;
};

//--------------------------------------
// モーションタスク
//--------------------------------------
class MotionTask {
public:
    MotionTask() {}
    virtual ~MotionTask() {}

    void Run(int interval) {
        処理あれこれ
    }
};

//--------------------------------------
// 当たり判定タスク
//--------------------------------------
class CollisionTask {
public:
    CollisionTask() {}
    virtual ~CollisionTask() {}

    void Run(int interval) {
        処理あれこれ
    }
};

//--------------------------------------
// 描画タスク
//--------------------------------------
class DrawTask {
public:
    DrawTask() {}
    virtual ~DrawTask() {}

    void Run(int interval) {
        処理あれこれ
    }
};

そして、タイマーイベントの中で上記タスクを1~3の順番で実行します。

MotionTask  mTask;
CollisionTask cTask;
DrawTask dTask;

//--------------------------------------
// タイマーイベント
//--------------------------------------
void TimerEvent(int interval) {
    mTask.Run(interval);
    cTask.Run(interval);
    dTask.Run(interval);
};

それほど複雑でないと思います。

さて、ここで問題になるのは4~5の処理の呼び出しです。
効果音はたとえばキャラクタ同士が衝突したときに衝突音を発生させたい場合があると思います。
ファイルI/Oの処理はゲームシナリオの切り替え時に途中結果を保存したり読み出したりしたい場合に必要になります。

これらの処理は1~3の処理に密接に関係してきます。
結論から言うと、これらも1つのタスクとしてみなすことが出来ます。
4、5の処理は3の描画処理の後に行うようにすればよいでしょう。

MotionTask  mTask;
CollisionTask cTask;
DrawTask dTask;
SoundTask sTask;
AsyncIOTask aTask;

//--------------------------------------
// タイマーイベント
//--------------------------------------
void TimerEvent(int interval) {
    mTask.Run(interval);
    cTask.Run(interval);
    dTask.Run(interval);
    sTask.Run(interval);
    aTask.Run(interval);
};

これでひとまず大枠は出来ました。
上記の処理はどれも似通った呼ばれ方をしていると思います。

上記の5つのタスクはそれぞれ特有の処理を受け持っています。
たとえば、MotionTaskはフィールド上のキャラクターの座標を更新する処理を行います。
座標を更新するキャラクタは一般に複数存在し、出現したり消えたりする可能性もあるでしょう。
これらの処理の実行管理は出来るだけ専用のクラスで行いたいものです。
間違ってもシナリオを作成する段階で意識すべきことではないでしょう。

このような処理は、大本のシステムにタスクを登録するようにすれば効率的でしょう。
たとえば、MotionTaskは以下のようになると思います。

//--------------------------------------
// モーションタスク
//--------------------------------------
class MotionTask {
public:
    class UpdateCoodinate {
    public:
        virtual ~UpdateCoodinate() {}
        virtual void Update() = 0;
    };

    MotionTask() {}
    virtual ~MotionTask() {}

    // 座標更新タスク登録
    void Register(UpdateCoodinate& task) {
        m_updates.push_back(task);
    }

    // 座標更新タスク登録解除
    void Unregister(UpdateCoodinate& task) {
        for ( auto it = m_updates.begin() ; it != m_updates.end() ; ++it ) {
            if ( &task == &(*it) ) {
                m_updates.erase(it);
                break;
            }
        }
    }

    // 座標の更新処理実行
    void Run(int interval) {
        for ( auto it = m_updates.begin() ; it != m_updates.end() ; ++it ) {
            m_updates.Update();
        }
    }

private:
    list<updateCoodinate> m_updates;
};

かなり無理やりですが、タスクの登録/解除を行うRegister/Unregisterメソッドと登録されたタスクを実行するRunメソッドが出来ると思います。
実際の動きはUpdateCoodinateを継承したクラスにて実装します。
たとえば、直線の動きならLinearMotionクラスを、円運動ならCurveMotionクラスをUpdateCoodinateクラスから派生させます。

この考え方は、ゲームに限らずアプリの開発でもよく用いられています。
因みに、上の実装はC#ならdelegateを使えば非常に簡単に実装できます。
C#のdelegateとゲームのタスクシステムはなかなかに相性が良いです。

他のタスクも同様の設計をしていけばよいかと思います。

まとまりが悪くなってしまいましたが、タスクシステムのクラス設計についてのメモ書きでした。