015.衝突判定の実装と簡単なAI(Dx11版)

 このサンプルはSimplSample015というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルはDx11版しかありません。Dx12版はありませんのでご了承ください。

 実行結果は以下のような画面が出ます。
 起動直後から、に見立てたブロックが追いかけてきます。プレイヤーは丸い球です。XBoxコントローラの右スティックで動かすことができ、左スティックでカメラを変化させることができます。また、カメラを寄るあるいは引くも左十字キーでできます。
 プレイヤーはAボタンでジャンプができます。移動するブロックのほかに障害物があり、これらと衝突判定します。

 

図0015a

 


【サンプルのポイント】

 今項のサンプルは、シンプルバージョンにおいて、どのようにゲーム機能を実装していくかというサンプルとなっています。
 SimpleSample014までのサンプルでは、個別の配置や変化を紹介してきました。しかしこれだけでは、ゲームという形には程遠い状態です。ゲームとしての最低限の機能としましては
1、コントローラやキーボードなどからの入力の受付
2、プレイヤーの操作
3、敵(またはほかのキャラクターやアイテムなど)のAI
4、衝突判定と衝突応答
5、エフェクト
6、インターフェイス
7、ミュージック
8、効果音
9、メニュー、サブメニューやステージ切り替えなどの機能
10、ステージデータなどの読み込み機能
11、プリミティブ形状作成やモデルの読み込みとその描画機能
 などがあげられるでしょう。
 このほかにももちろんそのゲーム固有の機能や、コンセプトやルールにのっとった機能が必要でしょう。
 フルバージョンであれば、ある程度汎用的に利用できる機能は実装されてますが、シンプルバージョンの場合は、これらを0から(あるいは0に近い部分から)実装する必要があります。
 このサンプルでは上記のうちの
1、コントローラからの入力の受付
2、プレイヤーの操作
3、敵(またはほかのキャラクターやアイテムなど)のAI
4、衝突判定と衝突応答
 のサンプルとなります。ただ一つ断っておきたいのは、あくまでサンプルだということです。
 実際にゲームに導入するためには、このままでは当然だめで、どんどん自分のコードで書き換えていく必要があります。つまり、ゲーム制作のとっかかりくらいの役割しか持ってませんので、そのつもりで実行してみてください。

■オブジェクトの種類■

 シーンに実装されているオブジェクトは以下になります。すべてCharacter.h/cppに記述されています。
1、平面(SquareObject)。地面です。タイリング処理されています。
2、プレイヤー(SphereObject)。コントローラで操作できます。
3、ブロック(MoveBoxObject)。AIでプレイヤーをどこまでも追いかけてきます。
4、固定の障害物(BoxObject)。プレイヤーは乗ることができる障害物です。
5、スプライト(WrappedSprite)。左上で回転しているスプライトです。インターフェイス等に利用できるサンプルです。
 これらのオブジェクトについて、順を追って説明します。

■5、スプライト(WrappedSprite)■

 最初は、スプライトです。こちらはほかのオブジェクトの影響は受けませんので最初に説明します。実際にインターフェイス等に利用する場合は、データのやりとりなどが必要になります。

初期化

 スプライトの初期化はWrappedSprite::OnCreate()関数で行います。ラッピング(タイリング)処理しますので。あらかじめコンストラクタに、X方向及びY方向に並べる数を設定しておきます。初期化のポイントは以下です。
void WrappedSprite::OnCreate() {
    float HelfSize = 0.5f;
    //頂点配列(縦横10個ずつ表示)
    m_BackupVertices = {
        { VertexPositionColorTexture(Vec3(-HelfSize, HelfSize, 0),
            Col4(1.0f,0,0,1.0f), Vec2(0.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(HelfSize, HelfSize, 0), 
            Col4(0, 1.0f, 0, 1.0f), Vec2((float)m_XWrap, 0.0f)) },
        { VertexPositionColorTexture(Vec3(-HelfSize, -HelfSize, 0), 
            Col4(0, 0, 1.0f, 1.0f), Vec2(0.0f, (float)m_YWrap)) },
        { VertexPositionColorTexture(Vec3(HelfSize, -HelfSize, 0), 
            Col4(1.0f, 1.0f, 0, 1.0f), Vec2((float)m_XWrap, (float)m_YWrap)) },
    };
    //インデックス配列
    vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
    //メッシュの作成(変更できる)
    m_SquareMesh = MeshResource::CreateMeshResource(m_BackupVertices, indices, true);
    //テクスチャの作成
    m_TextureResource = TextureResource::CreateTextureResource(m_TextureFileName, L"WIC");
}
 m_BackupVerticesVertexPositionColorTexture型の頂点の配列です。動的に頂点を変更できるように、メンバ変数になってます。作成時にテクスチャUV値を、X方向、Y方向の繰り返す数で初期化します。
 赤くなっているtrue頂点変更が可能なメッシュかどうかを指定します。MeshResource::CreateMeshResource()関数はこのパラメータがtrueの場合は頂点変更可能なメッシュを作成します。

更新処理

 更新処理はWrappedSprite::OnUpdate()関数で行います。以下がその実体です。
void WrappedSprite::OnUpdate() {
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    m_Rot += ElapsedTime;
    if (m_Rot >= XM_2PI) {
        m_Rot = 0;
    }
    UpdateVertex(ElapsedTime);
}
 ここではまず、App::GetApp()->GetElapsedTime()によって、前回からのターン時間を取得します。この値を、回転のラジアン値であるm_Rotに加算しています。これを
    m_Rot += 0.01f;
 などのように、即値(マジックナンバーではなかったとしても)を設定するのはできるだけ控えましょう。パソコンによっては、ターン時間が一定とは限りません。かならずElapsedTimeを何らかの形で計算に利用する癖をつけましょう。

 WrappedSprite::OnUpdate()ではUpdateVertex()関数を呼んでます。これはWrappedSpriteのプライベートメンバ関数です。実体は、以下です。
void WrappedSprite::UpdateVertex(float ElapsedTime) {
    m_TotalTime += ElapsedTime;
    if (m_TotalTime >= 1.0f) {
        m_TotalTime = 0;
    }

    auto Dev = App::GetApp()->GetDeviceResources();
    auto pD3D11DeviceContext = Dev->GetD3DDeviceContext();

    //頂点の変更
    //D3D11_MAP_WRITE_DISCARDは重要。この処理により、GPUに邪魔されない
    D3D11_MAP mapType = D3D11_MAP_WRITE_DISCARD;
    D3D11_MAPPED_SUBRESOURCE mappedBuffer;
    //頂点のマップ
    if (FAILED(pD3D11DeviceContext->Map(m_SquareMesh->GetVertexBuffer().Get(),
         0, mapType, 0, &mappedBuffer))) {
        // Map失敗
        throw BaseException(
            L"頂点のMapに失敗しました。",
            L"if(FAILED(pID3D11DeviceContext->Map()))",
            L"WrappedSprite::UpdateVertex()"
        );
    }
    //頂点の変更
    VertexPositionColorTexture* vertices
        = (VertexPositionColorTexture*)mappedBuffer.pData;
    for (size_t i = 0; i < m_SquareMesh->GetNumVertices(); i++) {
        Vec2 UV = m_BackupVertices[i].textureCoordinate;
        if (UV.x == 0.0f) {
            UV.x = m_TotalTime;
        }
        else if (UV.x == 4.0f) {
            UV.x += m_TotalTime;
        }
        vertices[i] = VertexPositionColorTexture(
            m_BackupVertices[i].position,
            m_BackupVertices[i].color,
            UV
        );
    }
    //アンマップ
    pD3D11DeviceContext->Unmap(m_SquareMesh->GetVertexBuffer().Get(), 0);
}
 ここでは、動的に頂点を変更する処理をしています。
 m_TotalTimeというfloat型のメンバ変数を持っていて、それにElapsedTimeを加算していきます。1秒をこえたら0に初期化します。つまり、流れるUV値は1秒基準で変化します。
 次に重要な処理はマップと言われる処理です。Dx11では、頂点バッファに登録されている領域にアクセスする場合、GPUによって邪魔がいらないよう、ロックを掛ける必要があります。それが
    D3D11_MAP mapType = D3D11_MAP_WRITE_DISCARD;
    D3D11_MAPPED_SUBRESOURCE mappedBuffer;
    //頂点のマップ
    if (FAILED(pD3D11DeviceContext->Map(m_SquareMesh->GetVertexBuffer().Get(),
         0, mapType, 0, &mappedBuffer))) {
 の部分です。mapTypeD3D11_MAP_WRITE_DISCARDにすることにより、書き込みできるようになります。
 D3D11_MAP_WRITE_DISCARDの場合、頂点バッファの内容は破壊されているので、頂点の内容の再構築が必要になります。この時に使用するのが、構築時にとっておいたm_BackupVerticesです。
    //頂点の変更
    VertexPositionColorTexture* vertices
        = (VertexPositionColorTexture*)mappedBuffer.pData;
    for (size_t i = 0; i < m_SquareMesh->GetNumVertices(); i++) {
        Vec2 UV = m_BackupVertices[i].textureCoordinate;
        if (UV.x == 0.0f) {
            UV.x = m_TotalTime;
        }
        else if (UV.x == 4.0f) {
            UV.x += m_TotalTime;
        }
        vertices[i] = VertexPositionColorTexture(
            m_BackupVertices[i].position,
            m_BackupVertices[i].color,
            UV
        );

    }
 上記の処理は、赤くなっているところでmappedBuffer.pDatverticesにキャストしています。これで、verticesへの書き込みは、マップされたデータへの書き込みになります。
 あとは、m_BackupVerticesを参照しながらUV値を変更します。これで、テクスチャが流れるような表現になります。
 書き込みが終わったら、アンマップします。
    //アンマップ
    pD3D11DeviceContext->Unmap(m_SquareMesh->GetVertexBuffer().Get(), 0);

描画処理

 描画はスプライトの描画になります。シェーダはこれまでも紹介してきたものです。WrappedSprite::OnDraw()関数で行いますが、透明処理するかしないかの分岐とラッピングサンプラーで、タイリング処理しています。以下の部分です。
    //テクスチャとサンプラーの設定
    ID3D11ShaderResourceView* pNull[1] = { 0 };
    pD3D11DeviceContext->PSSetShaderResources(0, 1, 
        m_TextureResource->GetShaderResourceView().GetAddressOf());
    //ラッピングサンプラー
    ID3D11SamplerState* pSampler = RenderState->GetLinearWrap();
    pD3D11DeviceContext->PSSetSamplers(0, 1, &pSampler);
 このようにして、スプライトオブジェクトを作成しています。

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

 スプライト(WrappedSprite)以外は3Dオブジェクトです。これらはPNT頂点(PositionNormalTexture)を持ちます。つまり描画処理はほぼ同じということになります。
 であれば各オブジェクトのOnDraw()関数を別に記述するのではなく、同じ処理をする関数(あるいはクラス)を作成するのが効率的です。
 PNTDrawObjectクラスはそんな役割のクラスです。

PNTDrawObjectクラスの実装

 PNTDrawObjectクラスは、ほかのクラス同様ObjectInterfaceとShapeInterfaceを親に持つクラスです。特徴とすれば、AddDrawMesh()関数という公開メンバ関数を持ち、ほかの3Dオブジェクトが、描画のタイミングでこの関数を呼び出します。
 この関数の宣言は以下のようになってます。
    //--------------------------------------------------------------------------------------
    /*!
    @brief 描画するオブジェクトを追加する
    @param[in]  MeshRes メッシュ
    @param[in]  TextureRes テクスチャ
    @param[in]  WorldMat ワールド行列
    @param[in]  Trace 透明処理するかどうか
    @param[in]  Wrap ラッピング処理するかどうか
    @return なし
    */
    //--------------------------------------------------------------------------------------
    void AddDrawMesh(const shared_ptr<MeshResource>& MeshRes,
        const shared_ptr<TextureResource>& TextureRes,
        const Mat4x4& WorldMat,
        bool Trace,bool Wrap = false);
 コメントにあるように、各3Dオブジェクトは、この描画クラスに、メッシュやテクスチャ、あるいはワールド行列など、3Dオブジェクトの描画に必要なパラメータを渡します。
 AddDrawMesh()関数では、これらのパラメータを受け取り、特殊な構造体である、DrawObject構造体のインスタンスを作成します。そして、PNTDrawObjectクラスのメンバであるm_DrawObjectVecに追加します。m_DrawObjectVecDrawObjectの配列(vector)です。
 以下はAddDrawMesh()関数の実体です。
void PNTDrawObject::AddDrawMesh(const shared_ptr<MeshResource>& MeshRes,
    const shared_ptr<TextureResource>& TextureRes,
    const Mat4x4& WorldMat,
    bool Trace, bool Wrap) {
    DrawObject Obj;
    Obj.m_MeshRes = MeshRes;
    Obj.m_TextureRes = TextureRes;
    Obj.m_WorldMatrix = WorldMat;
    Obj.m_Trace = Trace;
    Obj.m_Wrap = Wrap;
    m_DrawObjectVec.push_back(Obj);
}
 このように、m_DrawObjectVecにためておきます。そしてPNTDrawObject::OnDraw()で一気に描画します。描画のコードはこれまでに紹介してきたPNTオブジェクトの描画と基本的に同じです。オブジェクトの数だけループして描画します。
 さて、ここで、毎ターンm_DrawObjectVecにためたのでは、あふれてしまうのでは、という疑問がわきます、1ターンは約60分の1秒に1回ですから、いくら配置オブジェクトが少なくてもすぐに大量になってしまいます。
 ですから、PNTDrawObject::OnUpdate()では、この配列をクリアします。
void PNTDrawObject::OnUpdate() {
    m_DrawObjectVec.clear();
}
 こんな感じです、これは描画終了直後にクリアしても問題はありません。ようは、毎ターン、どこかで初期化されていればいいのです。
 さて、ここで疑問が生まれます。vectorSTLにある可変長配列ですが、毎ターンクリアとpush_backを繰り返して、負担はかからないのか、という疑問です。
 これにはSTLのvectorについて、少し説明が必要です。

STLのvector

 STLのvectorはC++で汎用的に使用できる、大変便利な可変長配列です。しかし可変長配列といいますが、実はデフォルトでは増えることがあっても減ることがない配列なのです。
 例えば、要素を指定しないでSTLのvectorを作成すると、要素数は0で初期化されます。ここまでは想像できます。
 1回push_backを呼ぶと、要素数は1になります。実は、ここにcapacityという特別な変数があり、これも1になります。
 capacity容量という意味です。vectorpush_backが呼ばれるたびに、必要なメモリをnewして、capacityを増やすのです。ちょっとわかりにくいかと思うので、要素数(size()で取得できる値)とcapacityの関係(一般的な動き)を以下に示します。
要素数(size()関数の戻り値) capacity(capacity()関数の戻り値)
1 1
2 2
3 4
5 8
9 16
17 32
 この表は、push_backによって、要素数が増えた場合に、newで作成されるcapacityの関係を表にしたものです。capacityの増加数を見るとわかりますが1,2,4,8,16,32....と、倍々増えていってるのがわかります。
 このようにSTLのvectorcapacityを、使用される要素数sizeより多く持つことにより、newの呼び出しを最小限に抑える機能を持っています。

 さて、このことを前提に、PNTDrawObject::OnUpdate()を見てみましょう。
    m_DrawObjectVec.clear();
 となっていますclear()関数要素数を0にする関数です。決してcapacityを0にする(つまりメモリを開放する)関数ではないのです。capacityが0になる(つまりメモリを開放する)のは、何もしなければ、そのvectorのデストラクタが呼ばれたときです。
 ですから、次のターンで、同じオブジェクトがpush_backされても、あらたにnewされるのではなく、無効状態になっているメモリに割り当てる形になります。このことを知っておくと、STLのvector内部のメモリの動き、が理解できると思います。

 一般的なゲームは、ステージの読み込み時に、配置されるオブジェクトを、おおむね全部、読み込みます。実行途中で読み込むことも可能ですが、newの呼び出しを極力減らすためにも、一気にメモリに読み込みます。
 ここでSTLのvectorと、ネイティブなC言語と比べてみましょう。
 例えば、150個のオブジェクトをステージ作成時に読み込む場合、C言語では150個分のメモリをmallocします。STLのvectorは、ここまで直接的ではないですが1,2,4,8,16,32,128,2568回newします(もちろん150個でメモリを初期化することもできますが、すべてpush_backを使ったとして、です)。
 この1回と8回の差がもったいないというのであれば仕方がないですが、STLのvectorの多大な恩恵に対して、ステージ構築時の8回のnewは決して無駄ではないと、僕は思います。

PNTDrawObjectクラスの描画

 さてこのようにAddDrawMesh()関数によって、描画すべきオブジェクトが登録されますが、実際の描画はPNTDrawObject::OnDraw()で行います。
 ここでは、m_DrawObjectVecにためられたオブジェクトの描画のためのデータを使って描画します。
 実は、ここではフルバージョンではできない処理が実装されています。BaseCrossのフルバージョンでは、汎用性を担保するために、各オブジェクトの描画の後に、パイプラインの設定を初期化します。PNTDrawObject::OnDraw()の最後に記述がある
    //後始末
    Dev->InitializeStates();
 という処理です。この関数を追いかけていくとわかりますが、レンダリングに必要なパラメータをすべて初期化しています。
 フルバージョンではこの関数を各オブジェクトの描画ごとに呼び出します。エフェクトなど特殊なオブジェクトは別ですがGameObjectの派生クラスを描画するたびに初期化します。
、実はこの処理は、案外コストが高いと思っています。(思っていますというのは、Dx11内部の処理なので細かいことまではわからない。大量な描画を繰り返して、処理速度を比較すればわかるかもしれないけど、そんな趣味はないので・・・)。
 いずれにせよ、パイプラインの設定変更は最低限に抑えたほうが速いに決まってます。
 ですから、配置されるオブジェクトの細かい処理を実装できるシンプルバージョンのほうが圧倒的に自由度が高いといえます。

■3Dオブジェクトの作成■

 さて、描画処理はPNTDrawObjectクラスに任せるとして、問題は各オブジェクトの更新処理です。このサンプルの一番のポイントはここにあります。
 配置される3Dオブジェクトをもう一度おさらいしてみましょう。
1、平面(SquareObject)。地面です。タイリング処理されています。
2、プレイヤー(SphereObject)。コントローラで操作できます。
3、ブロック(MoveBoxObject)。AIでプレイヤーをどこまでも追いかけてきます。
4、固定の障害物(BoxObject)。プレイヤーは乗ることができる障害物です。
 です。この中で動かないのは1と4です。は、コントローラによって、はAIによって動きがあります。
 では、動かない1と4について説明をします。

平面(SquareObject)の初期化

 平面(SquareObject)の初期化SquareObject::OnCreate()に記述があります。この関数はさらにCreateBuffers()メンバ関数を呼んで、頂点の初期化を行っています。以下はCreateBuffers()メンバ関数の実体です。
void SquareObject::CreateBuffers(float WrapX, float WrapY) {
    float HelfSize = 0.5f;
    vector<VertexPositionNormalTexture> vertices = {
        { VertexPositionNormalTexture(Vec3(-HelfSize, HelfSize, 0), 
                Vec3(0, 0, -1.0f), Vec2(0.0f, 0.0f)) },
        { VertexPositionNormalTexture(Vec3(HelfSize, HelfSize, 0), 
                Vec3(0, 0, -1.0f), Vec2(WrapX, 0.0f)) },
        { VertexPositionNormalTexture(Vec3(-HelfSize, -HelfSize, 0), 
                Vec3(0, 0, -1.0f), Vec2(0.0f, WrapY)) },
        { VertexPositionNormalTexture(Vec3(HelfSize, -HelfSize, 0), 
                Vec3(0, 0, -1.0f), Vec2(WrapX, WrapY)) },
    };

    vector<uint16_t> indices = {
        0, 1, 2,
        1, 3, 2,
    };
    //メッシュの作成(変更できない)
    m_SquareMesh = MeshResource::CreateMeshResource(vertices, indices, false);
}
 このように、タイリング処理をするので、分割数をテクスチャUVに設定しています。最後の
    //メッシュの作成(変更できない)
    m_SquareMesh = MeshResource::CreateMeshResource(vertices, indices, false);
 でメッシュを作成しますが、スプライト(WrappedSprite)と違って、こちらは頂点変化させないのでfalseを渡しています。

平面(SquareObject)の更新

 平面は変化しないので更新処理は空関数になっています。

平面(SquareObject)の描画

 描画処理は、前述したPNTDrawObjectAddDrawMesh()関数に必要なパラメータを渡します。実際の描画はPNTDrawObjectクラスが行います。
 以下、3Dオブジェクトの描画処理は同様の処理です。

固定の障害物(BoxObject)の初期化

 こちらも平面同様、変化しないので先に説明します。ただ、ちょっと平面とは違う部分があります。こちらは衝突判定は行います。自分からは判定は行いませんが、プレイヤーや移動するブロックが、このオブジェクトに対して衝突判定を行います。
 その判定がしやすいように、BoxBaseクラスというボックスの親を定義しています。
 このクラスはBoxObjectのほかに、移動するブロックであるMoveBoxObjectの親クラスでもあります。以下がBoxBaseクラスの宣言です。
//--------------------------------------------------------------------------------------
/// ボックスの親
//--------------------------------------------------------------------------------------
class BoxBase : public ObjectInterface, public ShapeInterface {
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief コンストラクタ
    */
    //--------------------------------------------------------------------------------------
    BoxBase() :
        ObjectInterface(),
        ShapeInterface() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~BoxBase() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief OBBを得る(仮想関数)
    @return OBB
    */
    //--------------------------------------------------------------------------------------
    virtual OBB GetOBB()const = 0;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 衝突判定をする(仮想関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCollision() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief 回転処理をする(仮想関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnRotation() {}
};
 このクラスはcppファイルの記述はありません。BoxObjectクラスと移動するブロックのMoveBoxObjectクラスの共通の親です。このようなクラスを用意することで、まとめて衝突判定ができるようになります。
 このクラスにはGetOBB()という純粋仮想関数があります。この関数は、派生クラスが必ず実装しなければいけない関数です。何をする関数かというと衝突判定に使用するOBBボリューム境界を返す関数です。プレイヤーや移動するブロックがこの関数を通じて、OBBを取り出します。

 それで固定のボックスであるBoxObjectBoxBaseの派生クラスとして作成します。以下は宣言です。
//--------------------------------------------------------------------------------------
/// 固定のボックス
//--------------------------------------------------------------------------------------
class BoxObject : public BoxBase {
    //中略
public:
    //中略
    //--------------------------------------------------------------------------------------
    /*!
    @brief OBBを得る
    @return OBB
    */
    //--------------------------------------------------------------------------------------
    virtual OBB GetOBB()const;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 初期化
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate() override;
    //中略
};
 このようにBoxBaseの派生クラスとして作成します。また仮想関数GetOBB()も実装します。

 固定の障害物(BoxObject)の初期化BoxObject::OnCreate()に記述があります。以下実体です。
void BoxObject::OnCreate() {
    vector<VertexPositionNormalTexture> vertices;
    vector<uint16_t> indices;
    MeshUtill::CreateCube(1.0f, vertices, indices);
    //メッシュの作成(変更できない)
    m_BoxMesh = MeshResource::CreateMeshResource(vertices, indices, false);

    //テクスチャの作成
    m_TextureResource = ObjectFactory::Create<TextureResource>(m_TextureFileName, L"WIC");
}
 ここではこれまでの例のように頂点を自作するのではなくシンプルバージョンでも使用できるユーティリティ関数を使用して、ボックス(立方体)を作成しています。
    MeshUtill::CreateCube(1.0f, vertices, indices);
 この部分です。これで、1辺が1.0fのPNT頂点の立方体を作成できます。これ以外の頂点や、あるいはタイリング処理したりする場合は、いったんユーティリティ関数を使ってPNT頂点のオブジェクトを作った後、内容を変更するといいでしょう。

固定の障害物(BoxObject)の更新と描画

 更新は何も記述しません。描画は、PNTDrawObject::AddDrawMesh()を呼び出します。

■3Dオブジェクトの配置と衝突判定■

 残りの2つのオブジェクトの説明の前に、このサンプルにおけるシーンへの配置の方法と衝突判定の実装方法を説明します。
 フルバージョンでは配置オブジェクトステージに配置されます。BaseCrossの命名では、シーンというのはアプリケーション唯一のオブジェクトでステージはシーンによって作成されるオブジェクトです。ですからゲームステージあるいはメニューステージなどはシーンによって切り替えられます。
 シンプルバージョンではステージという概念はありません。ですのでステージ(もしくはそれに類するオブジェクト)は自作する必要があるわけですがシーンは存在します。
 これまでのサンプル、そしてこのサンプルもシーンに直接オブジェクトを配置しています。

 さて、シーンにあるメンバ変数は各オブジェクトをコレクションしたものとなります。フルバージョンのようにゲームオブジェクトといった共通の親はないので、配置されるオブジェクトは、そのポインタ(shared_ptr)を、メンバ変数としてもちます。以下はSceneクラスの宣言部です。各オブジェクトクラスのポインタが用意されています。
//--------------------------------------------------------------------------------------
/// ゲームシーン
//--------------------------------------------------------------------------------------
class Scene : public SceneInterface {
    shared_ptr<SquareObject> m_SquareObject;             ///<平面オブジェクト
    shared_ptr<SphereObject> m_SphereObject;             ///<球オブジェクト
    vector<shared_ptr<BoxBase>> m_BoxVec;     ///<ボックスの配列

    shared_ptr<PNTDrawObject>    m_PNTDrawObject;                ///<描画オブジェクト
    shared_ptr<WrappedSprite> m_WallSprite;      ///<スプライト
//以下略
 このように平面、球(プレイヤー)、スプライトは単一のポインタ(shared_ptr)ですが、ボックスは配列になっています。それも、先ほど説明したBoxObjectのポインタの配列ではなく、親クラスであるBoxBaseのポインタの配列です。これの意味するところは、動きのないBoxObjectも、これから説明する、動きがあるMoveBoxObjectも、同じ配列にまとめられます。こうすることで、衝突判定を別々に書かなくても済むようになってます。
 各オブジェクトの配置は、Scene::OnCreate()関数で行っているわけですが、以下のようにボックスは配置されます。
void Scene::OnCreate() {

    //中略

    m_BoxVec.push_back(
        ObjectFactory::Create<BoxObject>(
            GetThis<Scene>(), strTexture, false,
            Vec3(5.0f, 0.5f, 5.0f),
            Quat(),
            Vec3(5.0f, 0.25f, 0.0f)
            )
    );

    //中略

    m_BoxVec.push_back(
        ObjectFactory::Create<BoxObject>(
            GetThis<Scene>(), strTexture, false,
            Vec3(5.0f, 0.5f, 5.0f),
            Quat(Vec3(0, 0, 1), -XM_PIDIV4),
            Vec3(-5.0f, 1.0f, 0.0f)
            )
    );


    strTexture = DataDir + L"wall.jpg";

    //移動ボックス
    m_BoxVec.push_back(
        ObjectFactory::Create<MoveBoxObject>(
            GetThis<Scene>(), strTexture, false,
            Vec3(0.25f, 0.5f, 0.5f),
            Quat(),
            Vec3(0.0f, 0.25f, 5.0f)
            )
    );

    //中略


}
 このようにBoxBaseのポインタの配列であるm_BoxVecMoveBoxObjectも配置しています。

ブロック(MoveBoxObject)クラスの実装と衝突判定

 さていよいよ、衝突判定を行うオブジェクトの作成です。
 まず、ブロックである、MoveBoxObjectです。初期化はMoveBoxObject::OnCreate()に記述がありますが、固定のボックスと大きな違いはありません。
 更新処理ですが、MoveBoxObjectクラスにはOnUpdate()関数のほかにMoveBoxObject::OnCollision()という衝突判定を行う関数があります。またMoveBoxObject::OnRotation()関数もあります。
 この関数は仮想関数で、親クラスである
//--------------------------------------------------------------------------------------
/// ボックスの親
//--------------------------------------------------------------------------------------
class BoxBase : public ObjectInterface, public ShapeInterface {
public:
    //中略
    //--------------------------------------------------------------------------------------
    /*!
    @brief OBBを得る(仮想関数)
    @return OBB
    */
    //--------------------------------------------------------------------------------------
    virtual OBB GetOBB()const = 0;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 衝突判定をする(仮想関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCollision() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief 回転処理をする(仮想関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
      virtual void OnRotation() {}
};
 OnCollision()やOnRotation()関数が、同じく仮想関数GetOBB()と違うところは純粋仮想関数ではないというところです。
 純粋仮想関数は、派生クラスで必ず記述しなければいけないのに対して、この仮想関数は上記のように空関数ですが実体を持ってます。ですから、派生クラスでは必要であれば多重定義するという意味になります。
 ですから、同じBoxBaseの派生クラスであるBoxObjectクラスではOnCollision()やOnRotation()関数は定義されてません。
 では、この関数は使われ方をするのでしょうか?答えはScene::OnUpdate()関数にあります。以下は実体です。
void Scene::OnUpdate() {
    //更新
    m_SquareObject->OnUpdate();
    m_SphereObject->OnUpdate();
    for (auto& v : m_BoxVec) {
        v->OnUpdate();
    }
    //衝突判定
    m_SphereObject->OnCollision();
    for (auto& v : m_BoxVec) {
        v->OnCollision();
    }
    //回転処理
    m_SphereObject->OnRotation();
    for (auto& v : m_BoxVec) {
        v->OnRotation();
    }
    //描画オブジェクトの更新
    m_PNTDrawObject->OnUpdate();
    m_WallSprite->OnUpdate();

    //中略

}
 このように、プレイヤーとボックスのOnUpdate()を呼んだあとで、再びそれらのOnCollision()、さらにOnRotation()を呼び出しています。
 すなわち、一通り移動処理が終わったところで、一通り衝突判定を行い、その後、一通り回転処理を行います。
 なぜこんなまどろっこいことをするんでしょうか。
 これは、ゲームにはターン(ステップ)がある、ということに起因します。
 現実の世界では時間は継続しています。しかし、ゲームのステップは約60分の1づつ、ストップモーションがかかった世界です。僕たちが移動と称しているのは、実は少しずつ飛んでいるのです。現実が実線とするなら、ゲームのターンは点線の感じです。飛び飛びです。
 そのため、すべてのオブジェっクトが移動し終わった後でないと、何と何が衝突しているかどうか、の、誤差が大きくなります。ですからすべてが移動し終わった後に衝突判定を行います。
 また、回転処理については、このサンプルの衝突判定がフルバージョンサンプル。304.衝突判定を考えるで述べた方法で判定していることが起因します。
 トンネル現象が起こらない手法として、上記手法をとっているわけですが、この方法だと回転に弱いのです。同じ回転を維持したまま、前回のターンと今回のターンの経過を前提として判定するので、各々の衝突直後の処理として回転させると、安定しない動きになってしまいます。

 ちょっと蛇足として、いわゆる物理エンジンが実装している衝突判定について述べます。多くのフレームワークが実装している物理エンジンは、トンネル現象を加味しない形で判定を行う場合が多いです。というのは衝突によって生じる回転を計算に入れているからです。例えば直方体同士が衝突した場合、当たる場所や角度によって、衝突後の回転が違うものになります。正確に物理計算をした場合、この回転の要素は無視できません。
 しかし、このサンプルでは、回転衝突後は移動方向を向くという形で簡略化しています。
 衝突判定は正確にやろうとしても、どこかに誤差が出てしまいます。物理エンジンの判定は、より正確でより複雑な形状に対応できますが、その分トンネル現象が起きやすかったりするので、60分の1さらに刻むことも必要でしょう。
 ただ、衝突判定の正確さはそのゲームによって違うと思います。作るのはゲームであってシミュレーションではありません。遊ぶユーザーがリアルに感じればそれでいいのです。逆に正確な物理計算がユーザーの操作の邪魔になってはいけません。

 というわけでScene::OnUpdate()による、オブジェクトの関数呼び出し方法に沿って、説明します。

ブロック(MoveBoxObject)クラスのOnUpdate()

 こちらはMoveBoxObject::OnUpdate()に実体があります。ここではUpdateVelosity()関数を呼び出しています。以下はMoveBoxObject::UpdateVelosity()の実体です。
void MoveBoxObject::UpdateVelosity() {
    auto ShPtrScene = m_Scene.lock();
    if (!ShPtrScene) {
        return;
    }
    //フォース(力)
    Vec3 Force(0, 0, 0);
    //プレイヤーを向く方向ベクトル
    Vec3 ToPlayerVec = 
        ShPtrScene->GetSphereObject()->GetPosition() - m_Pos;
    //縦方向は計算しない
    ToPlayerVec.y = 0;
    ToPlayerVec *= m_Speed;
    //力を掛ける方向を決める
    Force = ToPlayerVec - m_Velocity;
    //力と質量から加速を求める
    Vec3 Accel = Force / m_Mass;
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //速度を加速する
    m_Velocity += Accel * ElapsedTime;
}
 ここでは、プレイヤーを追いかけるというAI処理をフォースを用いて計算しています。
 フォースです。ものの速度を変化させるのには、どこかの方向にを加える必要があります。軽いものであれば、少ない力で変化させられますが、重いものだと大きな力が必要になります。そうして力と質量から加速を導き出し、それを現在の速度に加算します。すると、オブジェクトの移動する方向が変わります。
 このフォースを使った移動は、フルバージョンのサンプルでも随所に説明してますので、繰り返しませんが、オブジェクトを移動させる場合、以下の3つの方法が考えられます。
1、直接、位置(Position)を変化させる
2、速度(Velocity)を変化させて、その値でPositionを動かす。
3、力(Force)を作り出して、加速を加え、それで速度(Velocity)を変化させる。
  最後に、Velocityの値でPositionを動かす。
 1は直感的ですが、ほかのオブジェクトと相互作用をする場合、Velocotyを持たないので、エラーが出やすくなります。(例えば相手にめり込んで止まっちゃうとか)
 2は、1よりはましですが、Velocityを直接いじるので、例えば左に動く物体を、急に右に全速力で動かすなんてことが可能になります。現実にはこんなことはありえないので、注意して計算する必要があります。ほかのオブジェクトとの相互作用は、いくらかやりやすくなります。
 3は、2より、よりリアリティな動きを演出できます。半面ぬるぬるした動きになり、それはそれでゲーム的なリアリティは失われる場合があります。他オブジェクトとの相互作用は、かなりバグが少なく実装できます。

 結論として、1は他と相互作用しないのであればいいかと思いますが、相互作用をする場合、2か3を使うのがいいでしょう。どちらかは、オブジェクトのタイプによると思います。
 AIのみで動くブロックのような処理を実装する場合、3ばベストと考えます。半面プレイヤーに3を使うと、なんとなく操作しにくい、もたもたした動きになるので、操作する人にストレスを与えます。ですので、基本3にしておいて、コントローラ操作に対する自由度は高くしておくか、あるいは2を使う方法がいいと思います。
 今回のサンプルでは、プレイヤーは、2で実装しています。

ブロック(MoveBoxObject)クラスのOnCollision()

 これはMoveBoxObject::OnCollision()に記述されてますが、この関数ではCollisionWithBoxes()を呼んでいます。以下実体です、
void MoveBoxObject::CollisionWithBoxes(const Vec3& BeforePos) {
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //衝突判定
    auto ShPtrScene = m_Scene.lock();
    for (auto& v : ShPtrScene->GetBoxVec()) {
        if (v == GetThis<BoxBase>()) {
            //相手が自分自身なら処理しない
            continue;
        }
        OBB DestObb = v->GetOBB();
        OBB SrcObb = GetOBB();
        SrcObb.m_Center = BeforePos;
        float HitTime;
        Vec3 CollisionVelosity = (m_Pos - BeforePos) / ElapsedTime;
        if (HitTest::CollisionTestObbObb(SrcObb, CollisionVelosity, DestObb, 0, ElapsedTime, HitTime)) {
            m_Pos = BeforePos + CollisionVelosity * HitTime;
            float SpanTime = ElapsedTime - HitTime;
            //m_Posが動いたのでOBBを再取得
            SrcObb = GetOBB();
            Vec3 HitPoint;
            //最近接点を得るための判定
            HitTest::ClosestPtPointOBB(SrcObb.m_Center, DestObb, HitPoint);
            //衝突法線をHitPointとm_Posから導く
            Vec3 Normal = m_Pos - HitPoint;
            Normal.normalize();
            //速度をスライドさせて設定する
            m_Velocity = ProjUtil::Slide(m_Velocity, Normal);
            //Y方向はなし
            m_Velocity.y = 0;
            //最後に衝突点から余った時間分だけ新しい値で移動させる
            m_Pos = m_Pos + m_Velocity * SpanTime;
            //追い出し処理
            //少しづつ相手の領域から退避する
            //最大10回退避するが、それでも衝突していたら次回ターンに任せる
            int count = 0;
            while (count < 20) {
                //退避する係数
                float MiniSpan = 0.001f;
                //もう一度衝突判定
                //m_Posが動いたのでOBBを再取得
                SrcObb = GetOBB();
                if (HitTest::OBB_OBB(SrcObb, DestObb)) {
                    //最近接点を得るための判定
                    HitTest::ClosestPtPointOBB(SrcObb.m_Center, DestObb, HitPoint);
                    //衝突していたら追い出し処理
                    Vec3 EscapeNormal = SrcObb.m_Center - HitPoint;
                    EscapeNormal.y = 0;
                    EscapeNormal.normalize();
                    m_Pos = m_Pos + EscapeNormal * MiniSpan;
                }
                else {
                    break;
                }
                count++;
            }
        }
    }
}
 ここではShPtrScene->GetBoxVec()でシーンからボックスの配列を取り出し、その中の要素とOBB対OBBの衝突判定を行ってます。その際、ボックスの配列には自分自身も含まれるので、赤くなっているところのように自分とは判定しないという形にしています。
 具体的なコードの流れはコード中のコメントを読んでください。
 なお、ブロックオブジェクトからプレイヤーにたいする判定は行ってません。これはプレイヤーがわで行います。衝突判定は多対多の判定ですが、2重に行う必要はありません。
 また、最後の
            //追い出し処理
            //少しづつ相手の領域から退避する
            //最大10回退避するが、それでも衝突していたら次回ターンに任せる
            int count = 0;
            while (count < 20) {
                //退避する係数
                float MiniSpan = 0.001f;
                //もう一度衝突判定
                //m_Posが動いたのでOBBを再取得
                SrcObb = GetOBB();
                if (HitTest::OBB_OBB(SrcObb, DestObb)) {
                    //最近接点を得るための判定
                    HitTest::ClosestPtPointOBB(SrcObb.m_Center, DestObb, HitPoint);
                    //衝突していたら追い出し処理
                    Vec3 EscapeNormal = SrcObb.m_Center - HitPoint;
                    EscapeNormal.y = 0;
                    EscapeNormal.normalize();
                    m_Pos = m_Pos + EscapeNormal * MiniSpan;
                }
                else {
                    break;
                }
                count++;
            }
 は追い出し処理(拘束の解除)です。トンネル現象が起こらない形で判定してるので、この処理が必要なのは全部ではないのですが、まれに、相手に拘束されてしまう場合があるので、実装してます。これはOBBとOBBなので、ややこしい処理になってますが、プレイヤー側ではもう少し単純になります。

ブロック(MoveBoxObject)クラスのOnRotation()

 ここでは進行方向を徐々に向く処理(補間しながら回転する)処理となってます。MoveBoxObject::RotToHead()関数を確認ください。

プレイヤー(SphereObject)クラスの初期化と描画

 さて、いよいよプレイヤーです。こちらはこれまでの3Dオブジェクトと基本的に同じです。

プレイヤー(SphereObject)クラスのOnUpdate()

 更新処理はSphereObject::OnUpdate()が実体です。
void SphereObject::OnUpdate() {
    //1つ前の位置を取っておく
    m_BeforePos = m_Pos;
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //コントローラの取得
    auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
    auto ShPtrScene = m_Scene.lock();
    if (!ShPtrScene) {
        return;
    }
    if (CntlVec[0].bConnected) {
        if (!m_JumpLock) {
            //Aボタン
            if (CntlVec[0].wPressedButtons & XINPUT_GAMEPAD_A) {
                m_BeforePos.y += 0.01f;
                m_Pos.y += 0.01f;
                m_GravityVelocity = Vec3(0, 4.0f, 0);
                m_JumpLock = true;
            }
        }
        Vec3 Direction = GetMoveVector();
        if (Direction.length() < 0.1f) {
            m_Velocity *= 0.9f;
        }
        else {
            m_Velocity = Direction * 5.0f;
        }
    }
    m_Pos += (m_Velocity * ElapsedTime);
    m_GravityVelocity += m_Gravity * ElapsedTime;
    m_Pos += m_GravityVelocity * ElapsedTime;
    if (m_Pos.y <= m_BaseY) {
        m_Pos.y = m_BaseY;
        m_GravityVelocity = Vec3(0, 0, 0);
        m_JumpLock = false;
    }
}
 赤くなっているところは、コントローラとカメラの方向から、プレイヤーが向かうべき方向をGetMoveVector()で取り出します。コントローラが離された場合、速度は下がらないと変なので、その処理も加味しています。
 Aボタンでジャンプ処理もしています。

プレイヤー(SphereObject)クラスのOnCollision()

 こちらはSphereObject::CollisionWithBoxes()になります。以下が実体です。
void SphereObject::CollisionWithBoxes(const Vec3& BeforePos) {
    //前回のターンからの経過時間を求める
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //衝突判定
    auto ShPtrScene = m_Scene.lock();
    for (auto& v : ShPtrScene->GetBoxVec()) {
        OBB Obb = v->GetOBB();
        SPHERE Sp = GetSPHERE();
        Sp.m_Center = BeforePos;
        float HitTime;
        //相手の速度
        Vec3 DestVelocity(0, 0, 0);
        auto MovBoxPtr = dynamic_pointer_cast<MoveBoxObject>(v);
        if (MovBoxPtr) {
            DestVelocity = MovBoxPtr->GetPosition() - MovBoxPtr->GetBeforePos();
            Obb.m_Center = MovBoxPtr->GetBeforePos();
        }
        Vec3 SrcVelocity = m_Pos - BeforePos;

        Vec3 CollisionVelosity = (SrcVelocity - DestVelocity) / ElapsedTime;
        if (HitTest::CollisionTestSphereObb(Sp, CollisionVelosity, Obb, 0, ElapsedTime, HitTime)) {
            m_JumpLock = false;
            m_Pos = BeforePos + CollisionVelosity * HitTime;
            float SpanTime = ElapsedTime - HitTime;
            //m_Posが動いたのでSPHEREを再取得
            Sp = GetSPHERE();
            Vec3 HitPoint;
            //最近接点を得るための判定
            HitTest::SPHERE_OBB(Sp, Obb, HitPoint);
            //衝突法線をHitPointとm_Posから導く
            Vec3 Normal = m_Pos - HitPoint;
            Normal.normalize();
            Vec3 angle(XMVector3AngleBetweenNormals(Normal, Vec3(0, 1, 0)));
            if (angle.x <= 0.01f) {
                //平面の上
                m_GravityVelocity = Vec3(0, 0, 0);
            }
            else {
                //重力をスライドさせて設定する
                //これで、斜めのボックスを滑り落ちるようになる
                m_GravityVelocity = ProjUtil::Slide(m_GravityVelocity, Normal);
            }
            if (MovBoxPtr) {
                //お互いに反発する
                Vec3 TgtVelo = CollisionVelosity * 0.5f;
                if (TgtVelo.length() < 1.0f) {
                    //衝突時の速度が小さかったら、速度を作り出す
                    TgtVelo = MovBoxPtr->GetPosition() - m_Pos;
                    TgtVelo.normalize();
                    TgtVelo *= 2.0f;
                }
                Vec3 DestVelo(XMVector3Reflect(-TgtVelo, Normal));
                DestVelo.y = 0;
                MovBoxPtr->SetVelocity(DestVelo);
                //速度を反発させて設定する
                m_Velocity = XMVector3Reflect(TgtVelo, -Normal);
            }
            else {
                //速度をスライドさせて設定する
                m_Velocity = ProjUtil::Slide(m_Velocity, Normal);
            }
            //Y方向は重力に任せる
            m_Velocity.y = 0;
            //最後に衝突点から余った時間分だけ新しい値で移動させる
            m_Pos = m_Pos + m_Velocity * SpanTime;
            m_Pos = m_Pos + m_GravityVelocity * SpanTime;
            //もう一度衝突判定
            //m_Posが動いたのでSPHEREを再取得
            Sp = GetSPHERE();
            if (HitTest::SPHERE_OBB(Sp, Obb, HitPoint)) {
                //衝突していたら追い出し処理
                Vec3 EscapeNormal = Sp.m_Center - HitPoint;
                EscapeNormal.normalize();
                m_Pos = HitPoint + EscapeNormal * Sp.m_Radius;
            }
        }
    }
}
 ここでは、ボックスの配列をスキャンしながら、SPHEREとOBBの判定を行ってます。
 CollisionTestSphereObb()関数を呼び出す前に、もし、相手がMoveBoxObjectだった場合に、相手の速度を取得して、自分の速度から引き算します。すると、相手との相対速度が出るので、それでCollisionTestSphereObb()関数を呼びます。この関数は、衝突した場所などを返すのではなく1ターン内の衝突した時間を返すので、引き算して相手の動きを止めた形にしても結果は同じになります。
 衝突後の処理ですが、動かないブックスの場合はスライドして動くボックスの場合は反発にしています。そのとき、相手も少し反発させてます。(赤くなっている部分です)
 また、追い出し処理ですが、OBB対OBBよりはいくらか単純になります。

プレイヤー(SphereObject)クラスのOnRotation()

 こちらは、MoveBoxObjectクラスと変わりありません。

■まとめ■

 この項では、シンプルバージョンにおける3Dオブジェクトの実装を説明しました。かなり詳しく説明したつもりです。
 ただ冒頭にも述べました通り、実装の方法は無限にあり、このサンプルは一つの方法でしかないということと、ゲームを形成する実装としては、ほかにも必要な機能が山ほどある、ということです。
 そういいう意味でもしっかりと時間を確保してプログラムの学習に励んでいただきたいと思います。