705.物理世界を使ったステージ


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

 このサンプルは個別の機能を紹介するというより物理世界を使ったゲーム制作の具体的な手法を紹介しています。
 物理世界を使わないステージでも衝突判定はできますし、Rigudbodyコンポーネント速度を操作できます。しかし、物理世界を使わない場合衝突後の処理回転速度を管理するには、別に自作する必要があります。
 3D世界であっても、固定のカメラや、オブジェクトの動きに制限を設けたい(例えば勝手に回転されると困る場合)、あるいは3D世界を使った2Dなどの場合は物理世界を使わずに、6章までに紹介したBaseCrossの手法で記述したほうが良いでしょう。
 しかし、自由度の高い3D画面や、リアルなオブジェクトの動きを演出するためには、物理世界は威力を発揮します。

 また、このサンプルはマルチスレッド機能を実装しています。この機能により、ここではリソースの読み込みなどの時間がかかる処理中にお待ちくださいのメッセージを出力することを実装しています。
 ソリューションをリビルドして実行すると、次の画面が出てきます。

 

図0705a

 

 この画面がリソースのロード中の画面です。デバッグモードで実行すれば、比較的長い時間表示されます。リリースモードの場合は、一瞬、場合によってはほとんど表示されずに次の画面に映ります。
 リソースのロード中の画面もステージです。
 ステージの移行は以下のように記述します。ステージ操作シーンの役割です。
 まず、Scene.cppSceneの宣言に以下のようにメンバ関数を宣言します。OnCreate()関数はこれまでも出てきましたが、OnEvent()関数も作成します。
//--------------------------------------------------------------------------------------
/// ゲームシーン
//--------------------------------------------------------------------------------------
class Scene : public SceneBase {
//中略
public:
//中略
    //--------------------------------------------------------------------------------------
    /*!
    @brief 初期化
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate() override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief イベント取得
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnEvent(const shared_ptr<Event>& event) override;
};
 そうしたうえでScene.cppScene::OnCreate()関数およびScene::OnEvent()関数を記述します。
void Scene::OnCreate() {
    try {
        //自分自身にイベントを送る
        //これにより各ステージやオブジェクトがCreate時にシーンにアクセスできる
        PostEvent(0.0f, GetThis<ObjectInterface>(), GetThis<Scene>(), L"ToWaitStage");
    }
    catch (...) {
        throw;
    }
}

void Scene::OnEvent(const shared_ptr<Event>& event) {
    if (event->m_MsgStr == L"ToWaitStage") {
        //リソース読み込み用のステージ
        ResetActiveStage<WaitStage>();
    }
    else if (event->m_MsgStr == L"ToTitleStage") {
        ResetActiveStage<TitleStage>();
    }
    else if (event->m_MsgStr == L"ToGameStage") {
        ResetActiveStage<GameStage>();
    }
}
 Scene::OnEvent()というのはシーンに送られたイベントを処理する関数です。
 イベントを送るにはPostEvent()関数を使います。Scene::OnCreate()では、リソースを登録した後、自分自身にイベントを送ります。
 なぜ、Scene::OnCreate()にステージを作成する関数であるResetActiveStage()関数を呼び出さないのでしょうか。
 それはScene::OnCreate()の中の状態では、まだシーンが不完全な状態だからです。不完全な状態では、ステージからシーンの情報にアクセスすることができません。このサンプルには使ってませんが、ゲーム全体に共通するデータやパラメータはSceneに持たせることが多くなります。そうした場合、そのデータにアクセスするためにはScene::OnCreate()が終了している必要があります。
 PostEvent()関数次のターン以降OnEvent()関数を呼び出します。内部処理的には、イベントのプールにメッセージを貯めておき、指定のタイミングで指定されたオブジェクトにイベントを送ります。PostEvent()関数の第1引数の0.0fは送出までの時間です。0.0fで直近の次のターンとなり、1.0fと書けば1秒後という意味になります。

 さて、イベントの処理はScene::OnEvent()関数で行います。event->m_MsgStrにはメッセージ文字列が入ってますので、それがL"ToWaitStage"ならばリソース読み込み中の画面が表示されます。L"ToTitleStage"ならは、タイトルステージを構築します。
 またメッセージがL"ToGameStage"ならば、ゲームステージを構築します。
 リソース読み込み画面で、読み込みが終わると、自動的にL"ToTitleStage"を送出します。

リソース読み込みステージでの記述

 リソース読み込みステージWaitStageクラスです。GameStage,h/cppに記述があります。以下は宣言部です。
class WaitStage : public Stage {
    //ビューの作成
    void CreateViewLight();
    //スプライトの作成
    void CreateTitleSprite();
    //リソースロード用のスレッド(スタティック関数)
    static void LoadResourceFunc();
    //リソースを読み込んだことを知らせるフラグ(スタティック変数)
    static bool m_Loaded;
public:
    //構築と破棄
    WaitStage() :Stage() {}
    virtual ~WaitStage() {}
    //初期化
    virtual void OnCreate()override;
    //更新
    virtual void OnUpdate()override;
};
 ここに、LoadResourceFunc()関数m_Loaded変数の2つのスタティックメンバがあります。LoadResourceFunc()関数リソース読み込みを新しいスレッドで行う関数であり、m_Loaded変数読み込み終了したかどうかのフラグです。
 以下が実体です。GameStage.cppに記述があります。
bool WaitStage::m_Loaded = false;

//リソースロード用のスレッド(スタティック関数)
void WaitStage::LoadResourceFunc() {
    mutex m;
    m.lock();
    m_Loaded = false;
    m.unlock();

    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
    //各ゲームは以下のようにデータディレクトリを取得すべき
    //App::GetApp()->GetDataDirectory(DataDir);
    wstring strTexture = DataDir + L"sky.jpg";
    App::GetApp()->RegisterTexture(L"SKY_TX", strTexture);
    strTexture = DataDir + L"trace.png";
    App::GetApp()->RegisterTexture(L"TRACE_TX", strTexture);
    strTexture = DataDir + L"spark.png";
    App::GetApp()->RegisterTexture(L"SPARK_TX", strTexture);
    strTexture = DataDir + L"StageMessage.png";
    App::GetApp()->RegisterTexture(L"MESSAGE_TX", strTexture);
    //サウンド
    wstring CursorWav = DataDir + L"cursor.wav";
    App::GetApp()->RegisterWav(L"cursor", CursorWav);
    //BGM
    wstring strMusic = DataDir + L"nanika .wav";
    App::GetApp()->RegisterWav(L"Nanika", strMusic);

    m.lock();
    m_Loaded = true;
    m.unlock();

}

//中略


//初期化
void WaitStage::OnCreate() {
    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
    //お待ちくださいのテクスチャのみここで登録
    wstring strTexture = DataDir + L"wait.png";
    App::GetApp()->RegisterTexture(L"WAIT_TX", strTexture);
    //他のリソースを読み込むスレッドのスタート
    std::thread LoadThread(LoadResourceFunc);
    //終了までは待たない
    LoadThread.detach();


    CreateViewLight();
    //スプライトの作成
    CreateTitleSprite();
}

//更新
void WaitStage::OnUpdate() {
    if (m_Loaded) {
        //リソースのロードが終了したらタイトルステージに移行
        PostEvent(0.0f, GetThis<ObjectInterface>(), App::GetApp()->GetScene<Scene>(), L"ToTitleStage");
    }
}
 WaitStage::OnCreate()内で赤くなっているところは読み込み用の新しいスレッドを起動しているところです。新スレッドではWaitStage::LoadResourceFunc()を実行します。
 WaitStage::LoadResourceFunc()では、ミューテックスというオブジェクトを作成し、ロックをかけてからm_Loadedを操作します。操作後アンロックします。読み込みが終わったら、またロックをかけてm_Loadedをtrueにします。操作が終わったらアンロックします。こうすることでWaitStage::OnUpdate()でのm_Loadedのチェックを安全に行うことができます。
 ロードが終了しますとm_Loadedがtrueになりますのでタイトルステージに移行します。

 タイトルステージ内では、BボタンL"ToGameStage"イベントを送出します。

 

図0705b

 

 これがタイトルステージです。
 ここでBボタンを押すとゲームステージに移行します。

タイトルステージでの記述

 上記のタイトルステージではBでステージ切替という文字が出ます。これはテクスチャですが、そのままの意味で、コントローラのBボタンでゲームステージに切り替わります。
 Bボタンの入力を待って、ゲームステージに切り替える処理はGameStage.cppに記述がありますTitleStage::OnUpdate()関数で行います。サンプルの関係でGameStage.cppに記述がありますが、実際にはTitleStage.h/cppなどを別に作成し記述したほうがいいかと思います。
//更新
void TitleStage::OnUpdate() {
    //コントローラの取得
    auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
    if (CntlVec[0].bConnected) {
        //Bボタン
        if (CntlVec[0].wPressedButtons & XINPUT_GAMEPAD_B) {
            PostEvent(0.0f, GetThis<ObjectInterface>(), 
                App::GetApp()->GetScene<Scene>(), L"ToGameStage");
        }
    }
}
 ここで、PostEvent()関数を使用しています。送り先はApp::GetApp()->GetScene<Scene>()です。この記述でシーンのポインタ(shared_ptr)を渡すことができます。
 タイトルステージでBボタンを押すと、ステージがゲームステージに変わります。この間のフェイドアウトなどの処理は、ここでは述べませんが方法はあります。(いづれサンプル化します)

ゲームステージでの記述

 ステージが変わると以下の画面が出てきます。ゲームステージ(GameStage)です。

 

図0705c

 

 ステージに入ると上から物理オブジェクトが落下してきます。
 ここでは、プレイヤーはAボタンでジャンプし、Xボタンで砲弾を発射します。奥のほうに四角い敵がいて、砲弾を発射してきます。少し敵に近づくと、プレイヤーによってきたりします。
 BGMも流れ、砲弾を発射するとピンという発射音がします。
 プレイヤー自身も物理オブジェクトの影響を受け、敵の砲弾に当たるとエフェクトを出します。敵に砲弾が命中すると敵は消えてしまいます。プレイヤーは砲弾に当てっても無敵ですが、ゲームとして成立させるためには、プレイヤーも何らかのダメージを受ける必要があるでしょう。

 では一つ一つコードを見ていきましょう。
 まず、上から落ちてくるオブジェクト物理オブジェクトです。これらのオブジェクトはCharacter.h/cppに記述がありActivePsBox、ActivePsSphere、ActivePsCapsuleです。これらはFullSample701から703にあるオブジェクトとほとんど変わりませんが、ActivePsObjectという共通の親クラスを持ちます。またActivePsCapsuleについてはCollisionCapsuleコンポーネントと互換をとるために若干の修正を加えています。

 このサンプルのオブジェクトの特徴はCollisionコンポーネント(の派生クラス)を持ちつつ物理計算もするというものです。
 物理オブジェクトは勝手に衝突判定をしますが、同時にCollisionコンポーネント(の派生クラス)による判定も行います。ただし。Collisionコンポーネント(の派生クラス)側では判定は行いますが、衝突後の処理は行いません。
 基本的な設定としては。以下のようにOnCreate()関数に記述します。以下はActivePsSphere::OnCreate()ですが、
void ActivePsSphere::OnCreate() {
    //中略

    //衝突判定をつける
    auto PtrCol = AddComponent<CollisionSphere>();
    //衝突判定はNoneにする
    PtrCol->SetIsHitAction(IsHitAction::None);


    //中略

    //物理計算球体
    PsSphereParam param;
    //DEFAULT_SPHEREのスケーリングは直径基準なので、半径にする
    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_MotionType = PsMotionType::MotionTypeActive;
    param.m_Quat = m_Qt;
    param.m_Pos = m_Position;
    auto PsPtr = AddComponent<PsSphereBody>(param);
    PsPtr->SetDrawActive(true);
}
 赤くなっているところがポイントです。PtrCol->SetIsHitAction(IsHitAction::None);とすることで衝突後の処理を物理計算に任せることができ、なおかつ従来のBaseCrossの世界でも衝突状況を把握することが可能となります。

プレイヤーの記述

 プレイヤーはPlayer.h/cppに記述があります。ここでも上記同様Collisionコンポーネント(の派生クラス)を持ちつつ物理計算もするという処理になります。上記の物理オブジェクトと違う点はOnUpdate()OnUpdate2()の記述があるところです。
//初期化
void Player::OnCreate() {
//中略

    //衝突判定をつける
    auto PtrCol = AddComponent<CollisionSphere>();
    //判定するだけなのでアクションはNone
    PtrCol->SetIsHitAction(IsHitAction::None);


    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 = m_StartPos;
    param.m_LinearVelocity = Vec3(0);
    auto PsPtr = AddComponent<PsSphereBody>(param);
    PsPtr->SetAutoTransform(false);
    PsPtr->SetDrawActive(true);

//中略

}
 プレイヤーが砲弾を発射した時の処理は、Player::OnPushX()にあります。
void Player::OnPushX() {
    auto XAPtr = App::GetApp()->GetXAudio2Manager();
    XAPtr->Start(L"cursor");

    auto Ptr = GetComponent<Transform>();
    Vec3 Pos = Ptr->GetPosition();
    Pos.y += 0.3f;
    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 Group = GetStage()->GetSharedObjectGroup(L"ShellGroup");
    for (size_t i = 0; i < Group->size(); i++) {
        auto shptr = dynamic_pointer_cast<ShellSphere>(Group->at(i));
        if (shptr && !shptr->IsUpdateActive()) {
            //空きが見つかった
            shptr->Reset(Pos, velo);
            return;
        }
    }
    //ここまで来てれば空きがない
    GetStage()->AddGameObject<ShellSphere>(Pos, velo);
}
 この処理で砲弾の使いまわしをする記述になります。

敵の記述

 敵はCharecter.h/cppにあるBoxクラスです。これも同様にCollisionコンポーネント(の派生クラス)を持ちつつ物理計算もするという処理です。以下Box::OnCreate()関数ですが
void Box::OnCreate() {

//中略


    //衝突判定をつける
    auto PtrCol = AddComponent<CollisionObb>();
    //衝突判定はNoneにする
    PtrCol->SetIsHitAction(IsHitAction::None);

//中略

    //物理計算ボックス
    PsBoxParam param;
    //DEFAULT_CUBEのスケーリングは各辺基準なので、ハーフサイズにする
    param.m_HalfSize = Vec3(0.5f, 0.5f, 0.5f) * 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 = Qt;
    param.m_Pos = m_StartPos;
    auto PsPtr = AddComponent<PsBoxBody>(param);
    PsPtr->SetDrawActive(true);

    //ステートマシンの構築
    m_StateMachine.reset(new StateMachine<Box>(GetThis<Box>()));
    //最初のステートをSeekFarStateに設定
    m_StateMachine->ChangeState(BoxDefaultState::Instance());

}
 このほかに敵の処理はAI処理が加わります。これはステートマシンで実装しています。Box::OnUpdate()関数からの処理の流れを確認ください。

砲弾(発射する球体)の記述

 このサンプルで、唯一物理世界とは関係のないオブジェクトが砲弾ですCharacter.h/cppにあるShellSphereクラスです。
 以下ShellSphere::OnCreate()関数ですが、RigidbodyとCollisionSphereを使用しています。つまり、物理計算コンポーネントを使う代わりにRigidbodyコンポーネントを使用しています。CollisionSphereはこれまで同様PtrCol->SetIsHitAction(IsHitAction::None);とします。
void ShellSphere::OnCreate() {
//中略

    //Rigidbodyをつける
    auto PtrRedid = AddComponent<Rigidbody>();
    PtrRedid->SetVelocity(m_Velocity);
    //衝突判定をつける
    auto PtrCol = AddComponent<CollisionSphere>();
    PtrCol->SetIsHitAction(IsHitAction::None);


//中略

}
 このようにしておいて、ShellSphere::OnUpdate2()で以下のように記述があります。
void ShellSphere::OnUpdate2() {
    auto PtrTransform = GetComponent<Transform>();
    if (PtrTransform->GetPosition().y < -0.5f) {
        Erase();
        return;
    }
    auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark", false);
    if (GetComponent<Collision>()->GetHitObjectVec().size() > 0) {
        for (auto& v : GetComponent<Collision>()->GetHitObjectVec()) {
            auto& ptr = dynamic_pointer_cast<Box>(v);
            auto& ptr2 = dynamic_pointer_cast<Player>(v);
            auto& ptr3 = dynamic_pointer_cast<ActivePsObject>(v);
            if (ptr || ptr2) {
                if (ptr) {
                    GetStage()->RemoveGameObject<Box>(ptr);
                }
                else {
                    //ここにプレイヤーのダメージを記述

                }
                //スパークの放出
                if (PtrSpark) {
                    PtrSpark->InsertSpark(GetComponent<Transform>()->GetPosition());
                }
            }
            else if (ptr3) {
                //スパークの放出
                if (PtrSpark) {
                    PtrSpark->InsertSpark(GetComponent<Transform>()->GetPosition());
                }
                Erase();
            }
        }
    }
}
 ここでは衝突した相手によって処理が変わります。なら、エフェクトを出して敵を消します。物理オブジェクトの場合はエフェクトのみです。プレイヤーの場合もエフェクトのみですが、ゲームとして成立させるためには//ここにプレイヤーのダメージを記述の部分に、プレイヤーのダメージを記述する必要があるでしょう。
 またこの処理はOnCollision()関数を多重定義することでも記述可能かと思います。

 以上、割合と簡単な説明ですが、このサンプルにはBaseCrossが目指すものが凝縮されています。
 3Dゲームを作成する上での基礎的な最低限の機能が一通り入っています。皆さんの参考になれば幸いです。