022.Rigidbody(剛体)(Dx11版)

 このサンプルはSimplSample022というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルはDx11版しかありません。Dx12版はありませんのでご了承ください。
 サンプルを起動すると以下の画面が現れます。コントローラで操作でき、Aボタンでジャンプします。Bボタンで、いわゆるステージの切り替えができます。いくつかのオブジェクトが配置されており、プレイヤーはそれらと衝突します。

 

図0022a

 

【サンプルのポイント】

 このサンプルはRigidbody(いわゆる、剛体)についての説明です。
 とはいえ、ほかゲームエンジンにあるような物理計算を綿密に行うものではありません。ここで紹介するRigidbody速度フォースを持ち、衝突判定と衝突応答を行います。回転処理は自動には行いません。
 ここで紹介するRigidbodyは、各オブジェクトと分離して管理します。Rigidbodyマネージャという管理クラスを用意し、速度の反映や衝突関連の操作はマネージャで行います。
 ポイントをまとめると以下になります。
1、Rigidbodyクラス(構造体)の実装
2、Rigidbodyマネージャの実装
 これらについて、詳しく説明します。

■1、Rigidbodyクラス(構造体)の実装■

 Rigidbodyクラス構造体として作成します。GameStage.hに宣言があります。以下、抜粋ですが
struct Rigidbody {
    //オーナー
    weak_ptr<GameObject> m_Owner;
    //重力加速度
    Vec3 m_Gravity;
    //質量
    float m_Mass;
    //現在のフォース
    Vec3 m_Force;
    //速度
    Vec3 m_Velocity;
    //コリジョンのタイプ
    CollType m_CollType;
    //Fixedかどうか
    bool m_IsFixed;
    //スケール
    Vec3 m_Scale;
    //回転
    Quat m_Quat;
    //位置
    Vec3 m_Pos;
    //1つ前のスケール
    Vec3 m_BeforeScale;
    //1つ前の回転
    Quat m_BeforeQuat;
    //1つ前の位置
    Vec3 m_BeforePos;

//中略

};
 ここでは剛体を表現する、最低限の情報をまとめてあります。m_Ownerはこの剛体を持つオブジェクトです。安全のためweak_ptrになっています。
 実際に描画に必要なのはm_Scale、m_Quat、m_Posです。これで、ワールド行列を作成し描画します。ほかのデータは、計算用変数だったり、情報だったりします。
 綿密な物理計算はしませんが、基本的にニュートンの法則に従って計算します。
 m_CollTypeCollTypeのenum値です。CollTypeは、衝突判定の形状で、以下のように定義されます。
enum class CollType {
    typeNone,
    typeSPHERE,
    typeCAPSULE,
    typeOBB,
};
 このようにこのサンプルではSPHERE(球体)、CAPSULE(カプセル)、OBB(直方体)について対応しています。
 m_BeforeScale、m_BeforeQuat、m_BeforePos処理前の情報です。Update処理が終わったとき、処理前との変更を確認できます。
 さて、このような剛体クラスがあったとき、各オブジェクトはこのクラスをどのように扱うのでしょうか。
 それは、例えばPlayerクラスOnCreate()関数に記述があります。
void Player::OnCreate() {
    vector<VertexPositionNormalTexture> vertices;
    vector<uint16_t> indices;
    MeshUtill::CreateSphere(1.0f, 18, vertices, indices);
    //メッシュの作成(変更できない)
    m_SphereMesh = MeshResource::CreateMeshResource(vertices, indices, false);
    //タグの追加
    AddTag(L"Player");
    //Rigidbodyの初期化
    auto PtrGameStage = GetStage<GameStage>();
    Rigidbody body;
    body.m_Owner = GetThis<GameObject>();
    body.m_Mass = 1.0f;
    body.m_Scale = Vec3(0.25f);
    body.m_Quat = Quat();
    body.m_Pos = m_Posision;
    body.m_CollType = CollType::typeSPHERE;
    body.SetToBefore();

    PtrGameStage->AddRigidbody(body);

}
 このようにRigidbodyの変数bodyを定義し、GameStageAddRigidbody()関数bodyを追加します。
 GameStageには、以下のようにRigidbodyManagerクラスのshared_ptrがあります。
class GameStage : public Stage {
    Vec4 m_LightDir;        ///<ライト向き
    Camera m_Camera;        ///<カメラ
    //RigidbodyManager
    shared_ptr<RigidbodyManager> m_RigidbodyManager;
public:
//中略
};
 RigidbodyManagerクラスについては後で詳しく説明しますがRigidbodyの配列があり、GameStageAddRigidbody()関数はその配列にRigidbodyを追加します。

 さてPlayerではこのようにRigidbodyの作成と登録を行うわけですが、その更新(Update)はどのように行うのでしょうか?
 Player::OnUpdate()関数に記述があります。
void Player::OnUpdate() {
    auto& body = GetStage<GameStage>()->GetOwnRigidbody(GetThis<GameObject>());
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //コントローラの取得
    auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
    if (CntlVec[0].bConnected) {
        if (!m_JumpLock) {
            //Aボタン
            if (CntlVec[0].wPressedButtons & XINPUT_GAMEPAD_A) {
                body.m_BeforePos.y += 0.01f;
                body.m_Pos.y += 0.01f;
                body.m_Velocity += Vec3(0, 4.0f, 0);
                m_JumpLock = true;
            }
        }
        Vec3 Direction = GetMoveVector();
        if (length(Direction) < 0.1f) {
            body.m_Velocity.x *= 0.9f;
            body.m_Velocity.z *= 0.9f;
        }
        else {
            //フォースで変更する場合は以下のように記述
            //body.m_Force += Direction * 10.0f;
            //速度で変更する場合は以下のように記述
            body.m_Velocity += Direction * 0.5f;
            Vec2 TempVelo(body.m_Velocity.x, body.m_Velocity.z);
            TempVelo = XMVector2ClampLength(TempVelo, 0, 5.0f);
            body.m_Velocity.x = TempVelo.x;
            body.m_Velocity.z = TempVelo.y;
        }
    }
    body.m_Force += body.m_Gravity * body.m_Mass;
}
 ここではコントローラに合わせて、プレイヤーを移動させます。その際
    auto& body = GetStage<GameStage>()->GetOwnRigidbody(GetThis<GameObject>());
 で自分自身のRigidbodyを取得し、その内容を更新します。
 更新方法は、ここでは、Velocityを変更します。
    //速度で変更する場合は以下のように記述
    body.m_Velocity += Direction * 0.5f;
 もし、VelocityではなくForceを変更する場合は、上記をコメントにして
    body.m_Force += Direction * 10.0f;
 のコメントを外します。この違いは、実際に動かしてみるとわかると思いますが、フォースを変更する形だと、慣性がより強く働きます。
 RigidbodyにはVelocityForceがあるわけですが、実際の計算は以下のよう、Velocityにまとめられます。
    Vec3 accel = m_Force * m_Mass;
    m_Velocity += accel * ElapsedTime;
 この処理はマネージャで行われますので、オブジェクト側では、ForceもしくはVelocityの修正のみ行います。
 ここでのElapsedTime前回のターンからの時間です。Forceですから、ニュートンの第2法則により
加速度 = Force / 質量
 の関係があります。ですから質量が大きいものForceの影響は小さくなります。
 また、Forceは、毎ターン0に初期化され、そのターンで加わる力を足していきます。フォースとして自動的に加算される値に重力加速度があります。これは、上記プレイヤーの処理で
    body.m_Force += body.m_Gravity * body.m_Mass;
 と追加されます。ニュートンの第2法則は
Force = 質量 * 加速度
 ともも書けますので、この方法で重力加速度を追加しています。

 さて、このPlayer::OnUpdate()ではm_Pos(つまり位置)は修正はしていません。
 Rigidbodyを使う環境では位置情報の直接変更は、しない前提です。
 もし、直接変更したいのであればPlayer::OnUpdate2()関数で行います。この関数はRigidbodyマネージャの処理後に呼ばれますので、位置を直接変更できます。以下が実体です。
void Player::OnUpdate2() {
    auto& body = GetStage<GameStage>()->GetOwnRigidbody(GetThis<GameObject>());
    if (body.m_Pos.y <= m_BaseY) {
        body.m_Pos.y = m_BaseY;
        body.m_Velocity.y = 0;
        m_JumpLock = false;
    }
    auto& StateVec = GetStage<GameStage>()->GetCollisionStateVec();
    for (auto& v : StateVec) {
        if (v.m_Src == &body) {
            Vec3 Normal = v.m_SrcHitNormal;
            Normal.normalize();
            Vec4 v = (Vec4)XMVector3AngleBetweenNormals(Vec3(0, 1, 0), Normal);
            if (v.x < 0.1f) {
                m_JumpLock = false;
                break;
            }
        }
        if (v.m_Dest == &body) {
            Vec3 Normal = v.m_SrcHitNormal;
            Normal.normalize();
            Vec4 v = (Vec4)XMVector3AngleBetweenNormals(Vec3(0, 1, 0), Normal);
            if (v.x < 0.1f) {
                m_JumpLock = false;
                break;
            }
        }
    }
    auto LenVec = body.m_Pos - body.m_BeforePos;
    LenVec.y = 0;
    auto Len = LenVec.length();
    if (Len > 0) {
        Vec3 Cross = cross(Vec3(0, 1, 0), LenVec);
        Quat Span(Cross, Len / 0.5f);
        body.m_Quat *= Span;
    }
}
 赤くなっているところで、位置を変更しています。このサンプルではプレート(いわゆるゲーム盤)には衝突判定を設けずに、その下には行けないようになっています(衝突判定を設ける方法もあります)。
 このサンプルのような処理をする場合は、位置情報を強制的に戻す必要があり、その処理を行っています。
 そのあとの処理は、回転などの処理です。ここでは進行方向に回転する処理を行っています。

■2、Rigidbodyマネージャの実装■

 プレイヤーをはじめ、配置されているオブジェクトは(プレート以外は)、Rigidbodyを持っています。持っているというのは正確ではなくて、登録されているのほうが当たっているかもしれません。
 登録先はRigidbodyマネージャです。GameStage.h/cppに実装があります。
 以下は、宣言部です。長くなりますが
class RigidbodyManager : public GameObject {
    //Rigidbodyの配列
    vector<Rigidbody> m_RigidbodyVec;
    //衝突判定
    void CollisionDest(Rigidbody& Src);
    bool CollisionStateChk(Rigidbody* p1, Rigidbody* p2);
    bool CollisionTest(Rigidbody& Src, Rigidbody& Dest, CollisionState& state);
    //衝突ステートの配列
    vector<CollisionState> m_CollisionStateVec;
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief コンストラクタ
    @param[in]  StagePtr    ステージのポインタ
    */
    //--------------------------------------------------------------------------------------
    RigidbodyManager(const shared_ptr<Stage>& StagePtr);
    //--------------------------------------------------------------------------------------
    /*!
    @brief デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~RigidbodyManager();
    //--------------------------------------------------------------------------------------
    /*!
    @brief Rigidbodyの配列を得る
    @return Rigidbodyの配列
    */
    //--------------------------------------------------------------------------------------
    const vector<Rigidbody>& GetRigidbodyVec()const {
        return m_RigidbodyVec;
    }
    vector<Rigidbody>& GetRigidbodyVec() {
        return m_RigidbodyVec;
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief 衝突情報の配列を得る
    @return 衝突情報の配列
    */
    //--------------------------------------------------------------------------------------
    const vector<CollisionState>& GetCollisionStateVec()const {
        return m_CollisionStateVec;
    }
    vector<CollisionState>& GetCollisionStateVec(){
        return m_CollisionStateVec;
    }

    //--------------------------------------------------------------------------------------
    /*!
    @brief 指定のオーナーのRigidbodyを得る
    @param[in]  OwnerPtr    オーナーのポインタ
    @return 指定のオーナーのRigidbody
    */
    //--------------------------------------------------------------------------------------
    Rigidbody& GetOwnRigidbody(const shared_ptr<GameObject>& OwnerPtr) {
        for (auto& v : m_RigidbodyVec) {
            auto shptr = v.m_Owner.lock();
            if (shptr == OwnerPtr) {
                return v;
            }
        }
        throw BaseException(
            L"指定のRigidbodyが見つかりません",
            L"!Rigidbody",
            L"RigidbodyManager::GetOwnRigidbody()"
        );
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief 初期化
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate() override {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief フォースを初期化する
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void InitRigidbody();
    //--------------------------------------------------------------------------------------
    /*!
    @brief SrcのDestからのエスケープ
    @param[in]  Src     Srcのポインタ
    @param[in]  Dest    Destのポインタ
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void Escape(Rigidbody* Src, Rigidbody* Dest);
    //--------------------------------------------------------------------------------------
    /*!
    @brief 更新
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnUpdate()override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 最終更新
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnUpdate2()override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 描画
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnDraw()override {}

};
 RigidbodyManagerマネージャは様々な更新処理を行うので、宣言をすべて紹介しましたが、その中でも大事なのは以下の2つの配列です。
    //Rigidbodyの配列
    vector<Rigidbody> m_RigidbodyVec;
//中略
    //衝突ステートの配列
    vector<CollisionState> m_CollisionStateVec;
 Rigidbodyの配列はすでに説明しましたが、各オブジェクトから追加される配列です。
 衝突ステートの配列は、衝突情報を保持しておく配列です。衝突情報は
struct CollisionState {
    Rigidbody* m_Src;
    Vec3 m_SrcHitNormal;
    Rigidbody* m_Dest;
    Vec3 m_DestHitNormal;
    float m_HitTime;
};
 という構造体にまとめられます。中に生ポインタがありますが、この配列はターン毎に初期化されるので、無理やり、ポインタをdeleteでもしない限り安全です。

 RigidbodyManagerマネージャ自体は単なるゲームオブジェクトです。しかしゲームステージ上のゲームオブジェクトの配列に加わるのではなく、別途初期化されます。GameStage::OnCreate()関数の冒頭で初期化されます。
void GameStage::OnCreate() {
    //Rigidbodyマネージャの初期化
    m_RigidbodyManager 
        = ObjectFactory::Create<RigidbodyManager>(GetThis<GameStage>());

//以下略

}
 ここで初期化されますので後続のオブジェクトは、Rigidbodyの追加を行うことができます。

 さて、Rigidbodyマネージャの実際の仕事ですが、それを説明するのにゲームステージのUpdate処理を説明します。以下はGameStage::OnUpdateStage()関数です。この関数はシーンから呼ばれます。
void GameStage::OnUpdateStage() {
    //ターン毎の初期化
    m_RigidbodyManager->InitRigidbody();
    for (auto& v : GetGameObjectVec()) {
        //各オブジェクトの更新
        v->OnUpdate();
    }
    //Rigidbodyマネージャの更新(衝突判定など)
    m_RigidbodyManager->OnUpdate();
    for (auto& v : GetGameObjectVec()) {
        //各オブジェクトの最終更新
        v->OnUpdate2();
    }
    //自分自身の更新(カメラ)
    this->OnUpdate();
    //Rigidbodyマネージャの最終更新(衝突判定情報のクリア)
    m_RigidbodyManager->OnUpdate2();
}
 赤くなっているところが、Rigidbodyマネージャの操作になります。
 まず、m_RigidbodyManager->InitRigidbody()ですが
//ターン毎の初期化
void RigidbodyManager::InitRigidbody() {
    //1つ前の位置にセットとフォースの初期化
    for (auto& v : m_RigidbodyVec) {
        v.SetToBefore();
        v.m_Force = Vec3(0);
    }
}
 コメントにもあるようにターン毎の初期化です。RigidbodySetToBefore()を呼び出すことで、現在のスケーリング、回転、位置をとっておきます。
 ここでフォースも0に初期化します。
 続いてm_RigidbodyManager->OnUpdate();ですが、以下のように様々な処理を行ってます。各オブジェクトのOnUpdate()がよばれた後ですので、それらの動きに応じた処理になります。
void RigidbodyManager::OnUpdate() {
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //フォースから速度に変換
    for (auto& v : m_RigidbodyVec) {
        Vec3 accel = v.m_Force * v.m_Mass;
        v.m_Velocity += accel * ElapsedTime;
    }

    //衝突判定を行い、ヒットがあれば速度を変更する
    if (m_RigidbodyVec.size() >= 2) {
        //衝突判定
        for (auto& v : m_RigidbodyVec) {
            CollisionDest(v);
        }
    }
    if (m_CollisionStateVec.size() >= 2) {
        //ヒットタイムでソート(ヒットタイムが)近いものに対応
        auto func = [&](CollisionState& Left, CollisionState& Right)->bool {
            return (Left.m_HitTime < Right.m_HitTime);
        };
        std::sort(m_CollisionStateVec.begin(), m_CollisionStateVec.end(), func);
    }
    //衝突応答
    for (auto& v : m_CollisionStateVec) {
        if (!v.m_Src->m_IsFixed) {
            v.m_Src->Move(v.m_HitTime);
            v.m_Src->m_Velocity.slide(v.m_SrcHitNormal);
        }
        if (!v.m_Dest->m_IsFixed) {
            v.m_Dest->Move(v.m_HitTime);
            v.m_Dest->m_Velocity.slide(v.m_DestHitNormal);
        }
    }

    //設定された速度をもとに衝突無しのオブジェクトの位置の決定
    for (auto& v : m_RigidbodyVec) {
            v.Move(ElapsedTime);
    }

    //エスケープ処理
    for (auto& v : m_CollisionStateVec) {
        if (!v.m_Src->m_IsFixed) {
            Escape(v.m_Src, v.m_Dest);
        }
        if (!v.m_Dest->m_IsFixed) {
            Escape(v.m_Dest ,v.m_Src);
        }
    }
}
 まず、各オブジェクトで設定されたフォースを速度にまとめます。
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //フォースから速度に変換
    for (auto& v : m_RigidbodyVec) {
        Vec3 accel = v.m_Force * v.m_Mass;
        v.m_Velocity += accel * ElapsedTime;
    }
 の部分です。続いて衝突判定です。
    //衝突判定を行い、ヒットがあれば速度を変更する
    if (m_RigidbodyVec.size() >= 2) {
        //衝突判定
        for (auto& v : m_RigidbodyVec) {
            CollisionDest(v);
        }
    }
 2個以上のRigidbodyがある場合に行います。CollisionDest(v)というのは、多対多の衝突判定の1次元目です。この関数は、2次元めの判定を呼び出します。
 この関数を追いかけていくとわかりますが、衝突があった場合にm_CollisionStateVecに衝突情報が追加されます。ですので、続く
    if (m_CollisionStateVec.size() >= 2) {
        //ヒットタイムでソート(ヒットタイムが)近いものに対応
        auto func = [&](CollisionState& Left, CollisionState& Right)->bool {
            return (Left.m_HitTime < Right.m_HitTime);
        };
        std::sort(m_CollisionStateVec.begin(), m_CollisionStateVec.end(), func);
    }
    //衝突応答
    for (auto& v : m_CollisionStateVec) {
        if (!v.m_Src->m_IsFixed) {
            v.m_Src->Move(v.m_HitTime);
            v.m_Src->m_Velocity.slide(v.m_SrcHitNormal);
        }
        if (!v.m_Dest->m_IsFixed) {
            v.m_Dest->Move(v.m_HitTime);
            v.m_Dest->m_Velocity.slide(v.m_DestHitNormal);
        }
    }
 で衝突応答の処理を行ってます。ヒットタイムでソートというのは、ターン時間での衝突時間の速いものから順に応答処理を行うということです。1つのオブジェクトが複数のオブジェクトと衝突している場合があります。そういう場合に、速い時間で衝突したものから応答処理をしています。
<
 衝突判定は、衝突そのものより、応答のほうが処理は大変です。物理計算のライブラリなどはこの応答処理に多くのコードを割いています。
 このサンプルでは、このあたりをかなり簡略化しています。以下のような丸め込みがあります。
1、同じターン内で複数の衝突があった場合、本来なら、最初の衝突の応答を計算して、
 その後それに合わせた判定を行わなければならないが、簡略化している。
2、本来なら、回転も応答処理に入れるべきだが、その処理は行っていない
3、回転要素を加味しない代わりに、スライドと呼ばれる処理で移動速度を変更している
 このため、この衝突応答を修正することで、もっと細かな処理を実装することができます。CollisionStateには衝突ペアのほかに、衝突法線衝突時間も入っているので、いろいろな計算が可能と思います。

 Rigidbodyマネージャの操作ではその後、実際に位置の変更を行い、最後にエスケープ処理というのを行っています。この処理は、衝突応答によって位置の変更がされた場合、衝突した相手とまだ衝突している場合に、相手の領域から脱出する処理です。この処理は意外と重要で、これを行わないと、オブジェクトが別のオブジェクトの中に挟まってしまったりします。
 どういう処理をしているかどうかは
    Escape(v.m_Src, v.m_Dest);
 の呼び出しを追いかけてみてください。

 さてこのようにRigidbodyManager::OnUpdate()が行われるのですが、各ターンの最後にm_RigidbodyManager->OnUpdate2();が実行されます。内容は以下です。
    void RigidbodyManager::OnUpdate2() {
        //衝突情報のクリア
        m_CollisionStateVec.clear();
    }
 このように衝突情報のクリアを行います。衝突情報は各オブジェクトのOnUpdate2()呼び出しの時点では保持されてますので。もし衝突があったらなどの処理は各オブジェクトのOnUpdate2()で処理することができます。

 この項ではRigidbodyの作成、使用例について述べました。この方法ばかりではなくほかにもいろんな実装方法があります。例えばBaseCrossのフルバージョンではRigidbodyCollisionは別に実装し、またコンポーネントとして実装しています。
 最後になりましたが、このサンプルにはPlayerのほかにCapsulePlayerObbPlayerが定義されています。
 GameStage::OnCreate()関数
        //プレイヤーの作成
        AddGameObject<Player>(
            L"TRACE_TX",
            true,
            Vec3(0.0f, 0.125f, 0.0f)
            );

        //カプセルプレイヤーの作成
        //AddGameObject<CapsulePlayer>(
        //  L"TRACE_TX",
        //  true,
        //  Vec3(0.0f, 0.25f, 0.0f)
        //  );

        //OBBプレイヤーの作成
        //AddGameObject<ObbPlayer>(
        //  L"TRACE_TX",
        //  true,
        //  Vec3(0.0f, 0.125f, 0.0f)
        //  );
 のような記載がありますので、CapsulePlayerもしくはObbPlayerを実装したい場合は、コメントを外してください。ただしこれらの別タイプのプレイヤーは共存できませんので、どれか一つを選択してみましょう。