702.凸型、組合せオブジェクトと物理世界のAI


 このサンプルはFullSample702というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 リビルドして実行すると以下の画面が出てきます。

 

図0702a

 

 プレイヤーを動かすと、大きめの球体と凸型オブジェクトがごろごろ追いかけてきます。また、上からプリミティブが組み合わさった物体が落ちてきます。Bボタンで球を発射できます。追いかけるオブジェクトは、当たればひるみますが、逃げればまた追いかけてきます。

 このサンプルのテーマは2つあります。1つ目は凸型オブジェクトと組みあわせオブジェクトです。
 凸型オブジェクトへっこんだ部分がないオブジェクトです。物理ライブラリはこのようなオブジェクトも物理世界に加えることが可能です。
 もともと、BaseCrossでは多面体の形状を作成することができます。今回はこの中で正12面体を作成し、物理世界への加え方を紹介します。同様の方法で正4面体、正8面体、正20面体も作成できますので、興味があったら実装してみてください。
 また組み合わせオブジェクトボックスやカプセルといったプリミティブな形状を組み合わせて新しい形状を作成します。もともとあるマルチメッシュに物理計算が付いたものと考えることができます。

 もう一つのテーマはAI処理です。といっても簡単なものですが、チュートリアル106などでも紹介した追いかけるオブジェクトAI処理を、物理世界のオブジェクトに実装してみました。
 これまでのサンプルとは一味違った追いかけるオブジェクトを表現しています。

追いかけるAI処理

 まず、追いかけるオブジェクトの処理ですが、球体と正12面体の2種類があります。同じようなコードが予想されるので、共通の親クラスとしてSeekObjectクラスを用意します。また、このクラスはステートマシンも実装します。以下はSeekObject::OnCreate()関数です。Character.cppにあります。
void SeekObject::OnCreate() {
    //オブジェクトのグループを得る
    auto Group = GetStage()->GetSharedObjectGroup(L"SeekObjectGroup");
    //グループに自分自身を追加
    Group->IntoGroup(GetThis<SeekObject>());
    //ステートマシンの構築
    m_StateMachine.reset(new StateMachine<SeekObject>(GetThis<SeekObject>()));
    //最初のステートをSeekFarStateに設定
    m_StateMachine->ChangeState(FarState::Instance());
}
 このように、ここには描画系の準備や、メッシュの作成などは行っておりません。SeekObjectクラスには追いかけるAI処理のみ記述します。
 ステートはチュートリアル106のように2種類用意します。FarState(プレイヤーから遠い位置にいる処理)NearState(プレイヤーに近いときの処理)です。
 チュートリアル106ではここからステアリング行動クラスを呼び出していましたが、今回のサンプルでは物理世界ですのでRigidbodyコンポーネントは利用できません。ですので、自前で記述します。
 といってもステアリングの中身まで記述する必要はあありません。以下はSeekObject::SeekBehavior()関数ですSeekステアリングを実装します。
void SeekObject::SeekBehavior() {
    auto Pos = GetComponent<Transform>()->GetPosition();
    auto TargetTrans = GetStage()->GetSharedObject(L"Player")->GetComponent<Transform>();
    auto TargetPos = TargetTrans->GetPosition();
    bsm::Vec3 WorkForce;
    WorkForce = Steering::Seek(GetVelocity(), TargetPos, Pos, m_MaxSpeed);
    Steering::AccumulateForce(m_Force, WorkForce, m_MaxForce);
}
 赤くなっているところがステアリング処理です。Steering::Seek()探索行動Steering::AccumulateForce()フォースの調整です。Seek()で作り出しているフォースをAccumulateForce()で、上限値を超えてないか調整します。最終的にm_Forceが変化します。
 このようにSeekObject::SeekBehavior()のほかにSeekObject::ArriveBehavior()(到着行動)、SeekObject::SeparationBehavior()(分離行動)を作成します。
 各ステートではこれらの行動を組合せを変えて実装します。すなわちFarStateではSeparationBehavior()とSeekBehavior()NearStateではSeparationBehavior()とArriveBehavior()です。以下は、NearState::Execute()関数です。
void NearState::Execute(const shared_ptr<SeekObject>& Obj) {
    Obj->SeparationBehavior();
    Obj->ArriveBehavior();
    if (Obj->GetTargetToLen() >= Obj->GetStateChangeSize()) {
        Obj->GetStateMachine()->ChangeState(FarState::Instance());
    }
}
 この中で条件によってはステートを変更します。

凸型オブジェクト

 さて、SeekObjectクラスではAI処理のみ記述します。実際に形状の処理は、その派生クラスで行います。以下は凸型形状のオブジェクトであるActivePsConvexクラスの宣言です。Character.hに記述があります。
class ActivePsConvex : public SeekObject {
    Vec3 m_Position;
    //メッシュ(描画用)
    static shared_ptr<MeshResource> m_ConvexMesh;
    //物理計算用
    static shared_ptr<PsConvexMeshResource> m_PsConvexMesh;
public:
    //構築と破棄
    ActivePsConvex(const shared_ptr<Stage>& StagePtr,
        const Vec3& Position
    );
    virtual ~ActivePsConvex();
    //初期化
    virtual void OnCreate() override;
    //現在の速度を得る(仮想関数)
    virtual Vec3 GetVelocity() const;
    //更新
    virtual void OnUpdate() override;
};
 ここでは赤くなっているところのようにstatic変数を使っています。これの意味するところは、m_ConvexMeshm_PsConvexMesh各インスタンスで使いまわしするということです。これまでのサンプルではリソース登録という形でシーンなどにメッシュやテクスチャを登録していました。ここでもそれでいいのですが、1つのクラスでしか使いまわししないすなわちインスタンスのみの使いまわしであればシーンでのリソース登録はしなくても充分です。そういう場合はstatic変数にしておきます。
 クラス内にstatic変数を作成した場合、cpp側でも以下のようにstatic変数の実体記述する必要があります。Character.cpp内ですが
    //static変数の初期化
    shared_ptr<MeshResource> ActivePsConvex::m_ConvexMesh = nullptr;
    shared_ptr<PsConvexMeshResource> ActivePsConvex::m_PsConvexMesh = nullptr;
 のような形です。これらの初期化はActivePsConvex::OnCreate()関数で行います。
void ActivePsConvex::OnCreate() {
    if (!m_ConvexMesh || !m_PsConvexMesh) {
        vector<VertexPositionNormalTexture> vertices;
        vector<uint16_t> indices;
        MeshUtill::CreateDodecahedron(0.5, vertices, indices);
        m_ConvexMesh = MeshResource::CreateMeshResource(vertices, indices, false);
        m_PsConvexMesh = PsConvexMeshResource::CreateMeshResource(vertices, indices, 0.5f);
    }
    auto PtrTransform = GetComponent<Transform>();
    PtrTransform->SetScale(Vec3(1.0f));
    PtrTransform->SetQuaternion(Quat());
    PtrTransform->SetPosition(m_Position);

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

    auto PtrDraw = AddComponent<BcPNTStaticDraw>();
    PtrDraw->SetFogEnabled(true);
    PtrDraw->SetMeshResource(m_ConvexMesh);
    PtrDraw->SetTextureResource(L"WALL_TX");

    //物理計算凸面
    PsConvexParam param;
    param.m_ConvexMeshResource = m_PsConvexMesh;
    param.m_Mass = 1.0f;
    //慣性テンソルの計算(球と同じにする)
    param.m_Inertia = BasePhysics::CalcInertiaSphere(0.5f, param.m_Mass);
    param.m_MotionType = PsMotionType::MotionTypeActive;
    param.m_Quat = Quat();
    param.m_Pos = m_Position;
    auto PsPtr = AddComponent<PsConvexBody>(param);
    PsPtr->SetDrawActive(true);
    //親クラスのOnCreateを呼ぶ
    SeekObject::OnCreate();
}
 ここでは赤くなっている部分が2か所あります。上の部分ではstatic変数ならではの処理です。static変数が初期化されてなければ2つのメッシュリソースを作成します。MeshResourceクラスはこれまでも何回も出てきたので説明は省略します。
 PsConvexMeshResourceクラス物理計算用のメッシュリソースで、コンストラクタに頂点の配列とインデックスの配列を渡します。頂点の形式についてはVertexPositionNormalTexture型でなくてもよいVertexPositionNormal型でもよいのですが、PsConvexMeshResourceクラスはこのバックアップを所持しておいて後から取得できるので、Textureもあったほうが汎用的と考え、そのような設計にしました。(上記コードは、バックアップは使ってません)
 2つめの赤くなっているところは親クラスのOnCreate()を呼び出しています。C++の文法によって、仮想関数は一番末端の継承先の関数が呼ばれます。ここでのOnCreate()は、先ほど説明したように親クラスにもOnCreate()が記述されているので、親クラスの関数を呼ぶ必要があります。
 同様にActivePsConvex::OnUpdate()関数でも親クラスの関数を呼び出しています。
void ActivePsConvex::OnUpdate() {
    //親クラスのOnUpdateを呼ぶ
    SeekObject::OnUpdate();
    auto PtrPs = GetComponent<PsConvexBody>();
    //現在のフォースを設定
    PtrPs->ApplyForce(GetForce());
}
 OnCreate()では親クラスはあとから呼び出し、OnUpdate()では親クラスは先に呼び出しています。この違いは処理の内容です。OnCreate()の場合は、親クラスはステートマシンの設定を行っています。この処理は一連のOnCreate()の最後に記述したほうが良いので、後から呼んでます。
 対してOnUpdate()ではAI処理が親クラス側で実装されるので、派生クラスでは、その計算結果(フォース)を
    //現在のフォースを設定
    PtrPs->ApplyForce(GetForce());
 と物理オブジェクトに設定します。ですから派生クラス側で後から処理になります。このように、親クラスと継承先で同じ仮想関数を実装する場合、実行順番が重要になる場合が多いので注意しましょう。
 あとは追いかける球のクラスですが、ActivePsSphereクラスになります。同様の処理ですので説明は省略します。

 さて、以上で物理オブジェクトへのAI処理は実装できました。衝突後の処理球が当たったときの動作は物理計算がやってくれます。もしこの部分に手を加える必要があればOnUpdate2()を定義して記述してください。物理処理はOnUpdate()とOnUpdate2()の間に行われます。

組み合わせオブジェrクト

 最後ですが組み合わせオブジェrクトです。ActivePsCombinedObjectクラスが実装クラスです。以下はActivePsCombinedObject::OnCreate()関数です。
void ActivePsCombinedObject::OnCreate() {
    auto PtrTransform = GetComponent<Transform>();
    PtrTransform->SetScale(Vec3(1.0f));
    PtrTransform->SetQuaternion(m_Qt);
    PtrTransform->SetPosition(m_Position);
    //合成オブジェクトの準備
    PsCombinedParam param;
    //質量は重くする
    param.m_Mass = 3.0f;
    //Box用の慣性(慣性テンソル)を計算
    param.m_Inertia = BasePhysics::CalcInertiaBox(Vec3(2.5f, 1.0f, 1.0f), param.m_Mass);
    param.m_MotionType = PsMotionType::MotionTypeActive;
    param.m_Quat = m_Qt;
    param.m_Pos = m_Position;
    //合成されるプリミティブ(0番目、ボックス)
    PsCombinedPrimitive primitive;
    primitive.reset();
    primitive.m_CombinedType = PsCombinedType::TypeBox;
    primitive.m_HalfSize = Vec3(0.5f, 0.5f, 1.5f);
    primitive.m_OffsetPosition = Vec3(-2.0f, 0.0f, 0.0f);
    //合成オブジェクトに追加
    param.AddPrim(primitive);
    //合成されるプリミティブ(1番目、ボックス)
    primitive.reset();
    primitive.m_CombinedType = PsCombinedType::TypeBox;
    primitive.m_HalfSize = Vec3(0.5f, 1.5f, 0.5f);
    primitive.m_OffsetPosition = Vec3(2.0f, 0.0f, 0.0f);
    //合成オブジェクトに追加
    param.AddPrim(primitive);
    //合成されるプリミティブ(2番目、カプセル)
    primitive.reset();
    primitive.m_CombinedType = PsCombinedType::TypeCapsule;
    primitive.m_HalfLen = 1.5f;
    primitive.m_Radius = 0.5f;
    primitive.m_OffsetPosition = Vec3(0.0f, 0.0f, 0.0f);
    //合成オブジェクトに追加
    param.AddPrim(primitive);
    //物理コンポーネント(合成)
    auto PsPtr = AddComponent<PsCombinedBody>(param);
    PsPtr->SetDrawActive(true);
    //物理コンポーネントに合わせて描画コンポーネント(影も)を作成
    CreateDrawComp(param);
}
 べた書きの部分が多いのであまりきれいなソースとは言えませんが、組み合わせを実装するためにはそれなりに準備が必要なことがわかります。
 まず、合成されるオブジェクトの構造体PsCombinedParamを初期化します。
 そうしたうえで、基本となる形状(ボックスとかカプセル)を、PsCombinedPrimitiveという設定用の構造体に作成し、それを、PsCombinedParam構造体に加えます。上記赤くなっている部分は、0番目のボックスを加えている部分です。
 もう一つ赤くなっている(下の部分)は、描画関連を別関数CreateDrawComp()で処理します。ここでは物理計算で計算した結果をマルチメッシュリソースで表示するための処理です。マルチメッシュリソースチュートリアル011で紹介しています。
 組み合わせオブジェクトはAI処理などは行わずに、動きは物理計算に任せています。