14.Rigidbodyと物理世界

1401.物理世界への介入

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

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

 

図1401a

 

 この章は1102.物理計算を使った3Dの配置に紹介した物理的処理(Rigidbodyコンポーネント)をゲームにどのように反映していくか、のサンプルです。
 これまでのサンプルとは違い、物理計算とのかかわりを考えながら記述する必要があります。とはいえそんなに難しいことはありません。このゲームオブジェクトは物理計算するかしないかを常に考えながら実装すれば、大きな問題にはならないでしょう。
 ここで、Xボタンを押すと球が発射されます。その球をステージ上のオブジェクトに当てると、そのオブジェクトが黄緑色に変化します。

 

図1401b

 

 その状態で、Yボタンを押し続けると、選択されたオブジェクトが、プレイヤーの上部に引き寄せられます。Yボタンを離すと、そのオブジェクトは落ちてきます。場合によってはプレイヤーの頭から落ちてくる場合もあるので注意しましょう。(だからといってプレイヤーに何か損害が出るわけではないですが・・・)

 

図1401c

 


物理世界とのインターフェイス

 ここで、注目したいのは物理世界それ以外の世界(これまでのBaseCross64の世界)とのインターフェイス(橋渡し)をどうすればいいのか、という部分です。
 1102.物理計算を使った3Dの配置に紹介したサンプルはすべてが物理世界に入っているので、その部分を意識することはありませんでした。しかし、ゲーム全体を物理世界に入れてしまうと、それこそ、単なるシミュレーションで終わってしまいます。
 物理計算をゲームに入れる場合、言い方によってはどのようにして物理世界に介入し変えていくかがポイントになります。

 さて、サンプルの画面を見て、どのオブジェクトを物理世界から外すかを考えてみましょう。プレイヤー配置される物理オブジェクトはお互いに落ちてきたり、衝突したり、反発したりが必要です。台座も必要でしょう。
 しかし、発射される球体はどうでしょうか?このオブジェクトを物理世界に入れると、何かに当たった瞬間に、相手が動いてしまいます。このサンプルでは発射される球体的当てのような役割ではなくオブジェクトの選択です。ですから、物理世界からは外します。
 以下、発射される球体(FireSphere)OnCreate()関数ですCharactrr.cppに記述があります。
void FireSphere::OnCreate() {
    auto ptrTrans = GetComponent<Transform>();

    ptrTrans->SetScale(Vec3(m_Scale));
    ptrTrans->SetQuaternion(Quat());
    ptrTrans->SetPosition(m_Emitter);
    //コリジョンを付ける(ボリューム取得のため)
    auto ptrColl = AddComponent<CollisionSphere>();
    ptrColl->SetAfterCollision(AfterCollision::None);

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

    auto ptrDraw = AddComponent<BcPNTStaticDraw>();
    ptrDraw->SetFogEnabled(true);
    ptrDraw->SetMeshResource(L"DEFAULT_SPHERE");
    ptrDraw->SetTextureResource(L"SKY_TX");

    GetStage()->SetSharedGameObject(L"FireSphere", GetThis<GameObject>());
}
 このように、このオブジェクトにはRigidbodyコンポーネントは実装されていません。また赤くなっているところのようにCollisionSphereが実装されています。このコンポーネントは自身のSPHEREを取得するために実装されているので衝突判定を実装しているわけではありません。ですのでAfterCollision::Noneのヒットアクション設定になっています。
 このようなオブジェクトが、ステージ内に配置されるのは最初にXボタンが押されたときです。それはプレイヤーに記述します。以下はプレイヤーのXボタンハンドラです。
//Xボタンハンドラ
void Player::OnPushX() {
    auto Ptr = GetComponent<Transform>();
    Vec3 Pos = Ptr->GetPosition();
    Pos.y += 0.5f;
    Quat Qt = Ptr->GetQuaternion();
    Vec3 Rot = Qt.toRotVec();
    float RotY = Rot.y;
    Vec3 velo(sin(RotY), 0.05f, cos(RotY));
    velo.normalize();
    velo *= 20.0f;

    auto ShPtr = GetStage()->GetSharedGameObject<FireSphere>(L"FireSphere",false);
    if (ShPtr) {
        ShPtr->Reset(Pos, velo);
    }
    else {
        GetStage()->AddGameObject<FireSphere>(Pos, velo);
    }
}
 赤くなっているところのように、もしL"FireSphere"SharedGameObjectとして登録されていたら、そのオブジェクトを発射させ、そうでなければAddGameObjectしています。既に登録されているときは、Reset()というメンバ関数を使います。

 さて、このようにして発射されたは、物理世界との衝突判定をしなければいけません。それを実装しているのはFireSphere側にあります。
 以下のようにFireSphere::OnUpdate()に記述します(Character.cppにあります)。
void FireSphere::OnUpdate() {
    auto ptrTrans = GetComponent<Transform>();
    if (ptrTrans->GetPosition().y > -20.0f) {
        float elapsedTime = App::GetApp()->GetElapsedTime();
        Vec3 Ac = Vec3(0, -9.8f, 0) * 1.0f;
        m_Velocity += Ac * elapsedTime;
        auto Pos = ptrTrans->GetPosition();
        Pos += m_Velocity * elapsedTime;
        ptrTrans->SetPosition(Pos);
    }
    else {
        //じっとしている
        ptrTrans->SetPosition(Vec3(0, -20.0f, 0));
        return;
    }
    auto ptrColl = GetComponent<CollisionSphere>();
    //物理オブジェクトを持つ配列の取得
    vector<shared_ptr<Rigidbody>> PsComptVec;
    GetStage()->GetUsedDynamicCompoentVec<Rigidbody>(PsComptVec);
    for (auto& v : PsComptVec) {
        auto ptrG = dynamic_pointer_cast<ActivePsObject>(v->GetGameObject());
        if (ptrG) {
            auto ptrRegSp = dynamic_pointer_cast<RigidbodySphere>(v);
            auto ptrRegBox = dynamic_pointer_cast<RigidbodyBox>(v);
            auto ptrRegCap = dynamic_pointer_cast<RigidbodyCapsule>(v);
            bool hold = false;
            if (ptrRegSp) {
                if (HitTest::SPHERE_SPHERE(ptrRegSp->GetSPHERE(), ptrColl->GetSphere())) {
                    hold = true;
                }
            }
            else if (ptrRegBox) {
                Vec3 ret;
                if (HitTest::SPHERE_OBB(ptrColl->GetSphere(), ptrRegBox->GetOBB(), ret)) {
                    hold = true;
                }
            }
            else if (ptrRegCap) {
                Vec3 ret;
                if (HitTest::SPHERE_CAPSULE(ptrColl->GetSphere(), ptrRegCap->GetCAPSULE(), ret)) {
                    hold = true;
                }
            }
            if (hold) {
                auto ptrHold = m_HoldObject.lock();
                if (ptrHold) {
                    ptrHold->SetHold(false);
                }
                m_HoldObject = ptrG;
                ptrG->SetHold(true);
                ptrTrans->ResetPosition(Vec3(0, -20, 0));
                break;
            }
        }
    }
}
 まず、このFireSphereは、1回目の発射でステージに配置された以降はどこかに存在します。プレイヤーがXボタンで発射するときに呼び戻すような形です。もちろんUpdateActiveやDrawActiveを操作して、更新しなくて見えないようにする、方法もありますが、FireSphereはステートマシンも持たない単純なクラスなのでどこかにしまっておく方法をとってます。
 序盤、
    if (ptrTrans->GetPosition().y > -20.0f) {
        float elapsedTime = App::GetApp()->GetElapsedTime();
        Vec3 Ac = Vec3(0, -9.8f, 0) * 1.0f;
        m_Velocity += Ac * elapsedTime;
        auto Pos = ptrTrans->GetPosition();
        Pos += m_Velocity * elapsedTime;
        ptrTrans->SetPosition(Pos);
    }
    else {
        //じっとしている
        ptrTrans->SetPosition(Vec3(0, -20.0f, 0));
        return;
    }
 の処理は、もし位置のY座標が-20より大きければ、移動や重力を反映させるという処理です。逆に言えばもし位置のY座標が-20以下ならステージの下のほうでじっとしています。
 続いて物理オブジェクトとの判定ですが、まず、自分自身のCollisionSphereを取得しておいて
    //物理オブジェクトを持つ配列の取得
    vector<shared_ptr<Rigidbody>> PsComptVec;
    GetStage()->GetUsedDynamicCompoentVec<PsBodyComponent>(PsComptVec);
 とRigidbodyの派生クラスのコンポーネントの配列を取得します。Stageクラスにあるこのメンバ関数はGameObjectを飛び越して、各GameObjectが指定のコンポーネント(かその派生クラス)を持っていた場合、配列に収めてくれる関数です。
 PsComptVecコンポーネントの配列なので所持するGameObjectを取得する場合はv->GetGameObject()のように取得します。そうやって衝突判定を行っているのが、そのあとの処理です。
    for (auto& v : PsComptVec) {
        auto ptrG = dynamic_pointer_cast<ActivePsObject>(v->GetGameObject());
        if (ptrG) {
            auto ptrRegSp = dynamic_pointer_cast<RigidbodySphere>(v);
            auto ptrRegBox = dynamic_pointer_cast<RigidbodyBox>(v);
            auto ptrRegCap = dynamic_pointer_cast<RigidbodyCapsule>(v);
            bool hold = false;
            if (ptrRegSp) {
                if (HitTest::SPHERE_SPHERE(ptrRegSp->GetSPHERE(), ptrColl->GetSphere())) {
                    hold = true;
                }
            }
            else if (ptrRegBox) {
                Vec3 ret;
                if (HitTest::SPHERE_OBB(ptrColl->GetSphere(), ptrRegBox->GetOBB(), ret)) {
                    hold = true;
                }
            }
            else if (ptrRegCap) {
                Vec3 ret;
                if (HitTest::SPHERE_CAPSULE(ptrColl->GetSphere(), ptrRegCap->GetCAPSULE(), ret)) {
                    hold = true;
                }
            }
            if (hold) {
                auto ptrHold = m_HoldObject.lock();
                if (ptrHold) {
                    ptrHold->SetHold(false);
                }
                m_HoldObject = ptrG;
                ptrG->SetHold(true);
                ptrTrans->ResetPosition(Vec3(0, -20, 0));
                break;
            }
        }
    }
 「RigidbodySphere、RigidbodyBox、RigidbodyCapsule」はそれぞれ、「GetSPHERE()、GetOBB()、GetCAPSULE()」というメンバ関数を持っていて簡易的な衝突判定に使えるボリューム境界を取得できます。それを使って衝突判定します。
 処理を読めばわかるように、どこかにヒットしたら、そのヒットしたオブジェクトをm_HoldObjectというメンバ変数にセットします。すでにm_HoldObjectが有効だった場合は
   ptrHold->SetHold(false);
 とホールドが終わったことをそのオブジェクトに知らせます。それを受け取ったオブジェクトはホールドされているときに使用している色を元に戻します。
 その後あたらしくホールドされたオブジェクトに
    ptrG->SetHold(true);
 とし、ここで発射オブジェクトは役割を終えるので
    ptrTrans->ResetPosition(Vec3(0, -20, 0));
 と待機場所に移動します。

 さて以上がオブジェクトのホールドの処理でした。続いて、オブジェクトを持ち上げる処理です。これはプレイヤーに記述します。
 しかしプレイヤーの説明の前に、前述したホールドされたオブジェクトのSetHold()関数の中身を見てみます。以下の内容です。
void ActivePsObject::SetHold(bool b) {
    if (b) {
        if (m_StateMachine->GetCurrentState() == ActivePsDefaultState::Instance()) {
            m_StateMachine->ChangeState(ActivePsHoldState::Instance());
            //プレイヤーに自分がホールドされていることを伝える
            auto ptrPlayer = GetStage()->GetSharedGameObject<Player>(L"Player", false);
            if (ptrPlayer) {
                ptrPlayer->SetHoldObject(GetThis<ActivePsObject>());
            }
        }
    }
    else {
        if (m_StateMachine->GetCurrentState() == ActivePsHoldState::Instance()) {
            m_StateMachine->ChangeState(ActivePsDefaultState::Instance());
        }
    }
}
 ホールドされた場合、プレイヤーのSetHoldObject()関数を呼び出し、自分自身がホールドされたことを伝えてます。
 ここで注意したいのは、ホールドが解けた時は何もプレイヤーに知らせてないことです。
 これはこのサンプル特有で、最初にホールドされたオブジェクトが設定された以降は何かしらのオブジェクトがホールドされている状態になるということです。これは通常のゲームであればホールドを解除する(何もホールドしてない状態にする)という操作が必要でしょうが、ここでは実装していません。

さて、そんなわけでプレイヤーホールドされているオブジェクトを伝えることができました。
 そのオブジェクトを引き寄せる処理ですが、プレイヤーOnPushY()、OnPressY()、OnReleaseY()で行います。
 引き寄せるだけならOnPressY()だけでいいのですが、ここで引き寄せているオブジェクトがわかるように、ラインを引きます。すなわちプレイヤーとオブジェクトをつなぐ線です。そのオブジェクトに対する捜査にOnPushY()、OnReleaseY()を使ってます。ActionLineというオブジェクトですが、最初に必要とされたときに、プレイヤーによって作成されます。OnPushY()内です。
//Yボタンハンドラ(押した瞬間)
void Player::OnPushY() {
    //ホールドしたオブジェクトがなければ何もしない
    auto ptrHold = m_HoldObject.lock();
    if (!ptrHold) {
        return;
    }
    auto ptrActionLine = m_ActionLine.lock();
    if (ptrActionLine) {
        auto Check = ptrActionLine->GetEndObj();
        auto CheckHold = dynamic_pointer_cast<GameObject>(ptrHold);
        if (Check != CheckHold) {
            ptrActionLine->SetEndObj(ptrHold);
        }
        ptrActionLine->SetDrawActive(true);
    }
    else {
        //ラインの作成
        auto ptrLine = GetStage()->AddGameObject<ActionLine>(GetThis<GameObject>(), ptrHold);
        ptrLine->SetDrawActive(true);
        m_ActionLine = ptrLine;
    }
}
 ここでm_ActionLineActionLineのweak_ptrです。lockが成功すればすでに実装されているのがわかるので、それを使います。実装されてなければ
        //ラインの作成
        auto ptrLine = GetStage()->AddGameObject<ActionLine>(GetThis<GameObject>(), ptrHold);
 と作成します。
 ActionLineクラス起点のオブジェクトと終点のオブジェクトを持ちます。それぞれのオブジェクトの位置の変化により、動的にその線の状態を変えることができます。プレイヤーのホールドオブジェクトの引き寄せが始まったときに、表示させ、終わったときに表示しなくします。終わったときの処理はOnReleaseY()に記述され以下のような形です。
//Yボタンハンドラ(離した瞬間)
void Player::OnReleaseY() {
    auto ptrActionLine = m_ActionLine.lock();
    if (ptrActionLine) {
        ptrActionLine->SetDrawActive(false);
    }
}
 このラインオブジェクトはこのサンプルでは一直線ですが、ベジェ曲線のように表現できれば、引っ張る感じをもっと表現できると思います。

 さて、最後に引き寄せる処理です。プレイヤーのOnPressY()です。
//Yボタンハンドラ(押し続け)
void Player::OnPressY() {
    auto ptrTrans = GetComponent<Transform>();
    auto playerPos = ptrTrans->GetPosition();
    auto ptrHold = m_HoldObject.lock();
    if (ptrHold) {
        auto ptrPs = ptrHold->GetDynamicComponent<RigidbodySingle>(false);
        if (ptrPs) {
            auto psPos = ptrPs->GetPosition();
            float toY = 2.0f;
            if (psPos.y > 5.0f) {
                toY = 0.0f;
            }
            psPos.y = 0;
            playerPos.y = 0;
            Vec3 toPlayerVec = playerPos - psPos;
            ptrPs->WakeUp();
            ptrPs->SetLinearVelocity(Vec3(toPlayerVec.x, toY, toPlayerVec.z));
        }
    }
}
 赤くなっているところが物理オブジェクトへの介入の部分です。物理オブジェクトの速度を変化させています。その計算はそれまでにしておきます。例えば高さは5.0より高くはならないような処理も計算します。引き寄せるスピードは遠くにあるうちは速く、近くなると遅くなります。
 PsPtr->SetLinearVelocity()のまえに
    ptrPs->WakeUp();
 としているのがわかると思います。物理オブジェクトある程度動きが小さくなるか、あるいは何かに寄りかかってそれ以上動けなくなるスリープという状態に入ります。この状態は、ほかのオブジェクトがぶつかってくるとかまで続きます。
 これは余計な物理計算をしなくても済むように、物理ライブラリ側で実装されている仕組みです。
 また、この仕組みはptrPs->SetLinearVelocity()を呼び出しても起きないので、上記のように起きろ!と命令してから、速度を変更します。

 以上のように、この項では、物理世界への介入のサンプルを説明しました。
 ちょっとした実装ではありますが、このように遠くから引っ張り上げるなどの処理を加えると、あたかも超能力や魔法を使ったような動きになるのがわかります。
 このように物理世界を実装する場合は、どのようにして物理常識を変えていくかがポイントになるのではないでしょうか。
 単なるシミュレーションではなく、いろんなアイディアや世界観にあった不思議の世界を加えることで、逆に物理世界が生きてくるでしょう。