ホーム < ゲームつくろー! < IKD備忘録

設計な話
レイアウトアクションの状態遷移


 HUDは色々なアクションを起こします。そういうアクションの多くは内部で状態遷移を起こします。ゲームプログラムの永遠のテーマである状態遷移をHUDに追加してみましょう。


@ クリックするとくるっと回って元に戻る

 例として、HUDをクリックすると赤色になり、くるっと回ってまた元に戻るアクションをさせてみましょう。

 前章までのコードでHUDを表すObjectクラスがあって、このクラスはparamメンバにHUDの動きとなる移動量と回転量を保持しているのでした。ですから、クリックされた時にこのパラメータを連続的に変化させればくるっと回す事が出来るわけです。ここに来て初めて「時間」の概念が出てきました。

 Objectの状態を変更するためにupdateメソッドを追加し引数には差分時間を渡します。時間は整数値で与える事にします。これは変なズレを起こさせないための配慮です。基準として1秒を300分割します。60FPSで動くとすると1フレーム時間は5です。

// 時間更新
void Object::update(unsigned advance) {
    curTime += advance;
}

今回のアクションはクリックされるまではじっと待っている状態です。クリックされるとくるっと回る状態に移行し、再びじっと待っている状態に戻ります。つまり、2つの状態を交互に遷移するわけです。これをベタに書くとこうなります:

// 時間更新
void TurnRedObject::update(unsigned advance) {
    curTime += advance;

    if (bClicked)
        rotateState();
    else
        idle();
}

もちろんこれでは柔軟性も再利用性も無いわけで、ちゃんと柔らか〜くしてあげる必要があります。



A 状態クラスを作る

 じっとしている、そしてくるっと回るという各状態はクラス化できます。一般に一つの状態遷移は「初期化」「状態維持」「終了処理」の3つのプロセスを経ます。これらをそれぞれinitialize、update、releaseメソッドとしておきます。問題は状態を遷移する方法。これは次のようにしてみます。

 一つの状態遷移クラスは幾つかの次に繋がる足を持っているとします。Objectクラスは初期化の段階で状態遷移オブジェクトを作り、その足に続く状態オブジェクトをくっつけます。正しく足を接続したら、後は各状態遷移オブジェクトに挙動をゆだねます。状態クラスは次のような感じになります:

class Object {
public:
    ///////////////////////////
    // 状態遷移
    /////
    class State {
        static const unsigned nextStateNum = 4;
        State *nextStates[nextStateNum];

    protected:
        State() {}

    public:
        virtual ~State() {}

        // 初期化
        virtual void initialize(Object &env) {}

        // 状態更新
        virtual State *update(Object &env, unsigned advance) = 0;

        // リリース
        virtual void release(Object &env) {}

        // マウスアクション
        virtual void mouseAction(Object &env, Mouse &mouse) {}

    public:
        // 次のステートを登録
        void setNextState(unsigned id, State *nextState) {
            if (id >= nextStateNum)
                return;
            nextStates[id] = nextState;
        }

        // 次のステートを取得
        State *getNextState(unsigned id) {
            if (id >= nextStateNum)
                return 0;
            return nextStates[id];
        }
    };

...
};

状態遷移クラスStateはHUDオブジェクトと密接に関係します。そのためこのクラスはObjectクラスの子クラス(サブクラス)にします。こうすると、親オブジェクトにガンガンアクセスできるようになるわけです。

 nextStatesメンバには次に行くべき状態遷移オブジェクトを保持します。最大で足が4本あるわけです。まぁ大抵4本くらいあれば事足ります(笑)。
 Stateコンストラクタがprotectedなのが実はポイントです。これは勝手にnewされてオブジェクトを作れないようにしています。どうしてそんな事をしなければならないかは、後ほど出てきます。
 初期化、状態更新、リリース、そしてマウスアクションの引数にObjectの参照を渡しています。これを通してこれらのメソッド内からObjectを操作します。
 setNextStateメソッドで次の状態遷移オブジェクトを足にくっつけます。引数が生ポインタになっていますが、これには大きな理由があります。HUDの状態遷移は循環参照が嵐のように起こります。そのため、スマートポインタで保持するとあっさりとメモリリークが発生します。それを防ぐには生ポインタを登録せざるを得ません。「生ポインタ・・・危なくないですか?」と思うのは当然です。なるべく安全に扱うための対処をこの後します。

 Objectクラスは状態遷移を作って管理する人に変わります。状態遷移はObject::initializeメソッド内で作ります。今回作ろうとしているクリックすると赤くなってくるっと回るHUDオブジェクトだとこんな感じになります:

// 初期化
void TurnRedObject::initialize() {
    IdleState *idle = createNewState<IdleState>();
    RotateState *rotate = createNewState<RotateState>();
    idle->setNextState(0, rotate);
    rotate->setNextState(0, idle);

    setEntryState(idle);
}

ポイントが幾つかあります。Object::createNewStateメソッドは状態遷移オブジェクトを作るテンプレートメソッドです:

class Object {
    Vector<sp<State> > states;   // 作成したステートを保持しておく

protected:
    // ステート生成
    template<class T> T* createNewState() {
        sp<T> newObj(new T);
        states.push(newObj);
        return newObj.getPtr();
    }
    ....
};

内部ではテンプレート引数に指定された状態遷移クラスの新しいスマートポインタオブジェクトを作り、それをstates配列に登録します。これでObjectが消える時に作成した状態遷移オブジェクトも自動的に消えるという算段です。で、返すのは生ポインタです。このメソッドを通して作ったオブジェクトは寿命がはっきりとしている生ポインタなので、比較的安心して使えるわけです。
 ただ、Stateクラスのコンストラクタはprotectedで宣言されていました。new出来ないはずです。実は派生クラスでObjectクラスをfriend宣言します。Objectクラスだけ生成許可を与えるというわけです:

// 待機ステート
class IdleState : public State {
    friend Object;

    bool bClicked;

protected:
    IdleState() : bClicked() {}

public:
    virtual ~IdleState() {}
    virtual void initialize(Object &env);
    virtual State *update(Object &env, unsigned advance);
    virtual void mouseAction(Object &env, Mouse &mouse);
};

初期化の続きですが、繋げたい遷移をObject::State::setNextStateメソッドで登録しています。IdleStateは待機状態、RotateStateはくるっと回転状態を表すステートで、循環で参照しているのがわかりますね。スマートポインタだと「ぎゃー!」ってなります(^-^;。
 Object::setEntryStateメソッドは、一番最初のエントリーステートを設定します。これを設定しないと状態遷移がスタートしないわけです。これで、状態遷移を繋ぐ作業が出来ました。

 Object::updateメソッドは状態遷移を監視して盲目的に繋ぎ替える作業を行います:

// 時間更新
void Object::update(unsigned advance) {
    curTime += advance;
    if (curState) {
        State *pre = curState;
        curState = curState->update(*this, advance);
        if (pre != curState) {
            pre->release(*this);
            curState->initialize(*this);
        }
    }
}

時間をちょっと進めた後、現在の状態遷移オブジェクト(curState)の存在をチェックします。あればその生ポインタを一度保持しておき、状態遷移オブジェクトを更新します。戻り値は次の状態オブジェクトになっています。もしそれが前の物と異なっていたら、遷移が起こった事になりますので、前の状態遷移の後処理(releaseメソッド)を行い、同時に新しい状態遷移の初期化をします。これにより、自動的に状態がバンバン遷移していきます。

 状態遷移周りはおおよそこんな感じです。完全版はこの章のサンプルでどうぞ。では、これらの環境を使ってクリックしたら赤くなってくるっと回るHUDオブジェクトを作ります。



B クリックしたらくるっと回る

 クリックするまではHUDは待機状態で、マウスクリックを監視しています。クリックを検知したら、くるっと回る状態遷移にスイッチします。くるっと回っている間はマウス入力を無視します。回り終わったらまた待機状態に戻ります。今回作るHUDはこういうものです。

 待機ステートクラスはこんな感じの実装になります:

///////////////////////
// 待機ステート
////
void TurnRedObject::IdleState::initialize(Object &env) {
    TurnRedObject &owner = (TurnRedObject&)env;
    owner.param.color = 0xffffffff;
    bClicked = false;
}

Object::State *TurnRedObject::IdleState::update(Object &env, unsigned advance) {
    return bClicked ? getNextState(0) : this;
}

// マウスアクション
void TurnRedObject::IdleState::mouseAction(Object &env, Mouse &mouse) {
    TurnRedObject &owner = (TurnRedObject&)env;

    // クリック監視
    if (mouse.isDown(Mouse::L) && env.isContain(mouse.getPos())) {
        bClicked = true;

    // ファンクターに連絡
    for (unsigned i = 0; i < owner.functors.size(); i++)
        owner.functors[i]->run();
    }
}

初期化では親のHUDの色を白色に戻します。またクリックフラグを下ろします。更新は極めて簡単で、マウスクリックされていなければ自分自身を返し(newで作成されているので問題ありません)、マウスクリックされていたら次の状態遷移オブジェクト(くるっと回る遷移オブジェクト)を返します。クリックの監視はmouseActionメソッドが親から呼ばれてきますので、その中でチェックします。これ、前章でObject::mouseActionメソッド内に合った実装ほぼそのままです。

 太文字の箇所で、引数のObjectへの参照をキャストしています。これはIdleStateがTurnRedObjectのサブクラスである事がわかっているのでセーフです。

 くるっと回る状態はこうなります:

///////////////////////////
// ぐるっと回転ステート
////
void TurnRedObject::RotateState::initialize(Object &env) {
    TurnRedObject &owner = (TurnRedObject&)env;
    owner.param.color = 0xffff0000;
    curTime = 0;
}

Object::State *TurnRedObject::RotateState::update(Object &env, unsigned advance) {
    TurnRedObject &owner = (TurnRedObject&)env;

    curTime += advance;
    float t = (float)curTime / rotTime;
    t = t > 1.0f ? 1.0f : t;

    owner.param.rot = 360.0f * t;

    return (t >= 1.0f) ? getNextState(0) : this;
}

初期化メソッドで色を赤色にし、内部時間を0に戻します。内部時間は回転角度を決定するのに必要です。updateメソッド内は大した事をしてなくて、呼ばれる度に時間を進めてrotTime(1回転させる時間)との割合tを求め、それを回転角度に変換して親のパラメータを更新しています。遷移条件は1回転した時、つまりtが1.0fになった時です。

 さ〜これを実際に動かしますよぉ。HUDクリック!くるっと回転!わ〜い:

・・・伝わんねぇよなぁ。

 このサンプルプログラムはこちらからダウンロードできます。

 これでHUDに対してアクションを起こした時に動かすというゲームっぽい挙動をさせる土台が出来上がりました。そろそろ、絵を付けないと見栄えしなくなってきました。イメージをどう扱うか?次の章でまた試行錯誤です。