12.Update系の操作

1201.コンポーネントとステートと行動

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

 実行結果は以下のような画面が出ます。

 

図1201a

 

 チュートリアル001(FullSample101)と似ている感じですが、ちょっと違います。
 コントローラでプレイヤーを動かして、追いかけるオブジェクトに体当たりすると、相手がジャンプします。
 これは追いかけるオブジェクトプレイヤーと衝突した時の処理を実装したわけですが、ここ項では、衝突判定と衝突イベント、ステートとステートマシン、コンポーネントと行動、など、更新処理(UpDate系と呼んでいます)の処理を説明したいと思います。

 このサンプルにはプレイヤー追いかけるオブジェクトという、2種類のクラスのUpdate系処理が実装されています。

プレイヤー

 まずプレイヤーから説明します。このサンプルはFullSample101を修正して作成したので、プレイヤーについてはほとんど同じですが、FullSample101の項では説明しきれなかった部分も合わせて説明します。
 ダブって説明もあるかと思いますが、ご容赦ください。

 まず全体的な説明ですが、BaseCross64の基本的な構造として、オブジェクトはステージに配置されるという原則があります。BaseCross64ではステージと呼んでいますが、他のフレームワークではシーンというかもしれません。
 BaseCross64ではシーンというのはステージを管理するオブジェクトとなります。(詳しくは【0】BaseCross64概要を参照ください)

プレイヤーのステージへの追加

 プレイヤーのステージへの追加GameStageクラスGameStage::CreatePlayer()関数で行います。以下がコードです。
//プレイヤーの作成
void GameStage::CreatePlayer() {
    //プレーヤーの作成
    auto PlayerPtr = AddGameObject<Player>();
    //シェア配列にプレイヤーを追加
    SetSharedGameObject(L"Player", PlayerPtr);
    PlayerPtr->AddTag(L"Player");
}
 AddGameObjectテンプレート関数は可変長引数になっていて、上記の記述は、Playerクラスは、独自の引数無しで構築されるのがわかります。
 このような記述は、Playerクラスコンストラクタで確認できます。以下はPlayerクラスコンストラクタです。
Player::Player(const shared_ptr<Stage>& StagePtr) :
    GameObject(StagePtr),
    m_Velocity(0)
{}
 第1引数のconst shared_ptr<Stage>& StagePtrが存在するのがわかります。つまり、ステージのAddGameObjectテンプレート関数は、第1引数は必ずconst shared_ptr<Stage>& StagePtrを渡します。このことはライブラリ中、AddGameObjectテンプレート関数を確認するとわかります。以下がStage::AddGameObject()テンプレート関数のソースです。
template<typename T, typename... Ts>
shared_ptr<T> AddGameObject(Ts&&... params) {
    try {
        auto Ptr = ObjectFactory::Create<T>(GetThis<Stage>(), params...);
        PushBackGameObject(Ptr);
        return Ptr;
    }
    catch (...) {
        throw;
    }
}
 第1引数に自分自身のshared_ptrを渡してます。第2引数以降params...となっているのでそのまま渡されます。
 この仕様は例えばSeekObjectクラスコンストラクタを確認するとわかります。以下です。
SeekObject::SeekObject(const shared_ptr<Stage>& StagePtr, const Vec3& StartPos) :
    GameObject(StagePtr),
    m_StartPos(StartPos),
    m_StateChangeSize(5.0f),
    m_Force(0),
    m_Velocity(0)
{
}
 これを、GamaStage上に配置しているのはGameStage::CreateSeekObject()関数です。
//追いかけるオブジェクトの作成
void GameStage::CreateSeekObject() {
    //オブジェクトのグループを作成する
    auto Group = CreateSharedObjectGroup(L"SeekGroup");
    //配列の初期化
    vector<Vec3> Vec = {
    { 0, 0.125f, 10.0f },
    { 10.0f, 0.125f, 0.0f },
    { -10.0f, 0.125f, 0.0f },
    { 0, 0.125f, -10.0f },
    };

    //オブジェクトの作成
    for (auto& v : Vec) {
        AddGameObject<SeekObject>(v);
    }
}
 AddGameObjectテンプレート関数初期位置情報を渡しているのがわかります。

 ちょっと脱線しましたが、このようにAddGameObjectテンプレート関数の仕組みを理解すると、コンストラクタに引数を必要とするオブジェクトが簡単に作成できるのがわかります。

プレイヤーの初期化

 プレイヤーの初期化Player::OnCreate()で行います。AddGameObjectテンプレート関数でステージ上に配置するとフレームワークから呼び出されます。
void Player::OnCreate() {

    //初期位置などの設定
    auto Ptr = GetComponent<Transform>();
    Ptr->SetScale(0.25f, 0.25f, 0.25f); //直径25センチの球体
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    Ptr->SetPosition(0, 0.125f, 0);

    //CollisionSphere衝突判定を付ける
    auto PtrColl = AddComponent<CollisionSphere>();
    //重力をつける
    auto PtrGra = AddComponent<Gravity>();

    //文字列をつける
    auto PtrString = AddComponent<StringSprite>();
    PtrString->SetText(L"");
    PtrString->SetTextRect(Rect2D<float>(16.0f, 16.0f, 640.0f, 480.0f));

    //中略

    //カメラを得る
    auto PtrCamera = dynamic_pointer_cast<MyCamera>(OnGetDrawCamera());
    if (PtrCamera) {
        //MyCameraである
        //MyCameraに注目するオブジェクト(プレイヤー)の設定
        PtrCamera->SetTargetObject(GetThis<GameObject>());
        PtrCamera->SetTargetToAt(Vec3(0, 0.25f, 0));
    }
}
 更新処理に関係がある部分のみ抜粋しています。
 プレイヤーはTransform、CollisionSphere、Gravityの3つのコンポーネントを実装します。
 TransformはいきなりGetComponent<Transform>();と取得できます。

プレイヤーの更新

 プレイヤーはコントローラの動きに従って移動します。これはカメラであるMyCameraクラスと密接に関係します。
 カメラはMyCamera.h/cppに記述があります。
 MyCameraクラスの動きを詳しく読むとわかると思いますが、このカメラはコントローラのスティックの動きによって、動的にカメラ位置(Eye)カメラの視点(At)を決定します。
 プレイヤーはこの値を取得して、移動する方向を決定します。カメラの方向をまっすぐ進むとしてそこからスティックの向きを加算します。そうするとプレイヤーの向かう方向が決定します。以下の図を参照ください。

 

図1201b

 

 これを実装しているのがVec3 Player::GetMoveVector()const関数です。
Vec3 Player::GetMoveVector() const {
    Vec3 Angle(0, 0, 0);
    //コントローラの取得
    auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
    if (CntlVec[0].bConnected) {
        if (CntlVec[0].fThumbLX != 0 || CntlVec[0].fThumbLY != 0) {
            float MoveLength = 0;   //動いた時のスピード
            auto PtrTransform = GetComponent<Transform>();
            auto PtrCamera = OnGetDrawCamera();
            //進行方向の向きを計算
            Vec3 Front = PtrTransform->GetPosition() - PtrCamera->GetEye();
            Front.y = 0;
            Front.normalize();
            //進行方向向きからの角度を算出
            float FrontAngle = atan2(Front.z, Front.x);
            //コントローラの向き計算
            float MoveX = CntlVec[0].fThumbLX;
            float MoveZ = CntlVec[0].fThumbLY;
            Vec2 MoveVec(MoveX, MoveZ);
            float MoveSize = MoveVec.length();
            //コントローラの向きから角度を計算
            float CntlAngle = atan2(-MoveX, MoveZ);
            //トータルの角度を算出
            float TotalAngle = FrontAngle + CntlAngle;
            //角度からベクトルを作成
            Angle = Vec3(cos(TotalAngle), 0, sin(TotalAngle));
            //正規化する
            Angle.normalize();
            //移動サイズを設定。
            Angle *= MoveSize;
            //Y軸は変化させない
            Angle.y = 0;
        }
    }
    return Angle;
}
 この関数は、Player::MovePlayer()から呼ばれます。
void Player::MovePlayer() {
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    Vec3 Angle = GetMoveVector();
    if (Angle.length() > 0.0f) {
        auto Pos = GetComponent<Transform>()->GetPosition();
        Pos += Angle * ElapsedTime * m_Speed;
        GetComponent<Transform>()->SetPosition(Pos);
        //回転の計算
        auto UtilPtr = GetBehavior<UtilBehavior>();
        UtilPtr->RotToHead(Angle, 1.0f);
    }
}
 GetMoveVector()関数はここからしか呼ばれないので、Player::MovePlayer()内にロジックを記述しても問題はないのですが、関数に小分けにすることで、処理を明確にできます。こういったロジックを記述する関数は、20行以内くらいに書くようにするとコードがすっきりします。
 Player::MovePlayer()関数はさらにPlayer::OnUpdate()関数から呼ばれます。
 この関数はフレームワークから毎ターン呼び出される関数です。多重定義して実装します。
//更新
void Player::OnUpdate() {
    //コントローラチェックして入力があればコマンド呼び出し
    m_InputHandler.PushHandle(GetThis<Player>());
    MovePlayer();
}
 ここで、コントローラーからのボタンのプッシュを検証するハンドラ関数を呼んでいます。この関数はテンプレート関数で以下のようになっています。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();
            }
        }
    }
};
 ここではAボタンが押された瞬間かどうかを検証して、その場合は、指定されたオブジェクトのOnPushA()関数を呼び出します。
 このように、イベントハンドラ的に記述すると、可読性が高くなります。
 また、このハンドラを使用するオブジェクトはOnPushA()関数を持たなければいけません。逆に言えば、例えば敵キャラであっても、OnPushA()関数を実装すれば、コントローラのAボタンに対応するコードを記述することができます。
 もし、このハンドラを使用しようとするオブジェクトがOnPushA()関数を持たなければコンパイルエラーになります。
 こういった、テンプレート処理で記述する手法をジェネリックプログラミングといいます。

 さてOnPushA()関数の中身ですが以下のようになっています。
void Player::OnPushA() {
    auto Grav = GetComponent<Gravity>();
    Grav->StartJump(Vec3(0,4.0f,0));
}
 ここではGravityコンポーネントを取得して、StartJump()メンバ関数を呼び出しています。その名の通り、ジャンプさせる関数です。プレイヤーはAボタンでジャンプします。

 Player::OnUpdate()では、コントローラのハンドラ処理をしたあと、先ほど紹介したMovePlayer()関数を呼び出します。

プレイヤーの更新2

 フレームワークは更新処理用の関数として、もう一つOnUpdate2()関数を呼び出します。OnUpdate()関数との違いは、この2つの関数の間に、コンポーネントの処理や衝突判定などの処理が入ることです。つまりOnUpdate2()関数は、様々な自動処理の後に呼ばれるということです。必要であればOnUpdate2()関数を多重定義して実装します。
 ここでは、デバッグ用の文字列を表示しています。
void Player::OnUpdate2() {
    //文字列の表示
    DrawStrings();
}
 デバッグ用の文字列は、オブジェクトが思うように動かないときに、数々のパラメータがどういう値を持ってるかなど調べるのに役立ちます。ここで呼び出しているDrawStrings()関数では、以下のように、FPS値やプレイヤーの位置、重力加速度を取得して表示しています。必要がなくなれば、DrawStrings()関数呼び出しをコメントにすればいい形です。
void Player::DrawStrings() {

    //文字列表示
    auto fps = App::GetApp()->GetStepTimer().GetFramesPerSecond();
    wstring FPS(L"FPS: ");
    FPS += Util::UintToWStr(fps);
    FPS += L"\nElapsedTime: ";
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    FPS += Util::FloatToWStr(ElapsedTime);
    FPS += L"\n";

    auto Pos = GetComponent<Transform>()->GetPosition();
    wstring PositionStr(L"Position:\t");
    PositionStr += L"X=" + Util::FloatToWStr(Pos.x, 6, Util::FloatModify::Fixed) + L",\t";
    PositionStr += L"Y=" + Util::FloatToWStr(Pos.y, 6, Util::FloatModify::Fixed) + L",\t";
    PositionStr += L"Z=" + Util::FloatToWStr(Pos.z, 6, Util::FloatModify::Fixed) + L"\n";

    wstring GravStr(L"GravityVelocoty:\t");
    auto GravVelocity = GetComponent<Gravity>()->GetGravityVelocity();
    GravStr += L"X=" + Util::FloatToWStr(GravVelocity.x, 6, Util::FloatModify::Fixed) + L",\t";
    GravStr += L"Y=" + Util::FloatToWStr(GravVelocity.y, 6, Util::FloatModify::Fixed) + L",\t";
    GravStr += L"Z=" + Util::FloatToWStr(GravVelocity.z, 6, Util::FloatModify::Fixed) + L"\n";
    wstring str = FPS + PositionStr + GravStr;
    //文字列コンポーネントの取得
    auto PtrString = GetComponent<StringSprite>();
    PtrString->SetText(str);
}

追いかけるオブジェクト

 プレイヤーを追いかけるオブジェクトはSeekObjectです。Character.h/cppに記述があります。

SeekObjectのコンストラクタ

 SeekObjectのコンストラクタは、ステージのほかに開始位置を渡します。以下の様な形です。
SeekObject::SeekObject(const shared_ptr<Stage>& StagePtr, const Vec3& StartPos) :
    GameObject(StagePtr),
    m_StartPos(StartPos),
    m_StateChangeSize(5.0f),
    m_Force(0),
    m_Velocity(0)
{
}
 SeekObjectにはm_StartPosのほかにm_Force(力)m_Velocity(速度)という2つのベクトル(Vec3)を持っています。
 m_StateChangeSizeステートが切り替わる値です。
 このSeekObjectの構築はGamaStageで行います。GameStage::CreateSeekObject()です。
void GameStage::CreateSeekObject() {
    //オブジェクトのグループを作成する
    auto Group = CreateSharedObjectGroup(L"SeekGroup");
    //配列の初期化
    vector<Vec3> Vec = {
    { 0, 0.125f, 10.0f },
    { 10.0f, 0.125f, 0.0f },
    { -10.0f, 0.125f, 0.0f },
    { 0, 0.125f, -10.0f },
    };

    //オブジェクトの作成
    for (auto& v : Vec) {
        AddGameObject<SeekObject>(v);
    }
}
 この関数を呼び出しているのはGameStage::OnCreate()です。

SeekObjectの初期化

 初期化はSeekObject::OnCreate()です。
void SeekObject::OnCreate() {
    auto PtrTransform = GetComponent<Transform>();
    PtrTransform->SetPosition(m_StartPos);
    PtrTransform->SetScale(0.125f, 0.25f, 0.25f);
    PtrTransform->SetRotation(0.0f, 0.0f, 0.0f);

    //オブジェクトのグループを得る
    auto Group = GetStage()->GetSharedObjectGroup(L"SeekGroup");
    //グループに自分自身を追加
    Group->IntoGroup(GetThis<SeekObject>());
    //Obbの衝突判定をつける
    auto PtrColl = AddComponent<CollisionObb>();
    //重力をつける
    auto PtrGra = AddComponent<Gravity>();
    //分離行動をつける
    auto PtrSep = GetBehavior<SeparationSteering>();
    PtrSep->SetGameObjectGroup(Group);

    //中略

    //ステートマシンの構築
    m_StateMachine.reset(new StateMachine<SeekObject>(GetThis<SeekObject>()));
    //最初のステートをSeekFarStateに設定
    m_StateMachine->ChangeState(SeekFarState::Instance());
}
 ここではプレイヤーと同じように衝突判定重力を実装しています。
 プレイヤーと違うところはステートマシンの構築を行っているところです(赤くなっています)。

ステートマシンとは

 ゲーム上のオブジェクトは状態というのを持っています。
 逃げてる状態とか戦う状態とか瀕死の状態とかです。今回のプレイヤーは状態は持ってませんが、複雑な処理になれば、持つ必要が出てきます。
 SeekObjectに関して言えば、常にプレイヤーを追いかけているわけですがプレイヤーから一定間隔以上離れている状態(FarState)プレイヤーから一定間隔より近い状態(NearState)があります。
 この状態ステートといいます。そしてステートを管理するオブジェクトをステートマシンといいます。
 それぞれのステートごとに、SeekObjectの実装が変わります。すなわち、ステートにより、行う行動が変わるというわけです。
 それを、以下の表に表します。

ステート名 行動
SeekFarState スピードを出してプレイヤーを追いかける。
お互いはくっつかないようにする。
移動方向に頭を向ける
SeekNearState プレイヤーに近づく(到着する)
お互いはくっつかないようにする
移動方向に頭を向ける

 ステートマシンには、最初のステートをSeekFarStateに設定します。

ステートの実装

 ステートは以下のように宣言します。SeekFarStateを例にとります。
class SeekFarState : public ObjState<SeekObject>
{
    SeekFarState() {}
public:
    static shared_ptr<SeekFarState> Instance();
    virtual void Enter(const shared_ptr<SeekObject>& Obj)override;
    virtual void Execute(const shared_ptr<SeekObject>& Obj)override;
    virtual void Exit(const shared_ptr<SeekObject>& Obj)override;
};
 ObjStateテンプレートクラスを親クラスに持つ派生クラスとして作ります。
 メンバ関数としてInstance()、Enter()、Execute()、Exit()を作成します。
 Instance()はこのステートを参照するときに使います。static関数として作成します。ステートシングルトンとして作成します。シングルトンとは、プログラム中1つのインスタンスしか作れないクラスです。
 Enter()はこのステートに入ったときに呼ばれる関数です。一回だけ呼ばれます。
 Execute()はこのステーに入っている間、毎ターン呼ばれる関数です。
 Exit()はこのステートから出る(ステートマシンのChangeState()関数が呼ばれてステートを抜ける)時に呼ばれます。
 これらの関数をの呼び出しを行うのがステートマシンです。
 以下はSeekFarStateの実体です。
shared_ptr<SeekFarState> SeekFarState::Instance() {
    static shared_ptr<SeekFarState> instance(new SeekFarState);
    return instance;
}
void SeekFarState::Enter(const shared_ptr<SeekObject>& Obj) {
}
void SeekFarState::Execute(const shared_ptr<SeekObject>& Obj) {
    auto PtrSeek = Obj->GetBehavior<SeekSteering>();
    auto PtrSep = Obj->GetBehavior<SeparationSteering>();
    auto Force = Obj->GetForce();
    Force = PtrSeek->Execute(Force, Obj->GetVelocity(), Obj->GetTargetPos());
    Force += PtrSep->Execute(Force);
    Obj->SetForce(Force);
    Obj->ApplyForce();
    float f = bsm::length(Obj->GetComponent<Transform>()->GetPosition() - Obj->GetTargetPos());
    if (f < Obj->GetStateChangeSize()) {
        Obj->GetStateMachine()->ChangeState(SeekNearState::Instance());
    }
}

void SeekFarState::Exit(const shared_ptr<SeekObject>& Obj) {
}
 このようにSeekFarState::Enter()SeekFarState::Exit()空関数なのがわかります。このステートは入ったとき出るときは何もしません。
 Enter()、Execute()、Exit()は引数にSeekObjectのインスタンスが渡されますのでそのメンバ関数にアクセスできます。
 ここでの処理は、表にあるように
1、スピードを出してプレイヤーを追いかける。
2、お互いはくっつかないようにする。
3、移動方向に頭を向ける
 を実装します。1と2はSeekFarState::Executeで実装します。
 SeekSteering行動SeparationSteering行動Execute関数フォースを返します。つまりは、SeekObjectにを加えるわけです。それが
    Force = PtrSeek->Execute(Force, Obj->GetVelocity(), Obj->GetTargetPos());
    Force += PtrSep->Execute(Force);
    Obj->SetForce(Force);
    Obj->ApplyForce();
 というわけです。SetForceおよびApplyForceの違いは、前者はm_Forceのアクセサであり、後者はTransformへの反映です。
 SeekFarState::Executeでは、処理の結果、プレイヤーと距離が一定の長さより短くなったらSeekNearStateに変更します。
 SeekNearStateでの処理は、SeekSteeringArriveSteeringに代わる形です。

SeekObjectの更新処理

 このようにステートの実装が終わったらSeekObject::OnUpdate()を実装します。
void SeekObject::OnUpdate() {
    m_Force = Vec3(0);
    //ステートマシンのUpdateを行う
    //この中でステートの切り替えが行われる
    m_StateMachine->Update();
    auto PtrUtil = GetBehavior<UtilBehavior>();
    PtrUtil->RotToHead(1.0f);
}
 このように、基本的にステートマシンのUpdate()さえ呼べば、ステートの切り替えなどはステートマシンがやってくれます。
 ここでは共通処理として3、移動方向に頭を向けるを実装しています。UtilBehaviorという行動クラスを呼び出していますが、このクラスは汎用的に使用きる行動クラスです。RotToHead()関数は、頭を進行方向に向ける関数です。
 ここで気を付けたいのは、毎ターンm_Force = Vec3(0);とフォースを初期化しているところです。フォースとのターンで影響を与える力を計算するわけですから、この処理が必要となります。
 それに対してm_Velocityは毎ターン変化はしますが、初期化されるわけではありません。

SeekObjectのイベント処理

 SeekObjectプレイヤーと衝突した時にジャンプするようになってます。その処理はSeekObject::OnCollisionEnter()関数で行います。この関数は自身が衝突判定を持っていて、なおかつFixedでない場合に呼び出されます。
void SeekObject::OnCollisionEnter(shared_ptr<GameObject>& Other) {
    if (Other->FindTag(L"Player")) {
        auto Grav = GetComponent<Gravity>();
        Grav->StartJump(Vec3(0, 4.0f, 0));
    }
}
 ここでは衝突した相手がL"Player"というタグを持っていたら、ジャンプするように記述しています。