021.ゲーム実装の雛形(Dx11版)

 このサンプルはSimplSample021というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルはDx11版しかありません。Dx12版はありませんのでご了承ください。
 また、このサンプルはこれ以降のサンプルの、ひな形的な役割を担ってます。
 あるいは実際のゲーム制作の、ひな形としても利用できると思います(ただし、これ以降のサンプルと同じ設計で作成する場合)。設計的には、フルバージョンの簡略化したものです。もし、このような設計が気に入らなければ、SimpleSample015(あるいはそれ以前のサンプル)をひな形として、自分の設計で制作することをお勧めします。
 例えば画面のフェードイン。フェードアウトを実装する場合は、このサンプルをひな型にしてもよいですが、画面のオーバーラップを実装するには不向きです。そういう場合はSimpleSample015(あるいはそれ以前のサンプル)をひな形にするのをお勧めします。また今流行りのオープンワールドにも不向きでしょう。オープンワールドの実装にはゲームを遊んでいる最中の、リソース読み込みや破棄が必要であり、そのためにはマルチスレッド処理やテッセレーションも必要になるでしょう。

 また、これ以降SimplSample023までは、個別の機能を紹介するサンプルとなります。いろんな機能を同時に実装するにはSimplSample024を、ひな形として利用するとよいでしょう。

 サンプルを起動すると以下の画面が現れます。コントローラで操作でき、Aボタンでジャンプします。Bボタンで、いわゆるステージの切り替えができます。

 

図0021a

 

【サンプルのポイント】

 見た感じ、非常にシンプルですが、このサンプルにはいくつか新しい機能が実装されています。以下に列挙します。
1、ステージの切り替え
2、テクスチャのリソース登録
3、3Dの描画オブジェクト
4、親子関係のスプライト
 これらについて、詳しく説明します。

■1、ステージの切り替え■

 ゲームにはいろんな画面があります。ゲーム画面メインメニューリザルト画面などです。これらをBaceCrossではステージと称してます。
 ステージの実装方法はいろいろありますが、このサンプル(や今後のサンプル)ではシーンが動的に切り変える画面として実装しています。フルバージョンと同様です。つまりかなり縮小したフルバージョンの設計をしています。
 ステージシーンが管理します。シーンアクティブステージというのを一つだけ持っていて、それを動的に変更します。変更する際に今まで使っていたステージは破棄します。しかし、一度読み込んだリソースをステージの破棄のたびに一緒に破棄していたのでは、読み込む時間がもったいないのでリソース登録することにより、リソースの再読み込みをしなくても済むようになってます。(リソース登録については、3、テクスチャのリソース登録で説明します)。

ステージ切り替えの実装

 まず、シーンOnCreate()関数を見てください。
void Scene::OnCreate() {
    CreateResources();
    //自分自身にイベントを送る
    //これにより各ステージやオブジェクトがCreate時にシーンにアクセスできる
    PostEvent(0.0f, GetThis<ObjectInterface>(), GetThis<Scene>(), L"ToGameStage");
}
 赤くなっているところでイベントを自分自身に送ってます。PostEvent()関数は、最速で次のターンの最初に実装されるイベントです。ここではL"ToGameStage"というメッセージが送られます。
 送られたイベントはScene::OnEvent()関数で処理されます。ここでは以下のように記述があります。
void Scene::OnEvent(const shared_ptr<Event>& event) {
    if (event->m_MsgStr == L"ToGameStage") {
        //アクティブステージをGameStageに設定
        ResetActiveStage<GameStage>();
    }
    else if (event->m_MsgStr == L"ToEmptyStage") {
        //アクティブステージをEmptyStageに設定
        ResetActiveStage<EmptyStage>();
    }
}
 "ToGameStage"という文字列が送られた場合はGameStageがアクティブステージになります。ResetActiveStage()関数は、指定されたクラスのインスタンスをnewして、アクティブステージに設定します。その際、これまでのステージはスマートポインタにより、自動的に破棄されます。
 ステージがnewされたときに呼ばれる関数が、コンストラクタと、OnCreate()関数です。GameStage::OnCreate()関数は以下の通りです。
void GameStage::OnCreate() {

    //平面の作成
    Quat Qt;
    Qt.rotationX(XM_PIDIV2);
    AddGameObject<SquareObject>(
        L"SKY_TX",
        Vec3(50.0f, 50.0f, 1.0f),
        Qt,
        Vec3(0.0f, 0.0f, 0.0f)
        );

    //プレイヤーの作成
    AddGameObject<Player>(
        L"TRACE_TX", 
        true, 
        Vec3(0.0f, 0.125f, 0.0f)
        );

    //PNT描画オブジェクトの作成
    AddGameObject<PNTDrawObject>();

//中略

}
 このように、以前のサンプル(SimplaSample015)ではScene::OnCreate()関数に記述されていた内容が変更されてます。フルバージョンのように
    AddGameObjectテンプレート関数
 が実装されています。Stageクラスにあるこの関数は、フルバージョンの者とほぼ同じです。
 (SimplaSample015)のように、ゲームステージに各インスタンスのポインタを持つことも可能ですが、配列で持たせることにより、各オブジェクトの仮想関数呼び出しが効率化されています。
 しかし、Stageクラスにあるこの配列はGameObjectの配列です。各派生クラスの仮想関数呼び出しは、直接呼び出すよりは負担はかかります。これが我慢できない場合は、EmptyStageのように、各々メンバとして持つといいでしょう。
 さて、中身を見てみましょう。テクスチャもファイル名を渡すのではなくリソース名を渡しています。これは、テクスチャを使いまわしができるようにリソース登録されているためです。
 ゲームオブジェクトの配列化により、GameStage::OnUpdateStage()も以下のようにシンプルになります。
void GameStage::OnUpdateStage() {
    for (auto& v : GetGameObjectVec()) {
        //各オブジェクトの更新
        v->OnUpdate();
    }
    //自分自身の更新
    this->OnUpdate();
}
 GameStage::OnDrawStage()も同様です。
void GameStage::OnDrawStage() {
    //描画デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    Dev->ClearDefaultViews(Col4(0, 0, 0, 1.0f));
    //デフォルト描画の開始
    Dev->StartDefaultDraw();
    for (auto& v : GetGameObjectVec()) {
        //各オブジェクトの描画
        v->OnDraw();
    }
    //自分自身の描画
    this->OnDraw();
    //デフォルト描画の終了
    Dev->EndDefaultDraw();
}


 さて、このようにしてゲームステージは表示されるわけですが、もう一つのEmptyStageはどのように切り替えられるのでしょうか。答えはGameStage::OnUpdate()関数にあります。
 この関数ではカメラの移動を行っていますが、その際コントローラのBボタンが押されたらという処理が追加されています。
void GameStage::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"ToEmptyStage");
        }
    }
}
 このように、L"ToEmptyStage"というメッセージをシーンに送ってます。シーンではOnEvent()関数で、ゲームステージのアクティブ化が行われますので、ステージの切り替えが実行されます。

■2、テクスチャのリソース登録■

 ステージ切り替えのところでも少し触れましたが、このサンプルではテクスチャのリソース登録することで、同じテクスチャを、2つ以上メモリ上に配置しなくても済むような作りになってます。(同じデータを複数メモリ上に置くのは無駄ですからね)。
 テクスチャのリソース登録Appクラスにリソースを登録することで実装します。Scene::CreateResources()関数で行っています。
void Scene::CreateResources() {
    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"StageMessage.png";
    App::GetApp()->RegisterTexture(L"MESSAGE_TX", strTexture);
}
 赤くなっているところは、テクスチャリソースを作成してリソース登録するという処理です。テクスチャファイル名とリソース名を渡すとリソース登録してくれます。
 一度リソース登録したデータは、このゲームが終了するまでメモリに保持されます。
 もし、ゲーム途中で、もう使用しなくなったリソースがあれば、App::GetApp()->UnRegisterResource()関数で解放できます。しかしこの関数は存在チェックをしないので、失敗したら例外が発生するので、あらかじめApp::GetApp()->CheckResource()関数で存在をチェックするとよいでしょう。

 リソース登録メッシュWAVファイルもすることが可能です。このサンプルで使用するメッシュは、動的に頂点を変更するのが多いのでリソース登録してませんが、固定メッシュの場合はリソースにしておいたほうがいいでしょう。またWAVはデータが大きくなる傾向にあるので、リソース登録は必須と言えます。(WAVについてのサンプルは後ほど紹介します)。

■3、3Dの描画オブジェクト■

 このサンプルには、SimpleSample015同様描画オブジェクトが実装されています。DrawObjects.h/cppにあります。同じ描画方法をとるオブジェクトに対して、一度で描画をするオブジェクトですが、少し改良を加えています。
 3Dの処理の中で透明処理があります。透明なもの不透明なものの後に描画する必要があります。また、透明処理をするオブジェクト同士はカメラより遠いものから順番に描画すると、透明なのに奥が描画されない、などのエラーが出にくくなります。
 このサンプルの描画オブジェクトである、PNTDrawObjectクラス透明なものと不透明なものを別に管理しています。
 また、透明なもの同士は描画直前にカメラ位置から遠いもの順にソートします。以下がその部分です。
void PNTDrawObject::OnDraw() {
//中略
    //サブ関数呼び出し(不透明)
    OnDrawSub(m_DrawObjectVec,sb);
    //--------------------------------------------------------
    //透明の3Dオブジェクトをカメラからの距離でソート
    //以下は、オブジェクトを引数に取りboolを返すラムダ式
    //--------------------------------------------------------
    auto func = [&](shared_ptr<DrawObject>& Left, shared_ptr<DrawObject>& Right)->bool {
        auto LeftPos = Left->m_WorldMatrix.transInMatrix();
        auto RightPos = Right->m_WorldMatrix.transInMatrix();
        auto LeftLen = bsm::length(LeftPos - CameraEye);
        auto RightLen = bsm::length(RightPos - CameraEye);
        return (LeftLen > RightLen);
    };
    //ラムダ式を使ってソート
    std::sort(m_TraceDrawObjectVec.begin(), m_TraceDrawObjectVec.end(), func);
    //サブ関数呼び出し(透明)
    OnDrawSub(m_TraceDrawObjectVec, sb);
    //後始末
    Dev->InitializeStates();
    //描画用の配列をクリア
    m_DrawObjectVec.clear();
    m_TraceDrawObjectVec.clear();
}
 赤くなっているところが透明のものをソートしている個所です。ラムダ式なので
//ラムダ式を使ってソート
std::sort(m_TraceDrawObjectVec.begin(), m_TraceDrawObjectVec.end(), 
    [&](shared_ptr<DrawObject>& Left, shared_ptr<DrawObject>& Right)->bool {
    auto LeftPos = Left->m_WorldMatrix.transInMatrix();
    auto RightPos = Right->m_WorldMatrix.transInMatrix();
    auto LeftLen = bsm::length(LeftPos - CameraEye);
    auto RightLen = bsm::length(RightPos - CameraEye);
    return (LeftLen > RightLen);
});
 のように書けるわけですが、ラムダ式を一気に記述すると、訳が分からなくなるので、ラムダ式を分離しています。
 ラムダ式の理解には分離した状態で記述するとわかりやすいと思います。

■4、親子関係のスプライト■

 最後にスプライトの説明です。BaseCrossでは2Dの単一テクスチャオブジェクトスプライトと称しています。コ ード説明に入る前に、スプライトにはどういう機能があると便利かを記します。
1、PCT頂点を作成する機能(共通)
2、描画機能(共通)
3、頂点を変更する機能(個別)
4、テクスチャを動的に変更できる機能(個別)
5、エミッシブ色を変更できる機能(個別)
6、ブレンドステートを変更できる機能(個別)
 共通となっているのは、どのスプライトでも同じように記述できる部分で、個別となっているのは、スプライトの種類(クラス)やインスタンスごとに違う処理ができるの便利な部分です。
 こういった要件を踏まえて作成したのがSpriteBaseクラスとその派生クラスRotateSpriteとMessageSpriteです。
 共通の処理はSpriteBaseクラスで行い、ここでは個別処理用にインターフェイス(操作関数)を用意します。それで、各派生クラスでは、親クラスのインターフェイスを使って個別の処理をします。

1、PCT頂点を作成する機能(共通)と2.描画機能(共通)

 この機能は、親クラスのSpriteBase::OnCr、ate()関数で実装されてます。
void SpriteBase::OnCreate() {
    float HelfSize = 0.5f;
    //頂点配列(縦横指定数ずつ表示)
    m_BackupVertices = {
        { VertexPositionColorTexture(Vec3(-HelfSize, HelfSize, 0),
            Col4(1.0f,1.0f,1.0f,1.0f), Vec2(0.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(HelfSize, HelfSize, 0), 
            Col4(1.0f,1.0f,1.0f,1.0f), Vec2((float)m_XWrap, 0.0f)) },
        { VertexPositionColorTexture(Vec3(-HelfSize, -HelfSize, 0), 
            Col4(1.0f,1.0f,1.0f,1.0f), Vec2(0.0f, (float)m_YWrap)) },
        { VertexPositionColorTexture(Vec3(HelfSize, -HelfSize, 0), 
            Col4(1.0f,1.0f,1.0f,1.0f), Vec2((float)m_XWrap, (float)m_YWrap)) },
    };
    //頂点の初期修正(仮想関数呼びだし)
    AdjustVertex();
    //インデックス配列
    vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
    //メッシュの作成(変更できる)
    m_SquareMesh 
        = MeshResource::CreateMeshResource(m_BackupVertices, indices, true);
}
 このように最初にm_BackupVerticesを初期化し、赤くなっていおるところのようにAdjustVertex()を呼び出します。この関数は仮想関数になっていて、派生クラスで多重定義する音で、m_BackupVerticesの初期値を変更することができます。
 RotateSpriteクラスでは多重定義内で変更しています。
void RotateSprite::AdjustVertex() {
    //頂点色を変更する
    for (size_t i = 0; i < m_BackupVertices.size();i++) {
        switch (i) {
        case 0:
            m_BackupVertices[i].color = Col4(1.0f, 0.0f, 0.0f, 1.0f);
            break;
        case 1:
            m_BackupVertices[i].color = Col4(0.0f, 1.0f, 0.0f, 1.0f);
            break;
        case 2:
            m_BackupVertices[i].color = Col4(0.0f, 0.0f, 1.0f, 1.0f);
            break;
        case 3:
            m_BackupVertices[i].color = Col4(1.0f, 1.0f, 0, 1.0);
            break;
        }
    }
}

3、頂点を変更する機能(個別)

 これも、修正するタイミングで呼ばれる仮想関数が用意されています。UpdateVertex()関数です。
 RotateSpriteクラスでは多重定義して、UV値を変更して、テクスチャのスクロール処理を実装しています。
 MessageSpriteクラスでは多重定義して、点滅処理を実装しています。この仮想関数は、親クラスにおいて頂点バッファをあらかじめマップしておいて、そのデータのポインタを渡します。ですから、派生クラスではマップのことは考えずに修正処理のみ行うことができます。以下は、MessageSpriteでの更新処理です。
void  MessageSprite::UpdateVertex(float ElapsedTime, 
        VertexPositionColorTexture* vertices) {
    m_TotalTime += (ElapsedTime * 5.0f);
    if (m_TotalTime >= XM_2PI) {
        m_TotalTime = 0;
    }
    float sin_val = sin(m_TotalTime) * 0.5f + 0.5f;
    Col4 UpdateCol(1.0f, 1.0f, 1.0f, sin_val);
    for (size_t i = 0; i < m_SquareMesh->GetNumVertices(); i++) {
        vertices[i] = VertexPositionColorTexture(
            m_BackupVertices[i].position,
            UpdateCol,
            m_BackupVertices[i].textureCoordinate
        );

    }
}

その他の処理

 4、テクスチャを動的に変更できる機能(個別)、5、エミッシブ色を変更できる機能(個別)、6、ブレンドステートを変更できる機能(個別)については親クラスにアクセサが用意されてますので、それを利用します。アクセサは公開メンバですので外部からも利用できます。

■まとめ■

 今回は、シンプルバージョンという低レベルの状態から、どのようにしてゲームに仕上げていくか、そのヒントになる実装を紹介しました。
 また、今後、SImpleSample023までのサンプルの紹介では、個別に紹介することで、ほかの実装とは分けて考えることが可能にしてあります。
 それらは、衝突判定の実装、影の描画です。ほかの機能もそれらを参考に実装可能と思います。
 また、それらやほかの機能がミックスされた、汎用的なサンプルSImpleSample024に紹介します。それではモデルの描画や、リアルなシェーダも含まれます。