104.ステートと行動

このサンプルはFullTutorial004というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。

 リビルドして実行すると以下の画面が出てきます。球体が1つ表示され、コントローラで操作できます。少し経つと四方からが体当たりしてきます。逃げ回っても追いかけてきます。茶色い敵も近づいてきて、体当たりを仕掛けてきます。遠くのほうには赤い敵がいて、近づくとジャンプしながら追いかけてきます。こちらの敵はある一定の距離を離れると追いかけてきません。

 

図0104a

 


解説

 このサンプルは、そこそこ複雑な処理が実装されています。複雑な処理をどのように効率的に記述するかで、ソースの可読性も上がり、バグも少なくなります。
 FullTutorial002、003は、オブジェクトのOnUpdate()関数に位置や速度の変更を直接書いていました。
 そのような形で記述を続けると、以下のような現象に陥ってしまいます。
1、行動の分岐を記述するのに、if文やswitch文のオンパレードになってしまう。
2、敵のAIなどを記述する際、同じような行動も敵のクラス単位で記述しなければならず、
 それは往々にして似たような処理を何度も記述する羽目になってしまう。
 この状態を避けるために、BaseCrossではいくつかの機能を実装できる仕様になっています。

ステートとステートマシン

 まず代表的な仕組みがステートとステートマシンです。ステート状態です。
 GameObjectは多くの場合現在の状態を持っています。例えばプレイヤーを待っている状態プレイヤーを追いかけている状態あるいはジャンプしている状態などです。これらの状態ステートといいます。
 ステートは各オブジェクトに固有のものです。ですからプレイヤーがジャンプしている状態とか敵1がプレイヤーを追いかけている状態など、各GameObjectごとに記述します。
 そして、それらのステートを管理するのがステートマシンです。
 ステートマシンステートの切り替えの命令が来たら、前のステートを終わらせ、新しいステートに移るという処理を行います。その際、例えばAというステートからBというステートに移る場合、
1、AステートのExit()関数を呼ぶ
2、BステートのEnter()関数を呼ぶ
 の2つの処理を行います。この呼び出しにより、ステートのあとしまつや、新しいステートの準備を行うことができます。そして、ステートがBに切り替わった後は、毎ターン(約60分の1秒)ごとに、
3、BステートのExicute()関数を呼ぶ
 という呼び出しを行います。
 ですから、ステートの記述者は、自分を保持しているゲームオブジェクトのパラメータを監視して、何かに衝突するなどで変更があったら、ステートの切り替え命令をステートマシンに送ることができます。

 さて、ここまでがこれまでのBaseCrossに実装されていた機能です。2017年春のバージョンアップによって、さらに行動クラスというのを実装できるようになりました。

行動(Behavior)クラス

 行動は一般的(ゲームのアルゴリズムの書籍などでは)、Behaviorとかふるまいなどと呼ばれています。敵キャラにおけるプレイヤーを発見した時の行動攻撃を受けた時の行動などもそうです。アイテムがプレイヤーに触れた時の演出行動と言えます。オブジェクト化されたギミックであれば何らかの仕掛けがクリアしたときの行動もそうでしょう。
 それらの行動は、一般的にはステートと深く結びついていて、あるステートが開始した時の行動などといった感じで、オブジェクトのメンバ関数などに記述し、それをステートから呼び出す、といった手法をとるのが一般的です。
 ステートはその仕様上シングルトンでなければいけません。ですからステートにはメンバ変数は記述できないので、オブジェクト内やもしくは、ステートとは別の場所で記述します。
 しかし、往々にして行動のアルゴリズムは似たような記述になりがちです。ほとんど同じ行動を別々のオブジェクトに別々に記述するのは、無駄な記述と言えます。
 またこれらの処理をもっと細かく見た場合、何らかの演出回転する、光る、アニメーションするなどの細かく分けられます。またパラメータの変化ライフを変化させる、ポイントを変化させるに分けることができます。
 そういう形にしておけば、例えば
1、ハートのアイテムは回転して、ライフを増やす
2、コインのアイテムは光りながら回転して、ポイントを増やす
3、魔法瓶にアイテムは、回転しながら煙が出て、魔力とライフをアップする
4、敵の魔法瓶アイテムを横取りすると、光りながら煙が出て、魔力とポイントをアップする
 を処理する場合
行動 ハート コイン 魔法瓶 敵の魔法瓶
回転  
光る    
   
ライフ    
ポイント    
魔力    
 のような関係になります。つまり、各アイテムが実装すべき行動は重なる部分ができるわけです。
 そうした場合、各アイテムごとに回転などの行動を別々に記述したのでは、無駄が出てきます。もちろん、赤い煙とか黄色く光る、のような少し違う行動もありますが、もしこのような場合、別の行動として定義するか、カラー要素をパラメータ化することでやりくりが可能です。
 ようは、行動の再利用、あるいは行動の共有を可能にする仕組みが行動クラスというわけです。

ステートの実装

 それでは、このサンプルではどのようにステートと行動を実装しているか見てみましょう。Character.h/cppに記述があります。
 まずは、敵1を見てみます。クラスはEnemy1です。まず、ステートを準備します。Enemy1が持ってるステートはEnemy1FarStateEnemy1NearStateです。前者はプレイヤーからの距離が遠いときのステートであり、後者は近いときのステートです。
 以下は後者のステート(Enemy1NearState)です
//--------------------------------------------------------------------------------------
/// Enemy1のNearステート
//--------------------------------------------------------------------------------------
class Enemy1NearState : public ObjState<Enemy1>
{
    Enemy1NearState() {}
public:
    //ステートのインスタンス取得
    DECLARE_SINGLETON_INSTANCE(Enemy1NearState)
    virtual void Enter(const shared_ptr<Enemy1>& Obj)override;
    virtual void Execute(const shared_ptr<Enemy1>& Obj)override;
    virtual void Exit(const shared_ptr<Enemy1>& Obj)override;
};
 上記はEnemy1NearStateの宣言です。
    DECLARE_SINGLETON_INSTANCE(Enemy1NearState)
 というのはマクロですが、この記述で
    static shared_ptr<Enemy1NearState> Instance();
 と展開されます。ステートはシングルトンで作成するため、シングルトンのインスタンスのポインタをstaticで保持しておきます。そのため、この関数はそのポインタを取得する関数宣言です。これはマクロですから、展開後のコードを直接記述してもかまいません。
 ここには仮想関数として、Enter()、Excute()、Exit()の3つの関数があります。この関数は親クラスによって純粋仮想関数として宣言されているので、必ず実装しなければいけません。
 それぞれの役割とすれば、Enter()関数そのステートに入ったときに呼ばれます。Excute()関数そのステートが継続中に呼ばれます。Exit()関数そのステートが終了するときに呼ばれます。
 ではどこから呼ばれるのかというと、この後説明するステートマシンから呼ばれます。

 以下はEnemy1NearStateの実体です。Character.cppに記述があります。
//--------------------------------------------------------------------------------------
/// Enemy1のNearステート
//--------------------------------------------------------------------------------------
IMPLEMENT_SINGLETON_INSTANCE(Enemy1NearState)

void Enemy1NearState::Enter(const shared_ptr<Enemy1>& Obj) {
}

void Enemy1NearState::Execute(const shared_ptr<Enemy1>& Obj) {
    auto PtrArrive = Obj->GetBehavior<ArriveSteering>();
    if (PtrArrive->Execute(L"Player") >= Obj->GetNearFarChange()) {
        Obj->GetStateMachine()->ChangeState(Enemy1FarState::Instance());
    }
}

void Enemy1NearState::Exit(const shared_ptr<Enemy1>& Obj) {
}
 まずここにも
IMPLEMENT_SINGLETON_INSTANCE(Enemy1NearState)
 というマクロ呼び出しがあります。これは
shared_ptr<Enemy1NearState> Enemy1NearState::Instance() { 
    static shared_ptr<Enemy1NearState> instance;
    if(!instance) { 
        instance = shared_ptr<Enemy1NearState>(new Enemy1NearState); 
    }
    return instance;
}
 という形に展開されます。このソースを見ればわかるようにEnemy1NearStateという型名がいくつも出てきます。
 これは間違いのもとでもあるので、マクロ化しています。こちらも、展開後のコードを記述しても問題ありません。
 さて、Enemy1NearState::Instance()関数は何をやっているのでしょうか。ステートはシングルトンなので、自分自身のポインタを一つだけ保持しています。それが、
    static shared_ptr<Enemy1NearState> instance;
 です。このポインタが初期化後であればこの関数はそのポインタを返し、初期化前であれば初期化して返します。
 このように1回目の呼び出しでインスタンスが作成され、以降は作成されることはありません。

 続くEnemy1NearState::Enter()関数Enemy1NearState::Execute()関数Enemy1NearState::Exit()関数は前述したようにこのステートに入ったときこのステートにいる間の毎ターンこのステートを抜けるときの実装を記述します。
 ここでは、Enemy1NearState::Execute()関数のみ記述されています。Enter()やExit()は記述がありませんが、前述したように純粋仮想関数なので、空関数でも記述しておく必要があります。

ステートマシン

 ステートマシンは、各ゲームオブジェクトに1つだけ記述します。まず宣言です。以下はEnemy1のステートマシンです。宣言とアクセサを記述します。
//--------------------------------------------------------------------------------------
/// 敵1
//--------------------------------------------------------------------------------------
class Enemy1 : public GameObject {
//中略
    //ステートマシーン
    unique_ptr<StateMachine<Enemy1>>  m_StateMachine;
//中略
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  ステートマシンを得る
    @return ステートマシン
    */
    //--------------------------------------------------------------------------------------
    unique_ptr< StateMachine<Enemy1>>& GetStateMachine() {
        return m_StateMachine;
    }
//中略
};
 今回のサンプルではunique_ptrを使ってますが、shared_ptrでも問題はありません。
 続いて初期化ですがOnCreate()関数などで、以下のように記述します。以下はunique_ptrの場合の初期化方法です。
//初期化
void Enemy1::OnCreate() {
    //中略

    //ステートマシンの構築
    m_StateMachine.reset(new StateMachine<Enemy1>(GetThis<Enemy1>()));
    //最初のステートをEnemy1FarStateに設定
    m_StateMachine->ChangeState(Enemy1FarState::Instance());
}
 この記述でステートマシンが構築され、最初のステートにEnemy1FarStateが設定されます。
 そして、ゲームオブジェクトのOnUpdate()関数などで以下のように記述します。
void Enemy1::OnUpdate() {
    //ステートによって変わらない行動を実行
    auto PtrGrav = GetBehavior<Gravity>();
    PtrGrav->Execute();
    auto PtrSep = GetBehavior<SeparationSteering>();
    PtrSep->Execute();
    //ステートマシンのUpdateを行う
    //この中でステートの切り替えが行われる
    m_StateMachine->Update();
}
 こうしておくと例えば、どこかのステートなどで
    Obj->GetStateMachine()->ChangeState(Enemy1NearState::Instance());
 のように記述すれば、ステートマシンにより、前述したように
1、Enemy1FarStateステートのExit()関数を呼ぶ
2、Enemy1NearStateステートのEnter()関数を呼ぶ
 のようにステートの切り替えが行われます。

 さて、このように、各ステートのそれぞれの関数からは、ゲームオブジェクトの何らかの行動を記述する、あるいはゲームオブジェクト内の書かれたいくつかの行動を記述した関数を呼び出すわけですが、この行動を記述するメンバ関数をクラス化したものが行動クラスです。
 つまり行動をクラス化すれば、ゲームオブジェクトを超えて使えるので、各ゲームオブジェクトは個別に行動を記述するメンバ関数を用意する必要がなくなるわけです。

行動のクラス化

 具体的にEnemy1NearState::Execute()関数での記述を追いかけてみましょう。
void Enemy1NearState::Execute(const shared_ptr<Enemy1>& Obj) {
    auto PtrArrive = Obj->GetBehavior<ArriveSteering>();
    if (PtrArrive->Execute(L"Player") >= Obj->GetNearFarChange()) {
        Obj->GetStateMachine()->ChangeState(Enemy1FarState::Instance());
    }
}
 赤くなっているところが行動クラスのインスタンスを取得している箇所とその行動のメンバ関数を呼び出すところです。
Obj->GetBehavior<行動クラス名>()
 により、ゲームオブジェクトにその行動クラスが存在しなければ、インスタンスを作成しゲームオブジェクトに設置し、そのポインタを返します。
 ですから、あらかじめAddBehavior()などの関数で行動を追加しておく必要はありません(そういう関数は存在しません)。使うときに取得すればいいのです。
ArriveBehaviorの宣言と実体は、ライブラリ中、SharedLibプロジェクトのBehaviorSteering.h/cppに実装されています。以下がその宣言です。
//--------------------------------------------------------------------------------------
/// ArriveSteering行動クラス
//--------------------------------------------------------------------------------------
class ArriveSteering : public SteeringBehavior {
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    @param[in]  GameObjectPtr   ゲームオブジェクト
    */
    //--------------------------------------------------------------------------------------
    ArriveSteering(const shared_ptr<GameObject>& GameObjectPtr);
    //--------------------------------------------------------------------------------------
    /*!
    @brief  デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~ArriveSteering();
    //--------------------------------------------------------------------------------------
    /*!
    @brief  減速値を得る(デフォルト3.0)
    @return 減速値
    */
    //--------------------------------------------------------------------------------------
    float GetDecl() const;
    //--------------------------------------------------------------------------------------
    /*!
    @brief  減速値を設定する
    @param[in]  f   減速値
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void SetDecl(float f);
    //--------------------------------------------------------------------------------------
    /*!
    @brief  行動を実行する
    @param[in]  TargetPos   追いかける位置
    @return 追いかける位置との距離
    */
    //--------------------------------------------------------------------------------------
    float Execute(const bsm::Vec3& TargetPos);
    //--------------------------------------------------------------------------------------
    /*!
    @brief  行動を実行する
    @param[in]  TargetKey   追いかけるオブジェクトのキー(SharedObjec)
    @return 追いかけるオブジェクトとの距離
    */
    //--------------------------------------------------------------------------------------
    float Execute(const wstring& TargetKey);
private:
    // pImplイディオム
    struct Impl;
    unique_ptr<Impl> pImpl;
};
 ステートから呼び出されている関数を見てみましょう(赤くなっている関数です)。
float  ArriveSteering::Execute(const bsm::Vec3& TargetPos) {
    auto RigidPtr = GetGameObject()->GetComponent<Rigidbody>();
    auto TransPtr = GetGameObject()->GetComponent<Transform>();
    bsm::Vec3 Force = RigidPtr->GetForce();
    bsm::Vec3 WorkForce;
    WorkForce = Steering::Arrive(RigidPtr->GetVelocity(), TargetPos,
        TransPtr->GetWorldPosition(), RigidPtr->GetMaxSpeed(), pImpl->m_Decl) *  GetWeight();
    Steering::AccumulateForce(Force, WorkForce, RigidPtr->GetMaxForce());
    RigidPtr->SetForce(Force);
    auto Pos = TransPtr->GetWorldPosition();
    return bsm::length(Pos - TargetPos);
}

float ArriveSteering::Execute(const wstring& TargetKey) {
    auto TargetPtr = GetStage()->GetSharedObject(TargetKey);
    auto TargetPos = TargetPtr->GetComponent<Transform>()->GetWorldPosition();
    return Execute(TargetPos);
}
 ArriveSteering::Execute()関数は多重定義されていて、パラメータがキーワードせ呼び出される関数が呼ばれます。ここでは、キーワードからプレイヤーを特定し、その位置情報で、もう一つの関数を呼び出します。
 また、赤くなっているSteering::Arrive()関数呼び出しは、フォースを作り出す操舵関数です。ここでは到着操舵を呼び出しています。

 それでは、行動を一般化するメリットとはなんでしょう。
 それは多くの場合、敵やアイテムやギミックAI動作を少ないコード行数で実装できる、という部分です。
 たとえば、敵Aにはプレイヤーを追いかける行動があるとします。また敵Bにもプレイヤーを追いかける行動があるとした場合、同じようなコードを、これまでは敵A及び敵Bに記述する必要がありました。また、そうでない場合でもStrategyデザインパターンなどを作成し、ゲーム側で共有する行動を作成する必要がありました。
 もちろんStrategyパターンもいいのですが、今回実装した行動クラスStrategyパターンよりも自由度が高い設計になってます。Strategyパターンは、ある程度、親クラス側で仮想関数もしくはテンプレート内関数の関数名を固定する必要があります。これはStrategyパターンを呼び出す側が関数名を決めることにより、後からのクラス追加がしやすい設計になっているからです。しかし行動クラスでは保持する関数の形式はどのようにでも書けるというメリットがあります。
 たとえば、ArriveBehavior行動クラスEnter()関数がありますがJumpBehavior行動クラスにはEnter()関数はありません。代わりにStartJump()関数があります。

行動クラスの作成

 しかし、似たような行動であるがオブジェクトを超えて同じではない場合、もちろん、ライブラリではなく新規で行動クラスを作成することもできます。
 GameSourcesプロジェクトProjectBehavior.h/cppを見てみてください。
 ここには、例えばPlayerBehavior行動クラスがあります。この行動クラスはライブラリにあらかじめ用意されたクラスではありません。

 新規に行動クラスを作成するには、ProjectBehavior.hなどに
class TestBehavior : public Behavior {
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    @param[in]  GameObjectPtr   ゲームオブジェクト
    */
    //--------------------------------------------------------------------------------------
    TestBehavior(const shared_ptr<GameObject>& GameObjectPtr) :
        Behavior(GameObjectPtr)
    {}
    //以下、ほかのメンバ関数を作成する
    //例: TestFunction();
    void TestFunction();
};
 このようにBehaviorの派生クラスとして宣言します。そしてコンストラクタの引数を必ずconst shared_ptr<GameObject>& GameObjectPtrにする、ということです。ほかのタイプのコンストラクタは作成しません。
 こうしておいて、何らかのメンバ関数を作成しておきます。実体(ProjectBehavior.cppファイル)のほうは
void TestBehavior::TestFunction(){
    auto Pos = GetGameObject()->GetComponent<Transform>()->GetPosition();
    
}
 のようにGetGameObject()関数によって、その行動が設置されているオブジェクトのポインタを取得することができます。それは当然、そのオブジェクトが保持するコンポーネント類にアクセスできることになります。
 また行動クラスはステートとは違いメンバ変数を作成できます。行動クラスはシングルトンではありません。
class TestBehavior : public Behavior {
    //この行動のスピード
    float m_Speed;
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    @param[in]  GameObjectPtr   ゲームオブジェクト
    */
    //--------------------------------------------------------------------------------------
    TestBehavior(const shared_ptr<GameObject>& GameObjectPtr) :
        Behavior(GameObjectPtr),
        m_Speed(10.0f)
    {}
    //以下略
};
 行動クラスのインスタンスは、例えば上記TestBehavior行動クラスの場合、ステートなどから
    auto PtrTest = Obj->GetBehavior<TestBehavior>();
    PtrTest->TestFunction();
 と呼び出せばPtrTestTestBehavior行動クラスのインスタンスのポインタが返り、そのポインタを介してメンバ関数を呼び出せます。

行動とコンポーネント

 このように書き進めていくと、行動とコンポーネントが混同してしまうかもしれません。どちらもゲームオブジェクトに設置されるオブジェクトですので。コンポーネントというのは道具です。Transformコンポーネント位置や回転を設定できる道具ですしRigidbodyコンポーネント速度を管理する道具です。
 そして行動道具であるコンポーネントを実際に操作するオブジェクトです。もちろん衝突判定コンポーネントのように設置しただけで衝突を実装できるものもありますが、多くのコンポーネントはパラメータの設定や操作関数を呼び出すことで、そのコンポーネントを操作することになります。行動はそのコンポーネントの操作をクラス化したものと言えます。

プレイヤーの作成

 プレイヤーも行動がクラス化されています。
 敵キャラなどのAI動作をするわけでもないのにプレイヤーの行動をクラス化するメリットというのはなんでしょう。
 例えば(これはAIキャラにも言えることですが)、ゲーム制作過程でいま実装されているのとは、違う行動がほしいということはよくあります。
 そんな場合は、よくある方法としては現在の実装をコメントして別に記述することがよくありますが、実装してみたけどやはり前のほうが良いとなった場合、この方法だと、本当に前の状態まで戻すことができるのか、という部分でバグが起きやすくなります。
 こんな場合も行動をクラス化しておけば、新しい行動を新しいクラスとして記述し、呼び出す行動をそちらに差し替えます。そして前のほうが良いとなったら、前の行動クラスに、戻せばいいのです。もちろん、新しい実装が、かならずしも、前の実装と呼び出し方が同じになるとは限りません。しかしコメントにして新しく書くよりも保守性は上がるはずです。
 もちろんメンバ関数で行動の部分を分けて記述しておけば似たような効果が得られるかもしれませんが、クラスを別にしておいたほうがオブジェクト指向なのも確かです。

 プレイヤーはPlayer.h/cppに記述があります。敵キャラと同じように、ステートマシンとステートを保持しています。
 ステートデフォルトステートジャンプステートがあります。Aボタンでジャンプします。
 まずAボタンを押すというイベントをハンドラ化します。直接コントローラを見に行ってもいいのですが、ハンドラにすることでAボタンが押されたときに呼び出される関数を記述することが可能になります。
 ハンドラ化はそんなに大変なコードではありません。ProjectBehavior.hに以下のように記述します。
//--------------------------------------------------------------------------------------
/// コントローラのボタンのハンドラ
//--------------------------------------------------------------------------------------
template<typename T>
struct InputHandler {
    void PushHandle(const shared_ptr<T>& Obj) {
        //コントローラの取得
        auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
        if (CntlVec[0].bConnected) {
            //Aボタン
            if (CntlVec[0].wPressedButtons & XINPUT_GAMEPAD_A) {
                Obj->OnPushA();
            }
        }
    }
};
 このようにテンプレートとして記述しておくと、プレイヤー以外でもコントローラのボタンを受け付けやすくなります。
 このように記述しておいて、プレイヤーの宣言部に以下のように記述します。
//--------------------------------------------------------------------------------------
/// プレイヤー
//--------------------------------------------------------------------------------------
class Player : public GameObject {
    //中略
    //入力ハンドラー
    InputHandler<Player> m_InputHandler;
public:
    //中略
    //Aボタンハンドラ
    void OnPushA();
};
 こうしておいて、Player.cpp
//Aボタンハンドラ
void  Player::OnPushA() {
    //Aボタンが押されたときの何かの処理
}
 と記述します。この関数はAボタンが押されたときに呼ばれるようにします。
 最後に、Player::OnUpdate()に以下のように記述します。
//更新
void Player::OnUpdate() {
    //コントローラチェックして入力があればコマンド呼び出し
    m_InputHandler.PushHandle(GetThis<Player>());
    //ステートマシン更新
    m_StateMachine->Update();
    //中略
}
 コントローラのチェック関数とステートマシンの更新の順番は注意を払う必要があります。
 今回はAボタンが押されたタイミングでステートの変更が起こりますが、その後のステートマシンの更新ジャンプステートでの更新が行われますので、安全です。しかし、複雑なボタン処理をする場合、m_InputHandler.PushHandle()関数の戻り値を設定するなど、細かな制御が必要になることもあります。

 さて、サンプルではプレイヤーはデフォルトステートジャンプステートを持っています。いずれのステートでも、コントローラのスティックによって移動が可能ですが、その行動の記述は、ProjectBehavior.h/cppの、PlayerBehavior行動クラスに記述されています。各ステートではそのメンバ関数を呼び出します。
 以下は、デフォルトステートPlayerDefaultState::Execute()関数ですが
void PlayerDefaultState::Execute(const shared_ptr<Player>& Obj) {
    auto PtrDefault = Obj->GetBehavior<PlayerBehavior>();
    PtrDefault->MovePlayer();
}
 このようにPlayerBehavior行動クラスMovePlayer()関数を呼び出します。

 ジャンプステートへの切り替えはAボタンによって行われ、前述した Player::OnPushA()に記述されています。
//Aボタンハンドラ
void  Player::OnPushA() {
    if (GetStateMachine()->GetCurrentState() == PlayerDefaultState::Instance()) {
        //通常ステートならジャンプステートに移行
        GetStateMachine()->ChangeState(PlayerJumpState::Instance());
    }
}
 このようにステートが切り替えられるとPlayerJumpState::Enter()関数が呼ばれますので、
void PlayerJumpState::Enter(const shared_ptr<Player>& Obj) {
    auto PtrJump = Obj->GetBehavior<JumpBehavior>();
    PtrJump->StartJump(Vec3(0, 4.0f, 0));
}
 のように、ジャンプ行動を取得し、StartJump()関数を呼び出します。これでジャンプします。

 今後、BaseCrossのサンプルでは行動クラスが多数出てきます。ここでその概要やメカニズムを紹介しました。