110.剣で戦う

 このサンプルはFullTutorial010というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 リビルドして実行すると以下の画面が出てきます。リリースモードでのビルトをお勧めします。
 このサンプルにはいくつかのテーマがあります。以下に紹介します。
1、テンプレートGameObject
2、行動の使いまわしによるできるだけ少ないコード
3、セルマップによるAStar検索
4、細かなステートの変化
5、剣での戦い
 今項のタイトルにもなっている「剣での戦い」が最後に来ています。実は、このサンプルは「アルゴリズム研究」のサンプルです。
 「剣の振り方」などの「戦い」に特化したものではないとまずはお考えください。

 ゲームに限らず何かのプログラムを作成する上で、「コードの最小化」は頭のどこかに持っておきたい概念です。
 例えば、複数の敵が同じような攻撃をしてくる場合、その攻撃行動を敵ごとに記述するのはナンセンスですし、はっきり言ってメモリの無駄です。できるだけ同じようなコードは2回以上は書かずに済ませたいものです。
 それは必ずしも「速く動作する」に直結するものではありませんが、多くの場合「動作速度が速く」なります。この矛盾した説明をどう説明するか迷うことですが、コードを少なく記述する、ことは「最適化」を常に考えなければなりません。
 最適化は多くの場合「省力化」を生むことになり、結果として「動作速度が速く」なります。

 このサンプルをビルドして実行すると以下の画面が現れます。

 

図0110a

 

 「Aボタン」で、プレイヤーはジャンプします「Xボタン」で剣にのようなオブジェクトを振り、敵を攻撃します。
 敵は、プレイヤーが敵のテリトリーに入ると攻撃してきます。テリトリーを抜けると「元の場所」に帰ります。
 剣が相手に当たれば、相手ががノックバックします。プレイヤーも同様です。プレイヤー、敵双方ともHP(ライフゲージ)を持ってます。HPが0になると、負けです。倒れこんで黒焦げになります。しかし、これはサンプルですので、一定時間たつと復活します。

テンプレートGameObject

 これまでのサンプルでさんざん紹介してきた「GameObjectの派生クラス」ですが、じつはこのクラスは「テンプレートクラス」として作成可能です。
 このサンプルの「HPクラス(のマネージャクラス)」である「HPManegerクラス」は「Character.h」に記述がありますが、「テンプレートクラス」として実装されてます。
 「テンプレートクラス」を説明する前に、プレイヤー及び敵に「HPバー」を実装する方法を考えてみます。おおむね以下の3通りの方法があると思います。
1、プレイヤーと敵の2つのクラス(マネージャ含めると2セットというべきか)を作成する
2、クラスは1セットだが、プレイヤーや敵が、その「HPバーセット」を管理しHP変更の場合は、そのメンバ関数を呼び出す。
3、クラスは1セットで、「HPバーマネージャ」がプレイヤー及び敵の「GetHP()関数」呼び出しを行って、HPを取得する。
 1の方法は助長的です。敵の種類が増えるたびに、同じようなHPバークラスを作成する必要があります。
 1セットの「HPバークラス」で行うのは2か3ですが、2のケースであれば「テンプレートクラス」は必要がありません。
 3のケースは、「HPバークラス」が、そのバーを保持するオブジェクトのメンバ関数を呼び出す必要があります。例えば「GetHP()関数」などです。しかし、「GetHP()関数」は「GameObjectクラス」には存在しない関数です。ですから、「プレイヤーのGetHP()関数」や、「敵のGetHP()関数」を呼び出すためには、「プレイヤークラス」や「敵クラス」を特定する必要があります。
 これまでのサンプルでは「dynamic_pointer_cast<T>()関数」や「ステージ」の「GetSharedGameObject<T>()関数」などで特定してきましたが、もう一つの方法として紹介するのが「3のケース」の「テンプレートクラス(テンプレートゲームオブジェクトクラス)」です。

 「HPバー」は「Character.h」に「バーの背景」である「HPSquareBaseクラス」とHPバーそのものの「HPSquareクラス」、そしてそれらを管理するマネージャである「HPManegerテンプレートクラス」で構成されています。
 「HPManegerクラス」はテンプレートのため、cppへの記述はしません。(すべてヘッダに記述します)
 以下は「HPManegerクラス」です。そんなに長いコードではないので全部抜粋します。
//--------------------------------------------------------------------------------------
//  HPのマネージャ(テンプレートクラス)
//--------------------------------------------------------------------------------------
template<typename T>
class HPManeger : public GameObject {
    weak_ptr<T> m_TargetObj;
    weak_ptr<HPSquareBase> m_HPSquareBase;
    weak_ptr<HPSquare> m_HPSquare;
    Col4 m_Color;

    Quat Billboard(const Vec3& Line) {
        Vec3 Temp = Line;
        Mat4x4 RotMatrix;
        Vec3 DefUp(0, 1.0f, 0);
        Vec2 TempVec2(Temp.x, Temp.z);
        if (TempVec2.length() < 0.1f) {
            DefUp = Vec3(0, 0, 1.0f);
        }
        Temp.normalize();
        RotMatrix = XMMatrixLookAtLH(Vec3(0, 0, 0), Temp, DefUp);
        RotMatrix.inverse();
        Quat Qt;
        Qt = RotMatrix.quatInMatrix();
        Qt.normalize();
        return Qt;
    }


    void UpdateHPTrans() {
        auto ShTarget = m_TargetObj.lock();
        auto ShHPSquareBase = m_HPSquareBase.lock();
        auto ShHPSquare = m_HPSquare.lock();
        if (ShTarget && ShHPSquareBase && ShHPSquare) {
            auto HPBaseTrans = ShHPSquareBase->GetComponent<Transform>();
            auto HPTrans = ShHPSquare->GetComponent<Transform>();
            //スケーリング
            float MaxHP = ShTarget->GetMaxHP();
            float HP = ShTarget->GetHP();
            float Width = HP / MaxHP;

            HPBaseTrans->SetScale(0.5f, 0.125f, 0.5);
            HPTrans->SetScale(0.46f * Width, 0.1f, 0.46f);

            auto TargetTrans = ShTarget->GetComponent<Transform>();
            auto Pos = TargetTrans->GetPosition();
            Pos.y += 0.75f;
            HPBaseTrans->SetPosition(Pos);
            Pos = Vec3(0, 0, -0.001f);
            Pos.x -= 0.46f * 0.5f;
            Pos.x += (0.46f * Width * 0.5f);
            HPTrans->SetPosition(Pos);


            auto PtrCamera = GetStage()->GetView()->GetTargetCamera();
            Quat Qt;
            //向きをビルボードにする
            Qt = Billboard(PtrCamera->GetAt() - PtrCamera->GetEye());
            HPBaseTrans->SetQuaternion(Qt);
        }
    }
public:
    //構築と破棄
    HPManeger(const shared_ptr<Stage>& StagePtr,
        const shared_ptr<T>& TargetObj,
        const Col4& Col):
        GameObject(StagePtr),
        m_TargetObj(TargetObj),
        m_Color(Col)
    {}

    virtual ~HPManeger() {}
    //初期化
    virtual void OnCreate() override {
        auto HPBase = GetStage()->AddGameObject<HPSquareBase>();
        auto HP = GetStage()->AddGameObject<HPSquare>(m_Color);
        auto PtrTrans = HP->GetComponent<Transform>();
        PtrTrans->SetParent(HPBase);

        m_HPSquareBase = HPBase;
        m_HPSquare = HP;
        UpdateHPTrans();
    }

    virtual void OnUpdate2() override {
        UpdateHPTrans();
    }
};
 「テンプレートクラス」というのは「クラスの構築にテンプレート引数が必要」なクラスです。
 テンプレートですから、記述時は「どのクラスが渡されるか」はわかりません。コンパイル時に、クラスが指定されます。
 ちなみにこのクラスのインスタンス作成は「GamaStgae.cpp」に、「プレイヤーのHPバーを構築」は「GameStage::CreatePlayer()」内で
    AddGameObject<HPManeger<Player>>(PlayerPtr,Col4(0,1,0,1));
 及び、「敵のHPバーを構築」は「GameStage::CreateEnemy()」内で
    AddGameObject<HPManeger<Enemy>>(EnemyPtr, Col4(1, 0, 0, 1));
 という記述で登録しています。型引数を渡しているのがわかります。

 「HPManegerテンプレートクラス」には「UpdateHPTrans()」関数内に
    float MaxHP = ShTarget->GetMaxHP();
    float HP = ShTarget->GetHP();
 という記述があります。「ShTarget」は「T型」の「shared_ptr」変数です。
 ということは、このテンプレートクラスに関連付けられるクラスには「GetMaxHP()関数」と「GetHP()関数」が必要になります。
 この関係が「テンプレート」の面白いところです。「テンプレートクラス」はコンパイル(ビルド時)に、コンパイラが自動的に作成する「クラス」です。ですからビルド時までこのクラスの実装は検証されません。VC++には「インテリセンス」といって、存在しないメンバ関数などを記述すると赤く表示されたりする機能がありますが、「テンプレートクラス」にはこの機能は適用されません。だって、「テンプレートクラス」のソースの記述時には、どのようなクラスが関連づけられるのかわからないですから。

 「HPManegerテンプレートクラス」は、コンストラクタの引数に「T型のshared_ptr」を取ります。ここにプレイヤーや敵が渡されます。
 「OnCreate()関数」では
    auto HPBase = GetStage()->AddGameObject<HPSquareBase>();
    auto HP = GetStage()->AddGameObject<HPSquare>(m_Color);
 のように、「HPSquareBaseクラス(HPバーの背景の黒い部分)」と「HPSquareクラス(HPバー)」をステージの「AddGameObject()関数」を呼び出して構築します。「GetStage()」でステージのポインタを取得します。
 「HPSquareクラス」は「HPSquareBase」と親子関係として設定します。理由は、「HP」は「ビルボード処理」といって、常にカメラを向く処理をします。バラバラにビルボード処理をすると、手前に置くオブジェクト(つまりHP)と奥に置くオブジェクト(背景)のあいだに微妙な回転のずれが起きてしまいます。親子関係にすれば、HPは背景に合わせて回転しますので、ずれがなくなります。
 HPの値は各オブジェクトから「GetHP()関数呼び出し」で取得して、「GetMaxHP()関数」と合わせて計算します。その計算処理は「UpdateHPTrans()関数」で行います。

行動の使いまわしによるできるだけ少ないコード

 このサンプルには「行動クラス」が実装されています。中でも「FightBehavior」クラスは「戦う系の行動」をカプセル化したもので、プレイヤーも敵も、同じアルゴリズムを利用しています。
 ただ、プレイヤーの場合はコントローラでの操作ですし、敵は完全なAI処理です。その違いをどのようにして吸収しているかを説明したいと思います。
 「FightBehavior」クラスは「ProjectBehavior.h/cpp」で実装されています。以下がそのヘッダです。テンプレートクラスなので、実体もすべてヘッダファイルに記述されています。ですので、各関数の中身は中略しています。
template <typename T>
class FightBehavior : public ObjBehavior<T> {
    //回転切りの回転角度
    float m_RotationShakeRad;
    //剣のweak_ptr
    weak_ptr<Sword> m_SwordWeak;
    //相手の剣に当たったときの変数
    //相手の場所
    Vec3 m_HitBase;
    //ひるむ時間
    float m_HitSwordTime;
    //倒れた処理用
    enum class DieFlag {
        Down,
        Die,
        Up,
    };
    float m_DieRot;
    float m_DieInterval;
    DieFlag m_DieFlag;
    Vec3 m_PokeStart;
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    */
    //--------------------------------------------------------------------------------------
    FightBehavior(const shared_ptr<T>& GameObjectPtr):
        ObjBehavior(GameObjectPtr)
    {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~FightBehavior() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣を持つ処理
    @param[in]  SwordKey    剣を特定するキー
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void HaveSword(const wstring& SwordKey) {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣を振る処理の開始
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void StartShakeSword() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣を振る(回転)
    @return 半周回転したらtrue
    */
    //--------------------------------------------------------------------------------------
    bool RotationShakeSword() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣を突く処理の開始
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void StartPokeSword() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣を突く
    @return 一定距離突いたらtrue
    */
    //--------------------------------------------------------------------------------------
    bool PokeSword() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  相手の剣に当たったときの相手の位置の設定
    @param[in]  pos 位置
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void SetHitBase(const Vec3& pos) {
        m_HitBase = pos;
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  相手の剣に当たった行動
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void HitSwordBehavior() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  剣に当たってひるむ行動
    @return ひるむ時間が終わったらtrue
    */
    //--------------------------------------------------------------------------------------
    bool HitAfterdBehavior() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  倒れる開始
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void DieStartBehavior() {
        //中略
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief  倒れる行動
    @return 一連の動作が終わればtrue
    */
    //--------------------------------------------------------------------------------------
    bool DieBehavior() {
        //中略
    }
};

解説