読者です 読者をやめる 読者になる 読者になる

Flat Leon Works

アプリやゲームを作ってます。

【C++ ゲームプログラミング】タスクシステムの発展形

前回「タスクシステムからの脱却を考える」という記事を書きました。この記事では「タスクシステムではなく具体的なシステムを作ろう」と述べましたが、同時に「タスクシステムには大局的な使い方もある」ということも述べました。今回はそれを踏まえてタスクシステムをより便利にする方法考えたいと思います。

タスクが親子関係を持てるようにする

タスクが親子関係を持てるようになると、タスクのグループ化が可能になりタスクの扱いがとても楽になります。そして更新処理などの順番もある程度制御できるようになります。 またタスクが親子関係を持てるようにすることはタスクシステムを大局的に使うのに便利です。

タスクの親子関係で座標を相対的にする

タスクの親子関係で座標を相対的にするというのは、つまり親が動けば子も動くといったような機能です。これはタスクシステム側に持たせても良いし、タスクの個別の処理内でやるのでもいいと思います。ただし、タスクシステムを大局的に使うことを考えると、すべてのタスクに座標が必要なわけではないので一部のタスクにのみ座標を持たせる仕組みとそのタスクが座標を持っているかどうかを判断する仕組みが必要になります。(一番素直な実装はPositionTaskみたいな派生クラスを用意し、dynamic_castでPositionTaskかどうかを判定)

イベント伝搬の仕組みを導入する

イベントは主にシステム側から各タスクへの通知の仕組みです。例えば、ゲームが中断された瞬間に何かをさせたい場合は中断イベントクラスを作り、それを送信するようにします。各タスクはイベントを受信し、処理を行ったり行わなかったりします。このときイベントがすべてのタスクに送信されるのではなく、登録制にして一部のタスクにのみ送信されるような設計にする場合もあります。またタスクの親子関係を利用して、親でイベントを止め、子には伝搬しないようにする仕組みを導入することもできます。

ここで言うイベント伝搬の仕組みは、各タスクがイベントごとに仮想関数を用意するようなものではなく、各タスクがイベント受信のための仮想関数を1つだけ持つようなものです。この場合、受信するイベントの種類を判定するためにイベントにIDを持たせたり、ダウンキャストで判定を行うなどの処理が必要になります。

class A : public Task
{
public:
    #if 0 // こうではなくて
    virtual void ReceivePauseEvent( PauseEvent* p_event ); // 各タスクがイベントごとに仮想関数を用意するタイプ
    #else // こう
    virtual void ReceiveEvent( Event* p_event ) // 各タスクがイベント受信のための仮想関数を1つだけ持つタイプ
    {
        // イベントの判定が必要になる
        #if 0 // IDによる判定
        if ( p_event->GetID() == PAUSE_EVENT_ID )
        {
        }
        #else // ダウンキャストによる判定
        if ( dynamic_cast<PauseEvent*>(p_event) )
        {
        }
        #endif
    }
    #endif
};

このイベント伝搬の仕組みを導入すると、新しい処理の種類が増えた時に、タスク基底クラスに新しく仮想関数を追加する必要がなくなります。また、作っているゲーム特有の処理、例えばマリオで「Pスイッチが踏まれたときに何か処理をさせたい」という場合にも対応することができます。イベント伝搬ではなく処理ごとに仮想関数を用意する仕組みだと、タスク基底クラスにそのPスイッチ用の仮想関数を用意する必要がでてきます。しかしタスク基底クラスにゲーム特有の処理を含ませるのは正しく抽象化できていないことになるのでやってはいけません。

また、イベント伝搬の仕組みを導入すると「更新処理」「描画処理」もイベントの1つとして扱えるようになります。

メッセージ送受信の仕組みを導入する

メッセージ送受信はタスクからタスクへメッセージを送信するための仕組みですが特に決まった設計があるわけではなく、いろいろな設計があります。またイベントと区別されないこともあります。ここではメッセージ送受信の設計の1例として説明したいと思います。

メッセージ送受信は、タスクからタスクへメッセージを送信するための仕組みです。イベントと似ていますが用途が少し違います。イベントは全体への通知です。メッセージはタスクからタスクへの通知です。イベントは登録制の場合、各タスクが「イベントの種類」を指定して「システム」に「自分」を登録します。対してメッセージは、「メッセージの種類」を指定して「送信者」に「自分」を登録します。

メッセージのわかり易い例はUIです。例えばボタンタスクがあったとして、ボタンタスクはクリックされると「クリック」メッセージを送信します。もしこのボタンタスクの「クリック」メッセージに登録者がいれば、そこへメッセージが送信されます。メッセージの受信はイベントの受信とほぼ同じ作りになります。イベントと同じようにメッセージの種類(と場合によっては送信者)を判定する必要があります。

class A : public Task
{
public:
    virtual void ReceiveMessage( Message* p_message )
    {
        // イベントの判定が必要になる
        #if 0 // IDによる判定
        if ( p_message->GetID() == MESSAGE_CLICK_ID )
        {
        }
        #else // ダウンキャストによる判定
        if ( dynamic_cast<ClickMessage*>(p_message) )
        {
        }
        #endif
    }
};

これは少し不便なので、メッセージ受信登録時にメンバ関数ポインタも渡して呼ばれるメンバ関数を指定するような設計にすることも考えられます。

弱参照の仕組み

各タスクはいつ消えるのかわかりません。生のポインタでタスクを参照していると、破棄済みのタスクにアクセスしてしまう可能性があります。タスクを弱参照できる仕組みがあると便利でしょう。(というか必須?)

弱参照ポインタ(WeakPointer)はC++03には存在しません。Boostのboost::weak_ptrを利用するか、自作する必要があります。もちろんC++11以降のweak_ptrを使う手もあります。弱参照ポインタの仕組み自体は単純なので自作も難しくはありません。

弱参照ポインタの実装例( 適当に書いたのでバグがあるかも )

#include <stdio.h>

class Task;
class TaskPointer
{
    friend class Task;
    struct PointerInfo
    {
        Task* m_pTask;
        int   m_RefCount;
        PointerInfo(){ printf( "new PointerInfo\n" ); }
        ~PointerInfo(){ printf( "delete PointerInfo\n" ); }
    };
    PointerInfo* m_pPointerInfo;
    TaskPointer( Task* p_task )
    {
        m_pPointerInfo = new PointerInfo;
        m_pPointerInfo->m_pTask = p_task;
        m_pPointerInfo->m_RefCount = 1;
    }
public:
    TaskPointer( const TaskPointer& other )
    {
        m_pPointerInfo = other.m_pPointerInfo;
        if ( m_pPointerInfo ){ m_pPointerInfo->m_RefCount += 1; }
    }
    ~TaskPointer()
    {
        --m_pPointerInfo->m_RefCount;
        if ( m_pPointerInfo->m_RefCount == 0 ){ delete m_pPointerInfo; }
    }
    Task* GetPointer( void )
    {
        return m_pPointerInfo ? m_pPointerInfo->m_pTask : NULL;
    }
    void SetNull( void )
    {
        if ( m_pPointerInfo )
        {
            m_pPointerInfo->m_pTask = NULL;
        }
    }
};

class Task
{
    TaskPointer m_TaskPointer;
public:
    Task() : m_TaskPointer( this ) {}
    virtual ~Task() { m_TaskPointer.SetNull(); }
    TaskPointer GetTaskPointer( void ) { return m_TaskPointer; }
};

int main() {
    Task* p_task = new Task;
    TaskPointer p = p_task->GetTaskPointer();
    printf( "%s\n", p.GetPointer() ? "Exists" : "Not exists" );
    delete p_task;
    printf( "%s\n", p.GetPointer() ? "Exists" : "Not exists" );
    return 0;
}

別の方法としてタスクにIDやハンドルなど持たせて、それを使ってそのタスクが生きているかどうかを判定して生きている場合のみポインタにアクセスするという方法もあります。ただ、弱参照ポインタの方が楽だと思います。

タスクシステムという名前も発展させる

タスクシステムを発展させる方法を見てきましたが、実はこれらは他のシステムで見られるものです。

親子関係と親子関係による相対座標の機能が付いたシステムはゲームエンジンでよく見かけ、シーングラフと呼ばれたりします。またGUIフレームワークWidgetに親子関係を持たせることができ、親子関係による相対座標になっていたりします。イベントやメッセージの仕組みはGUIプログラミングには必須なのでたいてい存在します。

なので、タスクシステムに親子関係やイベント伝搬やメッセージ送受信の仕組みを入れた場合、それをタスクシステムと呼び続けるのは少し違和感があります。なんて呼べばいいのかは思いつきませんが…。フレームワークとか?少なくともTaskクラスはObjectクラスとかの方があっている気がします。

まとめ

タスクシステムを発展させるとフレームワークとして扱うこともできるようになる。