19.パターン集

1901.AbstractFactory

 この章はパターン集です。今日ではデザインパターン(いわゆるGoF)をはじめ、いろんなパターン(つまりはクラス設計などの型)が紹介されていますが、それらを、実際のアプリ(ゲームもアプリです)で使う場合、どのようにしたらいいのか、ぴんと来ないこともあるかと思います。
 そこでこの章ではBaseCross64パターンを使う場合の例を紹介したいと思います。
 とはいえ、BaseCross64は、全体の構造もパターンのかたまりのようなもので、例えばこの項で紹介する、AbstractFactoryStageクラスとその派生クラス(GameStageなど)の関係のようなものです。
 ですが、その関係をGemaObjectの派生として作成することでAbstractFactoryとは何かを確認することが可能です。

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

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

 

図1501a

 

図1501b

 

 AbstractFactoryというパターンはFactory(いわゆる工場)クラスを選択することで、別のオブジェクトの組み合わせを作成するパターンです。
 それをBaseCross64で考えた場合は、別のオブジェクトの組み合わせというのは、別のゲームステージのように考えることができます。
 ということはStageを親に持つGameStageクラス群というのも、AbstractFactoryと考えることができます。
 しかしそれではあまり面白くないので、GameStageクラスの中でGameObjectの派生クラスの形でAbstractFactoryを作成してみました。

AbstractFactoryクラス

 GameStage.h/cppにある、AbstractFactoryクラスを見てください。以下はヘッダ部です。
//--------------------------------------------------------------------------------------
//  親ファクトリー
//--------------------------------------------------------------------------------------
class AbstractFactory : public GameObject {
protected:
    //構築と破棄
    AbstractFactory(const shared_ptr<Stage>& StagePtr)
        :GameObject(StagePtr) {}
    virtual ~AbstractFactory() {}
    //ビューの作成
    void CreateViewLight();
    //プレイヤーの作成
    void CreatePlayer();
};
 このクラスはFactoryの親クラスです。この派生クラスとしてFactory1およびFactory2クラスがあります。

Factoryクラス(派生クラス)

 Factory1およびFactory2クラスは以下の様な構造です。Factory1ですが
//--------------------------------------------------------------------------------------
//  ファクトリー1
//--------------------------------------------------------------------------------------
class Factory1 : public AbstractFactory {
public:
    //構築と破棄
    Factory1(const shared_ptr<Stage>& StagePtr) :
        AbstractFactory(StagePtr)
    {}
    virtual ~Factory1() {}
    //初期化
    virtual void OnCreate() override;
};
 このようにOnCreate()関数だけを持ってます。Factory2も同様です。
 以下は、Factory1::OnCreate()の実体ですが
void Factory1::OnCreate() {
    //ビューとライトの作成
    CreateViewLight();
    //配列の初期化
    vector< vector<Vec3> > vecBox = {
        {
            Vec3(50.0f, 1.0f, 50.0f),
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(0.0f, -0.5f, 0.0f)
        },
    };
    //ボックスの作成
    for (auto v : vecBox) {
        GetStage()->AddGameObject<FixedBox>(v[0], v[1], v[2]);
    }
    //配列の初期化
    vector< vector<Vec3> > vecSp = {
        {
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(5.0f, 0.0f, 10.0f)
        },
        {
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(-5.0f, 0.0f, 10.0f)
        },
        {
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(5.0f, 0.0f, -10.0f)
        },
        {
            Vec3(0.0f, 0.0f, 0.0f),
            Vec3(-.0f, 0.0f, -10.0f)
        },
    };
    //障害物球の作成
    for (auto v : vecSp) {
        GetStage()->AddGameObject<FixedSphere>(1.0f, v[0], v[1]);
    }
    //オブジェクトのグループを作成する
    auto group = GetStage()->CreateSharedObjectGroup(L"ObjGroup");
    //配列の初期化
    vector<Vec3> vecSeek = {
        { 0, 0.125f, 10.0f },
        { 10.0f, 0.125f, 0.0f },
        { -10.0f, 0.125f, 0.0f },
        { 0, 0.125f, -10.0f },
    };
    //追いかけるオブジェクトの作成
    for (auto v : vecSeek) {
        GetStage()->AddGameObject<SeekObject>(v);
    }
    //配列の初期化
    vector<Vec3> vecPursuit = {
        { 10.0f, 0.125f, 10.0f },
    };
    //追跡するオブジェクトの作成
    for (auto v : vecPursuit) {
        GetStage()->AddGameObject<PursuitObject>(v);
    }
    //プレーヤーの作成
    CreatePlayer();
}
 CreateViewLight()とCreatePlayer()は親クラス側に実体があるので、その関数を呼び出します。あとはFactory1で実装すべきオブジェクトを追加しています。
 Factory2も似たような感じです。Factory1がイージーなステージだとすればFactory2はハードなステージという想定です。

どちらの工場を使うか

 このサンプルではFactory1とFactory2という2つのステージ設計がクラス化されているわあ家ですが、通常はイージーなステージを最初に作成して、たとえばそのステージをクリアした時に、よりハードなステージに行きます。
 とじゃいえサンプルなので、単に乱数で偶数か奇数かを取り出し、それによりFactory1かFactory2を選択します。その部分は以下です。GameStage::OnCreate()です。
//--------------------------------------------------------------------------------------
//  ゲームステージクラス実体
//--------------------------------------------------------------------------------------
void GameStage::OnCreate() {
    try {
        //2分の1の確率の乱数を発生
        if (rand() % 2) {
            AddGameObject<Factory2>();
        }
        else {
            AddGameObject<Factory1>();
        }
    }
    catch (...) {
        throw;
    }
}
 乱数により、どちらかのファクトリが選択されます。
 このように、例えば難易度によってクラスを変えるのがAbstractFactoryの特徴です。ステージナンバーなどによってパラメータの違いでステージを作成するのではなく、Factoryクラスそのものを変えるので、整理しやすくなります。
 その反面、たくさんステージがある場合(たとえば100とか)、100個のファクトリクラスを作成する形なので、これは現実的ではありません。このような場合は、ステージデータはCSVなどの形で外に出すべきでしょう。