202.ステージの構造

 ここではステージの親クラスであるStageクラスの構造と動作について説明します。

Stageクラス

 Stageクラスは以下のような親クラスを持ちます。

 

図0202a

 

 ObjectInterfaceOnCreate()関数、 OnPreCreate()関数、OnEvent()関数を持つ派生クラスを作成するためのインターフェイスです。このほかにGetThis()テンプレート関数により、自分自身のshared_ptrを返すことができます。
 ShapeInterfaceOnUpdate()関数、OnLastUpdate()関数、OnDraw()関数を持つ派生クラスを作成するためのインターフェイスです。
 Stageクラスは、これら両方親クラス(多重継承)するクラスとなります。
 なぜ、ObjectInterfaceShapeInterfaceの2つのインターフェイスがあるかというと、オブジェクトによってはOnUpdate()関数などがいらないクラスもあるからです。例えばGetThis()は実装したいけれど、OnUpdate()などはいらないクラスを作成する場合はObjectInterfaceだけを継承すればいいということです。ShapeInterfaceだけを継承することも可能ですが、あまりメリットはありません(その場合は、親クラス無しで作成したほうがいいと思われます)。

 Stageクラスは、ゲームステージメニューステージの親クラスです。ゲームオブジェクトを複数セットすることができ、更新や描画のタイミングゲームオブジェクトや、そのオブジェクトが持つコンポーネントに伝え(仮想関数を呼び出す)ます。

更新処理

 Stageクラスは、1ターンの間に様々な処理をこなします。以下は、ステージのアップデート処理を行うStage::UpdateStage()関数です。
//ステージ内の更新(シーンからよばれる)
void Stage::UpdateStage() {
    //追加・削除まちオブジェクトの追加と削除
    SetWaitToObjectVec();
    //Transformコンポーネントの値をバックアップにコピー
    for (auto ptr : GetGameObjectVec()) {
        if (ptr->IsUpdateActive()) {
            auto ptr2 = ptr->GetComponent<Transform>();
            ptr2->SetToBefore();
            auto RigidPtr = ptr->GetComponent<Rigidbody>(false);
            if (RigidPtr) {
                //Rigidbodyがあればフォースを初期化
                RigidPtr->SetForce(0, 0, 0);
            }
        }
    }

    //配置オブジェクトの更新処理
    for (auto ptr : GetGameObjectVec()) {
        if (ptr->IsUpdateActive()) {
            ptr->OnUpdate();
        }
    }
    //自身の更新処理
    if (IsUpdateActive()) {
        OnUpdate();
    }

    //配置オブジェクトのコンポーネント更新
    for (auto ptr : GetGameObjectVec()) {
        if (ptr->IsUpdateActive()) {
            ptr->ComponentUpdate();
        }
    }
    //衝突判定の更新(ステージから呼ぶ)
    UpdateCollision();
    //衝突判定のメッセージ発行(ステージから呼ぶ)
    UpdateMessageCollision();
    //配置オブジェクトの更新後処理
    for (auto ptr : GetGameObjectVec()) {
        if (ptr->IsUpdateActive()) {
            ptr->OnUpdate2();
        }
    }
    //自身の更新1
    if (IsUpdateActive()) {
        OnUpdate2();
    }
    //自身のビューをアップデート
    auto ViewPtr = GetView(false);
    if (ViewPtr && ViewPtr->IsUpdateActive()) {
        ViewPtr->OnUpdate();
    }

    //コリジョンのリセット
    for (auto ptr : GetGameObjectVec()) {
        ptr->CollisionReset();
    }
    //子供ステージの更新
    for (auto PtrChileStage : GetChileStageVec()) {
        PtrChileStage->UpdateStage();
    }
}
 このようにいくつもの処理を1ターンの間に行っています。それなりに理由があっての処理なのですが、ひょっとするとこれだけの処理を必要としないかもしれません。
 そんな場合は、Stage::UpdateStage()関数は仮想関数なので、派生クラスで多重定義することが可能です。

ゲームオブジェクトの追加と削除

 ゲームオブジェクトは、通常はGameObjectの派生クラスとしてクラスを定義し、そのクラスをAddGameObject<T>()テンプレート関数によって追加します。GamneObjectそのものを追加することもできますが、その場合OnUpdate処理などを定義することができませんので注意しましょう。
 この関数は
template<typename T, typename... Ts>
shared_ptr<T> AddGameObject(Ts&&... params) {
    try {
        auto Ptr = ObjectFactory::Create<T>(GetThis<Stage>(), params...);
        PushBackGameObject(Ptr);
        return Ptr;
    }
    catch (...) {
        throw;
    }
}
 のようになっています。これはObjectFactory::Create()関数により指定の型のゲームオブジェクトを作成し、
        PushBackGameObject(Ptr);
 で、追加待ち配列に追加されます。なぜ、直接ゲームオブジェクトの配列に追加しないかというと、例えばターン処理の間に、あるオブジェクトの更新処理で、別のオブジェクトが追加されると、ゲームオブジェクトの配列に変化が生じ、その後の更新処理がうまくいかないからです。
 これで追加されたオブジェクトは、毎ターンの最初の
    SetWaitToObjectVec();
 により処理されます。
 また、動的な削除にも対応しています。
    Stage::RemoveGameObject<ゲームオブジェクト型>(ゲームオブジェクトポインタ)
 を使うことにより、指定した型の指定したオブジェクトを削除できます。実際の削除もSetWaitToObjectVec()で処理されます。
 しかし、削除するオブジェクトのポインタをほかで保持していた場合削除の動きは不定になってしまうので注意しましょう。
 そういった削除エラー(ヌルポインタエラー)や、メモリリークを防ぐためには以下の処理を徹底することです。
1、基本的に別のゲームオブジェクトのshared_ptrを保持しない。
2、保持せずにSetSharedGameObject()関数、GetSharedGameObject()関数で使うときに取得する。
3、どうしても保持したければweak_ptrで保持し、使用の都度lock()で有効かどうかチェックする。
 このような処理を徹底すれば、動的な削除によるエラーは回避できます。
 ただ削除処理は、内部配列の順番を詰める動きになります。敵を倒したときとか不定期に、あるいは、たまに起きる場合は負荷も少ないですが、毎ターン追加と削除が繰り返されるのような処理は望ましくありません。
 そういった場合は削除はせずにSetUpdateActive()関数、SetDrawActive()関数を使って有効無効の切り替えで処理したほうが効率は格段に良くなります。
 AddGameObject()関数の類似系としてAddGameObjectWithParam()関数があります。
 AddGameObject()関数のように使用しますが、違いはパラメータがコンストラクタ引数にならないということです。
 例えば、TestObjectというクラスを、GameObjectの派生クラスとして作成したとします。するとヘッダは以下のようになります。
class TestObject : public GameObject {
public:
    //構築と破棄
    TestObject(const shared_ptr<Stage>& StagePtr);
    virtual ~TestObject(){}
    //初期化
    //OnCreate()関数は何もしない
    virtual void OnCreate() override{}
    //WithParamによる初期化(仮想関数でなくてよい)
    void OnCreateWithParam(const Vec3& Pos);
    //中略
};
 このようにconst shared_ptr<Stage>& StagePtrだけを引数に持つコンストラクタと、何らかのパラメータを持つOnCreateWithParam()関数を作成します。
 そして実体は
//コンストラクタ
TestObject::TestObject(const shared_ptr<Stage>& StagePtr):
GameObject(StagePtr)
{}

//WithParamによる初期化(仮想関数でなくてよい)
void TestObject::OnCreateWithParam(const Vec3& Pos){
    auto PtrTransform = AddComponent<Transform>();
    PtrTransform->SetPosition(Pos);
    PtrTransform->SetScale(1.0f, 1.0f, 1.0f);
    PtrTransform->SetRotation(0.0f, 0.0f, 0.0f);
}
 のように書きます。
 そして、ステージに配置する際は、GameStage::OnCreate()関数など(あるいはそこから呼ばれる関数)
AddGameObjectWithParam<TestObject>(Vec3(0.0f, 0.5f, 5.0f));
 と書けば、パラメータがTestObject::OnCreateWithParam()関数に渡されます。
 この記述方法のメリットは初期位置や初期設定をメンバ変数などに持たなくてもよいということです。
 コンストラクタの引数で渡した場合、コンストラクタ内ではコンポーネントの作成などはできないので、OnCreate()関数が呼ばれるまで、メンバ変数にその値を保持しなければなりません。
 そのメンバ変数はコンストラクタとOnCreate()の間に橋渡し的にしか使用されませんので、無駄と言えば無駄な変数です。
 ですので上記のような方法で初期化すれば、直接OnCreateWithParam()関数に渡され、その時点では、コンポーネントにアクセスできますので、無駄なメンバ変数はいらないことになります。
 しかし何かあったら初期位置に戻るなどの処理をする場合、初期位置をどこかに取っておく必要がありますので、普通にコンストラクタに引数を渡したほうが処理しやすくなります。
 どのような動きをするオブジェクトなのか、をよく考えて構築するといいでしょう。

共有オブジェクト

 AddBegeObject関数などで追加されたゲームオブジェクト(GameObjectの派生クラス)は、内部的にはshared_ptrの配列に保存されます。
 その配列はGetGameObjectVec()関数で取得できますが、この配列はすべてのオブジェクトの配列なので、その中から特定のオブジェクトが必要な場合、すべてをスキャンするのは非効率的です。
 そのため、オブジェクト追加時などに共有オブジェクトとして登録しておくと、便利です。
 SetSharedGameObject()関数GetSharedGameObject()テンプレート関数は、共有オブジェクトを操作する関数です。
 まず共有オブジェクトへの登録ですが、プレイヤーを共有化したい場合、プレイヤーの追加時に以下のように記述します。
//プレーヤーの作成
auto PlayerPtr = AddGameObject<Player>();
//シェア配列にプレイヤーを追加
SetSharedGameObject(L"Player", PlayerPtr);
 この処理でL"Player"というキーワードでプレイヤーを取得できるようになります。以下のように取得します。
auto PtrPlayer = GetStage()->GetSharedGameObject<Player>(L"Player", false);
if (PtrPlayer) {
    //見つかった場合の何かの処理
}
else{
    //見つからなかった場合の何かの処理
}
 この場合、プレイヤーが見つからない場合、PtrPlayerにはnullptrが入ります。見つかった場合見つからなかった場合と処理を分ける場合に有効です。
auto PtrPlayer = GetStage()->GetSharedGameObject<Player>(L"Player");
auto PtrTrans = PtrPlayer->GetComponent<Transform>();
 このようにGetSharedGameObject()にパラメータ無し(もしくはtrue)で呼び出すと、見つからなかった場合例外が発生します。必ず必要な場合はこの処理を行います。例外が発生するというのはゲームプログラミングではバグと考えて差し支えないと思いますので、原因を調べます。

 プレイヤーのように1つのオブジェクトならこの処理でいいのですが、例えば10匹の敵キャラを参照したい場合があります。そんな場合は共有オブジェクトグループを作成します。
    //Enemyのグループを作成する
    CreateSharedObjectGroup(L"EnemyGroup");
 そのようにしておいて、例えばオブジェクト追加時に
    auto EnemyPtr = AddGameObject<Enemy>();
    auto Group = GetSharedObjectGroup(L"EnemyGroup");
    Group->IntoGroup(EnemyPtr);
 のようにグループに追加します。そして参照する場合は、プレイヤーから参照したとすると
    auto Group = GetStage()->GetSharedObjectGroup(L"EnemyGroup");
    for (auto& v : Group->GetGroupVector()) {
        auto shptr = v.lock();
        if (shptr) {
            auto EnemyPtr = dynamic_pointer_cast<Enemy>(shptr);
            //EnemyPtrを使った何かの処理
        }
    }
 のように処理します。共有グループweak_ptrの配列です。ですから、lock()をかけて有効かどうかをチェックし、有効だった場合dynamic_pointer_cast目的の型のキャストします。それが成功したら、Enemyが特定できたことになります。
 このようなメカニズムなので同じ共有グループに追加するのは同じ型でなくてもよいことになります。例えば、ある広いステージで、特定のセルマップに配置されているオブジェクトを型は関係なくグループ化しておくことも可能です。

描画処理

 描画処理は以下のようになっています。
//ステージ内のすべての描画(シーンからよばれる)
void Stage::RenderStage() {
    //描画デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    auto MultiPtr = dynamic_pointer_cast<MultiView>(GetView());
    if (MultiPtr) {
        for (size_t i = 0; i < MultiPtr->GetViewSize(); i++) {
            MultiPtr->SetTargetIndex(i);
            if (IsShadowmapDraw()) {
                Dev->ClearShadowmapViews();
                Dev->StartShadowmapDraw();
                DrawShadowmapStage();
                Dev->EndShadowmapDraw();
            }
            //デフォルト描画の開始
            Dev->StartDefaultDraw();
            RsSetViewport(MultiPtr->GetTargetViewport());
            DrawStage();
            //デフォルト描画の終了
            Dev->EndDefaultDraw();
        }
        //描画が終わったら更新処理用に先頭のカメラにターゲットを設定する
        MultiPtr->SetTargetIndex(0);
    }
    else {
        if (IsShadowmapDraw()) {
            Dev->ClearShadowmapViews();
            Dev->StartShadowmapDraw();
            DrawShadowmapStage();
            Dev->EndShadowmapDraw();
        }
        //デフォルト描画の開始
        Dev->StartDefaultDraw();
        RsSetViewport(GetView()->GetTargetViewport());
        DrawStage();
        //デフォルト描画の終了
        Dev->EndDefaultDraw();
    }
    //子供ステージの描画
    for (auto PtrChileStage : pImpl->m_ChildStageVec) {
        PtrChileStage->RenderStage();
    }
}
 このようにビューシングルかマルチかによって処理を変えています。RenderStage()関数も仮想関数になっているので派生クラスで多重定義可能です。
 この関数は、内容的にはビューの振り分けと、描画処理の呼び出しを行っています。実際のオブジェクト単位の描画命令はDrawShadowmapStage();およびDrawStage();で行っています。これらも仮想関数です。
 DrawShadowmapStage()関数、DrawStage()関数も仮想関数ですので、必要があれば多重定義してください。

そのほかの処理

 Stageクラスではこのほかにパーティクル描画処理ビューの処理などがあります。親子関係も作成できます。それらについてはライブラリコードもしくはサンプルを参照ください。