112.物理処理

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

 

図0112a

 


物理ライブラリについて

 いわゆる物理ライブラリが、BaseCrossに実装されました。
 他のどのフレームワークでも当たり前のようについている物理ライブラリはこれまで(2017年12月まで)実装してきませんでした。
 物理ライブラリもいろいろありますがBaseCrossではSony Computer Entertainment社が著作権を持つPhysicsEffectsというライブラリを実装しました。このライブラリはゲーム制作者のための物理シミュレーション(インプレスジャパン)という書籍で紹介されているライブラリで、書籍のサイトからフリーソフト版(BSDライセンス)のものがダウンロードできます。
 そのライブラリをBaseCrossと相性がいいように若干カスタマイズしたものを使用しています。

サンプルについて

 サンプルを起動すると、上記の画面が出てきます。上から物体が落ちてきます。物体同士はお互いに影響を受けながら、転がったりはじかれたりします。
 Xボタンを押すと、プレイヤーが球を発射します。この球は物体と衝突すると、相手を飛ばしたり、あるいは自分が跳ね返ったりします。
 Bボタンステージの再読み込みが行われ、上から降ってくるところから再現することができます。再読み込みでも、プレイヤーの位置は保存されますので、近くから降ってくる物体を見ることができます。
 これらの物体の処理はCharacter.h/cppで行ってます。配置はこれまでのようにGameStage::OnCreate()で行ってます。
 球の発射はPlayer::OnPushX()で行ってます。

物理コンポーネント

 物理計算を実装するオブジェクトは物理コンポーネントを実装します。物理コンポーネントにはいろいろ種類がありますが、ここではボックス用のPsBoxBodyと、球体用のPsSphereBody、そしてカプセル用のPsCapsuleBodyを使用します。
 以下は、落ちてくるボックスである、ActivePsBoxクラスのOnCreate()関数です。
void ActivePsBox::OnCreate() {
    auto PtrTransform = GetComponent<Transform>();

    PtrTransform->SetScale(m_Scale);
    PtrTransform->SetQuaternion(m_Qt);
    PtrTransform->SetPosition(m_Position);

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

    auto PtrDraw = AddComponent<BcPNTStaticDraw>();
    PtrDraw->SetFogEnabled(true);
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetOwnShadowActive(true);
    PtrDraw->SetTextureResource(L"SKY_TX");

    //物理計算ボックス
    PsBoxParam param;
    //DEFAULT_CUBEのスケーリングは各辺基準なので、ハーフサイズにする
    param.m_HalfSize = m_Scale * 0.5f;
    param.m_Mass = 1.0f;
    //慣性テンソルの計算
    param.m_Inertia = BasePhysics::CalcInertiaBox(param.m_HalfSize, param.m_Mass);
    param.m_MotionType = PsMotionType::MotionTypeActive;
    param.m_Quat = m_Qt;
    param.m_Pos = m_Position;
    auto PsPtr = AddComponent<PsBoxBody>(param);
    PsPtr->SetDrawActive(true);
}
 赤くなっているところが物理計算ボックスのかかわる部分です。
 PsBoxParam構造体に各値を設定し、PsBoxBodyコンポーネントの構築用のパラメータに渡します。
    param.m_MotionType = PsMotionType::MotionTypeActive;
 というのは、アクティブということで、ほかの影響を受けます。それに対して、FixedPsBoxクラスでは
    param.m_MotionType = PsMotionType::MotionTypeFixed;
 としています。こうしておくと影響は受けません。
 また
    //慣性テンソルの計算
    param.m_Inertia = BasePhysics::CalcInertiaBox(param.m_HalfSize, param.m_Mass);
 というのは慣性テンソル(3x3行列)という角速度を求めるのに使うパラメータです。BasePhysics::CalcInertiaBox()のようにいくつかユーティリティ関数がありますのでそれを利用できます。

 ActivePsBoxでは
    PsPtr->SetDrawActive(true);
 となっています。これはワイアフレーム描画を行う設定です。つけなければワイフレームが描画されません。

 以上でActivePsBoxの記述は終了です。FixedPsBoxActivePsSphere、ActivePsCapsuleも同様の記述(コンストラクタ、デストラクタとOnCreate()関数のみ)です。

プレイヤー

 プレイヤーはもう少し複雑になります。以下はPlayer::OnCreate()関数(抜粋)です。
//初期化
void Player::OnCreate() {
    //初期位置などの設定
    auto Ptr = GetComponent<Transform>();
    Ptr->SetScale(Vec3(m_Scale));   //直径25センチの球体
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    auto bkCamera = App::GetApp()->GetScene<Scene>()->GetBackupCamera();
    Vec3 FirstPos;
    if (!bkCamera) {
        FirstPos = Vec3(0, m_Scale * 0.5f, 0);
    }
    else {
        FirstPos = App::GetApp()->GetScene<Scene>()->GetBackupPlayerPos();
    }
    Ptr->SetPosition(FirstPos);

    PsSphereParam param;
    //basecrossのスケーリングは直径基準なので、半径基準にする
    param.m_Radius = m_Scale * 0.5f;
    param.m_Mass = 1.0f;
    //慣性テンソルの計算
    param.m_Inertia = BasePhysics::CalcInertiaSphere(param.m_Radius, param.m_Mass);
    //プレイヤーなのでスリープしない
    param.m_UseSleep = false;
    param.m_MotionType = PsMotionType::MotionTypeActive;
    param.m_Quat.identity();
    param.m_Pos = FirstPos;
    param.m_LinearVelocity = Vec3(0);
    auto PsPtr = AddComponent<PsSphereBody>(param);
    PsPtr->SetAutoTransform(false);
    PsPtr->SetDrawActive(true);

//中略

}
 まず初期位置ですが、上の赤くなっているところのように、バックアップがあればそれを初期位置に設定します。
 BaseCrossではシーンステージを管理します。シーンはアプリケーション中、唯一のオブジェクトなので一種のグローバル変数はその中に記述することができます。
 サンプルのSceneクラスには、m_BackupCameraとm_BackupPlayerPosという2つの変数があり、Bボタンが押されたときにこの値を設定して、ステージの再読み込みをします。このほかにもゲーム中保存しておきたい変数などはSceneクラスに保存しておくとよいと思います。
 物理コンポーネントの設定はActivePsSphereと同様ですが、
    PsPtr->SetAutoTransform(false);
 と、自動でTransformコンポーネントを書き換える処理falseにします。物理計算では移動に応じて回転します。ここではfalseに設定します。
 実行画面をよく見るとわかりますが、プレイヤーを動かすとワイアフレームは回転しているのがわかると思います。
 つまり物理計算上は球体の移動は回転を伴うということです。しかしながら、このサンプルでは、ボールの発射も行う必要があり、その計算にプレイヤーの回転値を使用するので勝手に回転されては困るわけです。

 プレイヤーのOnUpdate()では、移動処理のみ行います。ここで、コントローラに合わせてPsSingleSphereBodyコンポーネントLinearVelocity(移動速度)を変更します。Y方向はジャンプしている可能性があるので、いじらないでおきます。
 移動は、なるべく速度を変更するかフォースを追加します。SetPosition()もありますが、オブジェクトが動いたことになってしまいますので、直接の位置設定は注意が必要です。スタート位置に戻る、などの処理が必要な場合は、後ほど説明するReset()という関数がありますのでそれを利用します。

 プレイヤーの処理では上記のようにPsSphereBodyコンポーネントの値を変更してますが、これをTransformに反映させなければ、正確な描画は行われません(SetAutoTransform(false);としているため)。ですので、OnUpdate2()で、PsSphereBodyコンポーネントの内容Transformコンポーネントに伝える処理をします。これはOnUodate()では行いません。というのは物理計算はOnUpdate()とOnUpdate2()の間に行われるからです。OnUpdate2()の時点でのPsSphereBodyコンポーネントの各値が、描画されるべき値となっているので、ここでTransformコンポーネントへの設定を行います。
void Player::OnUpdate2() {
    auto PtrPs = GetComponent<PsSphereBody>();
    auto Ptr = GetComponent<Transform>();
    Ptr->SetPosition(PtrPs->GetPosition());
    //回転の計算
    Vec3 Angle = GetMoveVector();
    if (Angle.length() > 0.0f) {
        auto UtilPtr = GetBehavior<UtilBehavior>();
        //補間処理を行わない回転。補間処理するには以下1.0を0.1などにする
        UtilPtr->RotToHead(Angle, 1.0f);
    }
//中略
}
 回転の設定はUtilBehaviorクラスのRotToHead()関数を行います。この関数は目標のAngleに補間処理しながら、Transformの回転を設定します。補間処理が必要なければ1.0を渡します。

ボールの発射

 ボールの発射はXボタンで行います。XボタンのプッシュはあらかじめInputHandler構造体によってハンドラ化されているので(詳しくはProjectBehavior.hを見てください)、Player::OnPushX()に記述します。
void Player::OnPushX() {
    auto Ptr = GetComponent<Transform>();
    Vec3 Pos = Ptr->GetPosition();
    Pos.y += 0.25f;
    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<FirePsSphere>(L"FirePsSphere", false);
    if (ShPtr) {
        ShPtr->Reset(Pos, velo);
    }
    else {
        GetStage()->AddGameObject<FirePsSphere>(Pos, velo);
    }
}
 ここでは、自分の位置と向きから、発射位置(エミッター)と発射速度を計算して、FirePsSphereクラスに設定します。もしFirePsSphereクラスのインスタンスがなければ、ゲームオブジェクトを追加して、すでにあれば、そのインスタンスを使いまわします。ですので、ゲーム上に存在するFirePsSphereクラスのインスタンスは常に一つ、ということになります。
 さて、FirePsSphereクラスの追加はわかりますが、使いまわしをする関数、Reset()は以下のような内容になります。FirePsSphere::Reset()関数です。Character.cppにあります。
void FirePsSphere::Reset(const Vec3& Emitter, const Vec3& Velocity) {
    auto PsPtr = GetComponent<PsSingleSphereBody>();
    PsSphereParam param;
    CreateDefParam(param);
    param.m_Pos = Emitter;
    param.m_LinearVelocity = Velocity;
    PsPtr->Reset(param, PsPtr->GetIndex());
}
 ここでは、構築時にも使ったPsSphereParam構造体を初期化して、コンポーネントのReset()関数を呼び出します。CreateDefParam()関数は、構築時と同じパラメータの部分をセットする関数です。
 Reset()関数呼び出しの際、第2引数に剛体のインデックスを渡します。これはコンポーネントのGetIndex()関数で取得できます。
 このようにして、同じオブジェクトを使いまわしにする場合はReset()関数を使用できるのがわかります。
 またこの関数は、スタート位置に戻るなどの場合にも使用できます。前述したようにSetPosition()を使うと途中のオブジェクトと衝突してしまいます。ですので、Reset()関数で位置を初期化することができます。

物理計算について

 このようにチュートリアル012について説明してきましたが、そのコードを見ると、ほかのサンプルより、ずいぶん単純なのがわかると思います。
 物理計算は、3D上の物体が、あたかも実在してるかのような動きを見せます。それも物理ライブラリを使うことで、かなり少ない記述で実装することが可能になります。
 それはそれでよいことなのですが、1つ落とし穴があります。物理計算はゲームのルールではないということです。ゲームのルールを考える場合、物理計算を前提としてしまうと、そこから抜け出すことができなくなってしまいます。(できちゃったような気になってしまうのです)。

 とはいえ、物理計算という表演手段はとても魅力的なものです。ぜひ使いこなしてもらえればと思います。(物理計算については章立てでサンプルも記述する予定です)