109.セルマップと経路検索

 ここから本格的にBaseCrossの紹介を行います。
このサンプルはFullTutorial009というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 リビルドして実行すると以下の画面が出てきます。リリースモードでのビルトをお勧めします。

 

図0109a

 


サンプルの動作

 まず、コントローラの左スティックを動かしてみましょう。プレイヤー(手前の丸いオブジェクト)が移動します。右スティックでカメラを回転させることができます。カメラは上下にも移動します。また、オブジェクトを移動させると、カメラはついていきます。
 十字スティックの上下を操作すると、カメラがズームイン、ズームアウトします。
 また、Aボタンを押すとジャンプします。Bボタンを押すとモードが変わります。モードを変えてAボタンを押すと、白い攻撃球が発射されます。
 画面上に区画になっているのはセルマップです。手前からセルマップは3つ配置されています。
 セルマップは、手前のセルマップにはマップ情報が表示されています。マップ情報はセルのインデックス各セルのコストです。コストは通常1が設定され、障害物があるセルは-1が設定されています。
 真ん中のセルマップは区画のみ表示されています。奥のセルマップは何も表示されません。セルマップ情報はいわばデバッグ用の表示で、ゲーム完成時には何も表示しない(つまり奥セルマップの状態)になります。このサンプルがデバッグモードで動作が遅いのはこのセルマップの情報表示が原因です。すべてのセルマップ情報表示をしない設定にすると、デバッグモードでも充分は速度が出ると思います。
 それぞれのセルマップにはが配置され、それぞれが自分の属するセルマップをテリトリーとしています。
 プレイヤーがセルマップの中に入ると、そこをテリトリーとするが追いかけてきます。テリトリーから抜けるとそれ以上追いかけては来ません。

解説

 このサンプルにはBaseCrossの様々な機能が盛り込まれています。
 まず、セルマップから解説しましょう。

セルマップの作成

 セルマップは、あらかじめ定義されたゲームオブジェクトです。ステージにセルマップを配置するためには、GameStage.cppにあるように、以下のように記述します。
//セルマップの作成
void GameStage::CreateStageCellMap() {
    auto Group = CreateSharedObjectGroup(L"CellMap");
    float  PieceSize = 1.0f;
    auto Ptr = AddGameObject<StageCellMap>(Vec3(-10.0f, 0, 4.0f),PieceSize,20,7);
    //セルマップの区画を表示する場合は以下の設定
    Ptr->SetDrawActive(true);
    //さらにセルのインデックスとコストを表示する場合は以下の設定
    Ptr->SetCellStringActive(true);
    SetSharedGameObject(L"StageCellMap1", Ptr);
    //グループに追加
    Group->IntoGroup(Ptr);

    Ptr = AddGameObject<StageCellMap>(Vec3(-10.0f, 0, 16.0f), PieceSize, 20, 7);
    //セルマップの区画を表示する場合は以下の設定
    Ptr->SetDrawActive(true);
    //さらにセルのインデックスとコストを表示する場合は以下の設定
    //Ptr->SetCellStringActive(true);
    SetSharedGameObject(L"StageCellMap2", Ptr);
    //グループに追加
    Group->IntoGroup(Ptr);

    //以下3つ目のセルマップはグループを別にする
    //動的にセルマップを変更する敵用
    auto Group2 = CreateSharedObjectGroup(L"CellMap2");

    Ptr = AddGameObject<StageCellMap>(Vec3(-10.0f, 0, 28.0f), PieceSize, 20, 7);
    //セルマップの区画を表示する場合は以下の設定
    Ptr->SetDrawActive(true);
    //さらにセルのインデックスとコストを表示する場合は以下の設定
    Ptr->SetCellStringActive(true);
    SetSharedGameObject(L"StageCellMap3", Ptr);
    //グループに追加
    Group2->IntoGroup(Ptr);

}
 赤くなっているところは、手前のセルマップを作成している個所です。
 作成する前に、
    auto Group = CreateSharedObjectGroup(L"CellMap");
 という形で、グループを作成します。グループというのは、ステージ上のゲームオブジェクトを、関連するもの同士をまとめる機能で、配置オブジェクト全体からある特定のオブジェクトを参照する(探し出す)より効率的にアクセスできます。各セルマップはこのグループ内に収めることで、特定しやすくなります。
 グループを作るにはキーワードを指定するだけです。
 続いて
    float  PieceSize = 1.0f;
    auto Ptr = AddGameObject<StageCellMap>(Vec3(-10.0f, 0, 4.0f),PieceSize,20,7);
 の記述です。
 セルマップのクラスはStageCellMapです。このオブジェクトはコンストラクタでいくつかの情報を渡す必要があります。
 1つ目の引数は最小位置です。セルマップはXZ平面上2次元オブジェクトですが、内部的には3次元で管理しています。各セルはAABBという3次元区画を持ってます。AABBとはXYZ軸に平行な直方体です。セルマップでは、縦横そして高さが同じ立方体が、内部的なAABBとなります。そのため、それらの各セルの、一番左手前にあるセルの最小位置が引数になります。一番手前のセルマップはVec3(-10.0f, 0, 4.0f)の位置から開始するので、そのようなパラメータを渡します。
 2番目の引数は各セルの縦横(高さ)の大きさです。ここでは1.0fで作成します。
 次の20X方向のセルの数です。続く7Z方向のセルの数です。
 このように、セルマップ全体の大きさ、は引数に入れないことに注意してください。1区画の大きさとセル数がわかれば、セル全体の大きさは計算できますのでそのような引数のほうが矛盾なくマップを作成できます。縦方向(Y方向)は、1つのセルの大きさです。つまりY方向のセルの数は常に1になります。
 ここでは渡してませんが、StageCellMapのコンストラクタには5つ目のパラメータがありデフォルトのコストを渡します。この値はデフォルト引数になっており、値は1です。
 続く設定はセルマップの表示状態の設定です。
    //セルマップの区画を表示する場合は以下の設定
    Ptr->SetDrawActive(true);
    //さらにセルのインデックスとコストを表示する場合は以下の設定
    Ptr->SetCellStringActive(true);
 表示状態は2段階で設定できます。SetDrawActive()関数表示するかしないかの設定ができます。デフォルトは表示しないです。続くSetCellStringActive()関数で、セル情報文字列を設定するかどうかを決めます。セル情報文字列セルのインデックスとコストです。
 続く以下の行で、共有オブジェクトとして設定し、また、先ほど作成したグループに自分自身を追加します
    SetSharedGameObject(L"StageCellMap1", Ptr);
    //グループに追加
    Group->IntoGroup(Ptr);
 SetSharedGameObject()関数は、ほかのゲームオブジェクトなどからこのオブジェクトをキーワードで参照できるようにする設定です。は自分のテリトリーを特定する必要がありますので、このように設定にします。
 最後の行でグループに追加してます。
 これで1つのセルマップの追加は終わりです。同様の処理を真ん中のセルマップ、奥のセルマップに対して行います。ただし、SetSharedGameObject()関数に渡すキーワードはセルマップごとに違いますので注意しましょう。
 また、もう一つ注意したいのは、セルマップは敵を作成する前に作成しておくということです。敵のコンストラクタには、テリトリーとするセルマップを渡す必要があります。先に作成するとはどういうことか、ですが、以下、GameStage::OnCreate() 関数ですが、
void GameStage::OnCreate() {
    try {
        //リソースの作成
        CreateResourses();
        //ビューとライトの作成
        CreateViewLight();
        //プレートの作成
        CreatePlate();
        //セルマップの作成
        CreateStageCellMap();
        //固定のボックスの作成
        CreateFixedBox();
        //プレーヤーの作成
        CreatePlayer();
        //敵の作成
        CreateEnemy();
    }
    catch (...) {
        throw;
    }
}
 このように、敵は最後に作成しているのがわかります。
 またこのサンプルではリソースはGemaStage内で登録しています(前のサンプルとは違います)。

障害物の作成

 セルマップ上に配置される直方体の障害物の作成です。このクラスはFixedBoxクラスで、Character.h/cppに記述があります。以下はCharacter.cppにある、初期化時のコードです。
void FixedBox::OnCreate() {
    auto PtrTransform = GetComponent<Transform>();

    PtrTransform->SetScale(m_Scale);
    PtrTransform->SetRotation(m_Rotation);
    PtrTransform->SetPosition(m_Position);

    //衝突判定
    auto PtrObb = AddComponent<CollisionObb>();
    PtrObb->SetFixed(true);

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");

    auto PtrDraw = AddComponent<PNTStaticDraw>();
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetOwnShadowActive(true);
    PtrDraw->SetTextureResource(L"SKY_TX");
}
 ここでは、Transformコンポーネントを取得して、コンストラクタで渡された、スケーリング、回転、位置を設定します。
 その後
    //衝突判定
    auto PtrObb = AddComponent<CollisionObb>();
    PtrObb->SetFixed(true);
 という記述でOBB衝突判定コンポーネントを設定してます。OBBというのはAABBを回転できるようにしたものです。このサンプルでは障害物は回転してないですが、回転したオブジェクトの配置も可能です。
 SetFixed(true);は、衝突判定コンポーネントの設定ですが、衝突しても影響受けないという設定です。通常は影響を受けるです。衝突判定は、衝突後滑るとか反発するという設定が可能です。しかしここでのFixedBoxクラスは障害物ですので、プライヤーや敵が衝突したからと言って、それに反動して動かないようにしています。
 FixedBoxクラスの記述自体はそんなに複雑ではないですが、それを配置するためにはGameStage::CreateFixedBox()関数で、一つ一つ配置しています。実際にゲーム作成を行う場合、このようなゲーム盤上の配置オブジェクトは、CSVファイルなどで位置や大きさなどを定義し、それを読み込みながら配置したほうが、カスタマイズしやすくなりますし、可読性も高くなります。

 さて、GameStage::CreateFixedBox()関数では、各ボックスを配置した後以下のような記述があります。(赤い部分です)
//固定のボックスの作成
void GameStage::CreateFixedBox() {
    //配列の初期化
    vector< vector > Vec = {
        {
            Vec3(1.0f, 0.5f, 40.0f),
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(9.5f, 0.25f, 20.0f)
        },

    //中略

    };

    //ボックスのグループを作成
    auto BoxGroup = CreateSharedObjectGroup(L"FixedBoxes");
    //オブジェクトの作成
    for (auto v : Vec) {
        auto BoxPtr = AddGameObject(v[0], v[1], v[2]);
        //ボックスのグループに追加
        BoxGroup->IntoGroup(BoxPtr);
    }
    //最初の2つのセルマップへのボックスのコスト設定
    SetCellMapCost(L"CellMap");
    //奥のセルマップへのボックスのコスト設定
    SetCellMapCost(L"CellMap2");
}
 SetCellMapCos()関数は、セルマップからセルの配列を取り出し、それぞれのセルにボックスがかかってないか確認し、かかってたらセルのコストを-1に設定しています。
 以下がその内容です。
//固定のボックスのコストをセルマップに反映
void GameStage::SetCellMapCost(const wstring& CellMapGroupName) {
    //セルマップ内にFixedBoxの情報をセット
    auto Group = GetSharedObjectGroup(CellMapGroupName);
    auto BoxGroup = GetSharedObjectGroup(L"FixedBoxes");

    //セルマップグループを取得
    for (auto& gv : Group->GetGroupVector()) {
        auto MapPtr = dynamic_pointer_cast<StageCellMap>(gv.lock());
        if (MapPtr) {
            //セルマップからセルの配列を取得
            auto& CellVec = MapPtr->GetCellVec();
            //ボックスグループからボックスの配列を取得
            auto& BoxVec = BoxGroup->GetGroupVector();
            vector<AABB> ObjectsAABBVec;
            for (auto& v : BoxVec) {
                auto FixedBoxPtr = dynamic_pointer_cast<FixedBox>(v.lock());
                if (FixedBoxPtr) {
                    auto ColPtr = FixedBoxPtr->GetComponent<CollisionObb>();
                    //ボックスの衝突判定かラッピングするAABBを取得して保存
                    ObjectsAABBVec.push_back(ColPtr->GetWrappingAABB());
                }
            }
            //セル配列からセルをスキャン
            for (auto& v : CellVec) {
                for (auto& v2 : v) {
                    for (auto& vObj : ObjectsAABBVec) {
                        if (HitTest::AABB_AABB_NOT_EQUAL(v2.m_PieceRange, vObj)) {
                            //ボックスのABBとNOT_EQUALで衝突判定
                            v2.m_Cost = -1;
                            break;
                        }
                    }
                }
            }
        }
    }
}
 こうしておくことで、この後説明する敵キャラの経路検索でボックスを回り込みながらプレイヤーを追いかけるようになります。

敵キャラの作成

 ゲームステージ上はプレイヤーが先に追加されますが、敵キャラの説明を先に行います。
 敵キャラはボックス同様Character.h/cppにあります。
 まず、敵キャラはステートとステートマシンを実装しています。BaseCrossにはいくつかのタイプのステート、あるいはBehavior(行動)、そしてステートマシンがあります。
 ステートやBehaviorは、階層化もできますが、今回は一番単純なステートを使用します。
 ここで、ステートとは何かをちょっとだけ説明します。
 動的に変化するオブジェクトは状態というのを持ってます。例えば待っている状態追いかけている状態また攻撃する状態などです。
 これらを、例えばOnUodate()関数などの中でif文やswitch文により分岐すると、簡単なものならいいのですが複雑になればなるほど何が何だか分からなくなっていきます。また、バグも多くなります。
 そんな時状態を効率よく分岐させる機能jがあれば、コードも可読性が上がりますし、なによりよくわからないバグを防ぐことができます。
 まず状態ですが、これをステートと呼びます。オブジェクトは常に何らかのステートの状態に属します。
 ステートの作成方法もいくつかあるのですが、今回の敵キャラに実装しているのは階層化しないステートです。
 階層化しないステートでは、いくつかの関数がありその関数に従って、オブジェクトの動作を定義します。
 前置きはこのくらいで実際にコードを見てみましょう。Character.hには、敵キャラのステートの宣言があります。
//--------------------------------------------------------------------------------------
/// デフォルトステート
//--------------------------------------------------------------------------------------
class EnemyDefault : public ObjState<Enemy>
{
    EnemyDefault() {}
public:
    //ステートのインスタンス取得
    DECLARE_SINGLETON_INSTANCE(EnemyDefault)
    virtual void Enter(const shared_ptr<Enemy>& Obj)override;
    virtual void Execute(const shared_ptr<Enemy>& Obj)override;
    virtual void Exit(const shared_ptr<Enemy>& Obj)override;
};

//--------------------------------------------------------------------------------------
/// プレイヤーを追いかけるステート
//--------------------------------------------------------------------------------------
class EnemySeek : public ObjState<Enemy>
{
    EnemySeek() {}
public:
    //ステートのインスタンス取得
    DECLARE_SINGLETON_INSTANCE(EnemySeek)
    virtual void Enter(const shared_ptr<Enemy>& Obj)override;
    virtual void Execute(const shared_ptr<Enemy>& Obj)override;
    virtual void Exit(const shared_ptr<Enemy>& Obj)override;
};
 まず、ObjStateテンプレートクラスの派生クラスとしてステートを宣言します。今回は2つのステートを宣言しています。
 ステートはEnter()、Execute()、Exit()の各関数を実装します。それぞれ仮想関数になっていて、Enter()は、そのステートに入ったときに1回だけ呼ばれます。
 Execute()は、アップデート時に毎ターン呼ばれます。そしてExit()は、そのステートから抜けるときに呼ばれます。
 ステートシングルトンになっています。なので、例えば敵キャラは3体ありますが、それぞれのインスタンスで使用するステートのインスタンスは一つです。
 そのために複数の敵キャラが使いまわししますので、ステーにはメンバ変数を記述してはいけません
 両ステートにある
    //ステートのインスタンス取得
    DECLARE_SINGLETON_INSTANCE(EnemyDefault)
 は、マクロです。マクロでコードを作成しています。具体的には、上記のマクロは
    static shared_ptr<EnemyDefault> Instance();
 に展開されます。これはInstance()関数というstatic関数の宣言を生成するマクロです。
 では、各ステートの実体を見てみましょう、以下がそのコードです。Character.cppにあります。
    //--------------------------------------------------------------------------------------
    /// デフォルトステート
    //--------------------------------------------------------------------------------------
    //ステートのインスタンス取得
    IMPLEMENT_SINGLETON_INSTANCE(EnemyDefault)

    void EnemyDefault::Enter(const shared_ptr<Enemy>& Obj) {
    }

    void EnemyDefault::Execute(const shared_ptr<Enemy>& Obj) {
        if (!Obj->DefaultBehavior()) {
            Obj->GetStateMachine()->ChangeState(EnemySeek::Instance());
        }
    }
    void EnemyDefault::Exit(const shared_ptr<Enemy>& Obj) {
    }

    //--------------------------------------------------------------------------------------
    /// プレイヤーを追いかけるステート
    //--------------------------------------------------------------------------------------
    //ステートのインスタンス取得
    IMPLEMENT_SINGLETON_INSTANCE(EnemySeek)

    void EnemySeek::Enter(const shared_ptr<Enemy>& Obj) {
        auto PtrSeek = Obj->GetComponent<SeekSteering>();
        PtrSeek->SetUpdateActive(true);
    }

    void EnemySeek::Execute(const shared_ptr<Enemy>& Obj) {
        if (!Obj->SeekBehavior()) {
            Obj->GetStateMachine()->ChangeState(EnemyDefault::Instance());
        }
    }

    void EnemySeek::Exit(const shared_ptr<Enemy>& Obj) {
        auto PtrSeek = Obj->GetComponent<SeekSteering>();
        PtrSeek->SetUpdateActive(false);
    }
 関数はそろってますが、単純な記述ばかりです。まず、デフォルトステートですが、これは敵の待機時のステートです。敵は、プレイヤーが自分のテリトリーに入るまではじっとしてます。このステートがデフォルトステートです。
 まず、宣言部にもあったように、インスタンス取得用のマクロがあります。
    //ステートのインスタンス取得
    IMPLEMENT_SINGLETON_INSTANCE(EnemyDefault)
 がそうです。展開すると
shared_ptr<EnemyDefault> EnemyDefault::Instance() {
    static shared_ptr<EnemyDefault> instance;
    if(!instance) { 
        instance = shared_ptr<EnemyDefault>(new EnemyDefault);
    }
    return instance;
}
 となります。赤い部分に注目すると、これは型名なわけですが、これだけの短いコードに、実に5か所EnemyDefaultが出てきます。これを機械的に記述するのもおっくうですし、バグのもとにもなるので、マクロ化してあります。
 個人的にはできるだけマクロの使用は最小限には抑えたいのですが、この部分の場合は仕方がありません。

EnemyDefault::Enter()関数EnemyDefault::Exit()関数は空関数です。EnemyDefault::Execute()関数には記述がありますが、オブジェクト(つまりEnemy型)のメンバ関数DefaultBehavior()関数を呼び出してその戻り値次第で、ステートをEnemySeekにチェンジしています。このDefaultBehavior()関数の内容は、後ほど説明します。
 続いてEnemySeekステートですが、マクロはEnemyDefaultと同様です。
 こちらはEnemySeek::Enter()関数、EnemySeek::Exit()関数に記述があります。といいましても、オブジェクトのSeekSteeringコンポーネントを取り出し、それの処理を有効か無効かに設定しています。ステートのEnter()関数やExit()関数はこのように、ステートの前処理、後処理を記述します。
 ステートの毎ターンごとの処理EnemySeek::Execute()関数は、オブジェクトのSeekBehavior()関数を呼び、その戻り値によってステートをEnemyDefaultにチェンジするかどうかの処理をしています。

 では、Enemyクラスのほうを見てみましょう。
 まず、ステートを分岐するためのマネージャ的なクラスであるステートマシンをメンバ変数に持ちます。  以下はEnemyクラスの宣言部です。Character.hに記述されてます。
//--------------------------------------------------------------------------------------
//  敵
//--------------------------------------------------------------------------------------
class Enemy : public GameObject {
protected:
    weak_ptr<StageCellMap> m_CelMap;
    Vec3 m_Scale;
    Vec3 m_StartRotation;
    Vec3 m_StartPosition;
    vector<CellIndex> m_CellPath;
    //現在の自分のセルインデックス
    int m_CellIndex;
    //めざす(次の)のセルインデックス
    int m_NextCellIndex;
    //ターゲットのセルインデックス
    int m_TargetCellIndex;
    shared_ptr<StateMachine<Enemy>> m_StateMachine;
    //進行方向を向くようにする
    void RotToHead();
public:
    //構築と破棄
    Enemy(const shared_ptr<Stage>& StagePtr,
        const shared_ptr<StageCellMap>& CellMap,
        const Vec3& Scale,
        const Vec3& Rotation,
        const Vec3& Position
    );
    virtual ~Enemy();
    //プレイヤーの検索
    bool SearchPlayer();

    //デフォルト行動
    virtual bool DefaultBehavior();
    //Seek行動
    bool SeekBehavior();
    //アクセサ
    shared_ptr< StateMachine<Enemy> > GetStateMachine() const {
        return m_StateMachine;
    }
    //初期化
    virtual void OnCreate() override;
    //操作
    virtual void OnUpdate() override;
    virtual void OnUpdate2() override;
};
 赤い部分がステートマシンです、このクラスで、必要に応じてステートの切り替えを行います。また、ステートマシンのアクセサ(取得)のメンバ関数を持ってます。
 Enemyクラスはコンストラクタでセルマップ(のスマートポインタ)を渡します。渡されたセルマップは
    weak_ptr<StageCellMap> m_CelMap;
 に、weak_ptrとして保持します。ここはshared_ptrで持っていてもおおむね問題ないかと思いますが、安全策(メモリリーク発生の温床になる)ので安全策でweak_ptrにしてます。

 以下は、実体です。まず、Enemy::OnCreate()関数です。
void Enemy::OnCreate() {
    auto PtrTransform = GetComponent<Transform>();
    PtrTransform->SetPosition(m_StartPosition);
    PtrTransform->SetScale(m_Scale);
    PtrTransform->SetRotation(m_StartRotation);
    //重力をつける
    auto PtrGravity = AddComponent<Gravity>();
    //Rigidbodyをつける
    auto PtrRigid = AddComponent<Rigidbody>();
    //反発係数は0.5(半分)
    PtrRigid->SetReflection(0.5f);
    auto PtrSeek = AddComponent<SeekSteering>();
    PtrSeek->SetUpdateActive(false);
    //パス検索
    auto MapPtr = m_CelMap.lock();
    if (!MapPtr) {
        throw BaseException(
            L"セルマップが不定です",
            L"if (!MapPtr) ",
            L" Enemy::OnCreate()"
        );
    }
    auto PathPtr = AddComponent<PathSearch>(MapPtr);

    //SPの衝突判定をつける
    auto PtrColl = AddComponent<CollisionSphere>();
    PtrColl->SetIsHitAction(IsHitAction::AutoOnParent);

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_SPHERE");

    auto PtrDraw = AddComponent<PNTStaticDraw>();
    PtrDraw->SetMeshResource(L"DEFAULT_SPHERE");
    PtrDraw->SetTextureResource(L"TRACE2_TX");
    //透明処理をする
    SetAlphaActive(true);

    m_StateMachine = make_shared<StateMachine<Enemy>>(GetThis<Enemy>());
    m_StateMachine->ChangeState(EnemyDefault::Instance());
}
 ここではまず、Transformコンポーネントを取得し、スケール、回転、位置を設定します。
 続いて重力(Gravity)コンポーネント速度を使った物理計算(Rigidbody)コンポーネントを追加します。
 そのうえでSeekSteeringコンポーネントも追加します。このコンポーネントは何かを追いかけるコンポーネントです。このように○○Steeringとなっているコンポーネントは、フォースをもとに加速を計算し、それでRigidbodyの速度を変更するコンポーネントです。これらの動きの特徴はあくまで希望であって決定ではないということです。
 例えばある一定の動きをしている物体の動きを変えたければ、変えたい方向に押したりまたは、逆方向に引いたりしますね。しかし、実際に力を加えた方向にすぐに変わるかというとそうではないです。それはその物体の質量現在の速度に依存します。軽くて遅い物体であれば簡単に方向を変えられますが、重くて速い物体の方向を変えるのは至難の業です。そういう、現実に皆さんが、普段から何気なく使っている物理特性をシミュレーとしたものです。
 SeekSteeringコンポーネント何かを追いかけるコンポーネントですが、何かの方向に力を加えるだけで、何かのほうにオブジェクト場所を移動するわけではないのです。
 このような形なので、追いかけている途中に何かにぶつかったら、動きが変わったり、あるいは跳ね返ったりします。
 SeekSteeringコンポーネントは初期状態では無効にしておきます。
 続いて経路検索コンポーネントを追加します。以下の記述です。
    //パス検索
    auto MapPtr = m_CelMap.lock();
    if (!MapPtr) {
        throw BaseException(
            L"セルマップが不定です",
            L"if (!MapPtr) ",
            L" Enemy::OnCreate()"
        );
    }
    auto PathPtr = AddComponent<PathSearch>(MapPtr);
 PathSearchコンポーネントが経路検索です。ここでは追加しておくだけです。
 その後、CollisionSphere(球体衝突判定)、そして描画用のコンポーネントを追加します。最後に
    m_StateMachine = make_shared<StateMachine<Enemy>>(GetThis<Enemy>());
    m_StateMachine->ChangeState(EnemyDefault::Instance());
 でステートマシンを構築します。最初のステートはEnemyDefaultとしています。(Instanse関数の戻り値を渡します)。
 こうしておくとOnUpdate()関数は以下のようにシンプルなものになります。
    void Enemy::OnUpdate() {
        //ステートマシンのUpdateを行う
        //この中でステートの切り替えが行われる
        m_StateMachine->Update();
    }
 ステートマシンは、コメントのように、各ステートを動的に切り替えながら更新処理を行います。

 さて、ではEnemyDefaultから呼ばれるEnemy::DefaultBehavior()関数です。
bool Enemy::DefaultBehavior() {
    auto PtrRigid = GetComponent<Rigidbody>();
    auto Velo = PtrRigid->GetVelocity();
    Velo *= 0.95f;
    PtrRigid->SetVelocity(Velo);
    auto MapPtr = m_CelMap.lock();
    if (MapPtr) {
        auto PlayerPtr = GetStage()->GetSharedGameObject<Player>(L"Player");
        auto PlayerPos = PlayerPtr->GetComponent<Transform>()->GetPosition();
        CellIndex PlayerCell;
        if (MapPtr->FindCell(PlayerPos, PlayerCell)) {
            return false;
        }
    }
    return true;
}
 ここでは、まずRigidbodyコンポーネントを取り出し、速度を0.95倍にしています。
 これはどういう処理かというと、EnemySeekステートから戻ってきたときに、速度の勢いがついているときのためで、毎ターン0.95倍することで、違和感なくストップするようになってます。
 続く処理はプレイヤーが自分のテリトリーに入っているかどうかです、入っていたら、falseを返します。セルマップの
MapPtr->FindCell(PlayerPos, PlayerCell)
 という呼び出しは、指定した3次元上の位置がセルマップの中に含まれていたらPlayerCellにそのセル情報を返しtrueが返ってきます。つまりセルマップの中にいたらtrueです。
 その場合、Enemy::DefaultBehavior()はfalseを返します。この戻り値は逆でも(trueを返す)でもいいかと思いますが、ステート内の処理で、行動関数を呼び出してfalseなたステート変更という記述にしたいためこうしています。
 続いて、EnemySeekから呼ばれるEnemy::SeekBehavior()は以下のようになってます(抜粋)。
bool Enemy::SeekBehavior() {
    auto PlayerPtr = GetStage()->GetSharedGameObject<Player>(L"Player");
    auto PlayerPos = PlayerPtr->GetComponent<Transform>()->GetPosition();
    auto MapPtr = m_CelMap.lock();
    if (MapPtr) {
        if (SearchPlayer()) {
            auto PtrSeek = GetComponent<SeekSteering>();
            if (m_NextCellIndex == 0) {
                //中略
            }
            else {
                //中略
            }
            return true;
        }
        else {
                //中略
        }
    }
    return false;
}
 ここで一番重要なのは、赤くなっているようにSearchPlayer()呼び出しです。この関数はEnemyのメンバ関数でセル検索そのものを呼び出しています。
bool Enemy::SearchPlayer() {
    auto MapPtr = m_CelMap.lock();
    if (MapPtr) {
        auto PathPtr = GetComponent<PathSearch>();
        auto PlayerPtr = GetStage()->GetSharedGameObject<Player>(L"Player");
        auto PlayerPos = PlayerPtr->GetComponent<Transform>()->GetPosition();
        m_CellPath.clear();
        //パス検索をかける
        if (PathPtr->SearchCell(PlayerPos, m_CellPath)) {
            //検索が成功した
            m_CellIndex = 0;
            m_TargetCellIndex = m_CellPath.size() - 1;
            if (m_CellIndex == m_TargetCellIndex) {
                //すでに同じセルにいる
                m_NextCellIndex = m_CellIndex;
            }
            else {
                //離れている
                m_NextCellIndex = m_CellIndex + 1;

            }
            return true;
        }
        else {
            //失敗した
            m_CellIndex = -1;
            m_NextCellIndex = -1;
            m_TargetCellIndex = -1;
        }
    }
    return false;
}
 このようにPathSearchコンポーネントSearchCell()関数を呼び出します。この関数は、検索に成功すると、第2引数に経路のセルの配列を返します。それをメンバ変数に保存して、失敗したらfalseを返すような形です。
 呼び出し元のEnemy::SeekBehavior()は、プレイヤーがテリトリーから離れたらステートを変更するようfalseを返します。
 このようにEnemySeekステートでは毎ターン経路検索を実行します。これを、例えばプレイヤーが動かなければ再検索しないなどの最適化を行うことは可能です。
 ただ、PathSearchコンポーネントSearchCell()関数は、自分から目標にレイ(直線)を飛ばして障害物に当たらなかったら経路検索しないで直接経路に返すという処理をします。つまり、目標が見えれば直接向かうというアルゴリズムになっていますので、ある程度は最適化されていると思います。

プレイヤー

 最後にプレイヤーですが、この解説もかなりの分量になってしまったので、別のサンプルで解説します(プレイヤーの処理はほぼ同じ処理のものがサンプルには多いので)。

 さて、この項では、コンポーネントの説明やセルマップの説明など、ちょっと欲張りすぎた感があります。別のサンプルでもコンポーネントはどんどん出てきますので、別の項も参照ください。