Mission5: 敵キャラの表示。


【ステップ1】

 今度は、Mission4 の続きである。
 典型的(と思われる)敵キャラ制作を行ってみる。
 現状はプログラムの簡易さに主眼を置いているので、
 今後の mission で本格的な改造が必要になるであろう。

 まぁとりあえず始めよう。
 VisualC++ の画面は、以下のようになっているはずである。

【Misson3 画面】
図1 Mission3画面


【ステップ2 敵キャラクラスの設計】

プロジェクトを改造して行く。
敵キャラクラスに必要な処理は、以下の様であろう。

表1:キャラクラス
番号種類説明・注意点
コンストラクタ他の処理を呼んでも安全な様に準備しておく。
デストラクタ全ての後始末をできるようにする。
デバイス依存メモリオブジェクト生成InitDeviceObjectsから呼び出される処理。
デバイス依存デバイスオブジェクト生成RestoreDeviceObjectsから呼び出される処理。
デバイス依存デバイスオブジェクト消去InvalidateDeviceObjectsから呼び出される処理。
デバイス依存メモリオブジェクト消去DeleteDeviceObjectsから呼び出される処理。
フレーム毎処理アニメーション処理を行う。
レンダリング処理レンダリングを行う。


クラス設計のポリシーは、
できる限り、どんな呼ばれ方をしても暴走しない・・・・。
である。

なぜなら、ゲームの場合、キャラの作成者と、キャラの処理を呼び出すプログラムの作成者が 別人である事が多いので、特にキャラ側のプログラム呼び出しに必要な条件のハードルが高いと 致命的なバグ要因となるからである。

そのため、 が求められる。

ちなみに、表1の「番号」というのは、以下説明をして行く為の便宜的なものである。


【ステップ3 敵キャラクラスの作成】

では実際に作って行こう。以下の指令に従うべし。


【ステップ3−1 クラスの挿入 Part1】
解説
「挿入」メニューより、「クラスの新規作成」
を選択する。
図2 新規クラスの作成


【ステップ3−2 クラスの挿入 Part2】
解説
すると、左図のようなウインドウが現れた
はずである。

とりあえず、以下の設定で「OK」ボタンを
クリックする。

クラスの種類:
"Generic クラス"

クラスの名前:
"CMeshTest"

図3 新規クラスを設定


【ステップ3−3 新規クラスの編集 Part1】
解説
ここで、ワークスペースを見ると、
左図のように、新しいファイル、

"MeshTest.cpp","MeshTest.h"

が出てきたはずだ。

これは、クラス「CMeshTest」を記述する
ファイルである。

ファイル名とクラス名の関連は
説明しなくても判りますね?

ここでは、"MeshTest.h"を
ダブルクリックして下さい。
図4 新規クラスを設定


【ステップ3−4 新規クラスの編集 Part2】
解説
そうすると、左のリストのように、
クラス宣言を行っている部分が出てくるはずだ。

これを、リスト2のように書き換える。
リスト1 CMeshTest 宣言


class CMeshTest  
{
public:
        CMeshTest();                                                            // 処理1
        virtual ~CMeshTest();                                                   // 処理2
        HRESULT InitDeviceObjects(LPDIRECT3DDEVICE8 lpd3ddev);                  // 処理3
        HRESULT RestoreDeviceObjects(LPDIRECT3DDEVICE8 lpd3ddev);               // 処理4
        HRESULT InvalidateDeviceObjects();                                      // 処理5
        HRESULT DeleteDeviceObjects();                                          // 処理6
        void    Update(float timeElapsed);                                      // 処理7
        void    Render(LPDIRECT3DDEVICE8 lpd3ddev);                             // 処理8
private:
        HRESULT GenerateEnhancedMesh( LPDIRECT3DDEVICE8 lpd3ddev, UINT dwNewNumSegs ); // 内部処理

        TCHAR                   m_strMeshPath[512]; // メッシュが存在するディレクトリ名を保存する。
        LPD3DXMESH              m_pMeshSysMem;      // メモリ上のメッシュへのポインタ
        LPD3DXMESH              m_pMeshEnhanced;    // (可能ならば)ビデオメモリ上のメッシュへのポインタ
        D3DXMATERIAL*           m_pMaterials;       // マテリアル(質感)配列へのポインタ
        LPDIRECT3DTEXTURE8*     m_ppTextures;       // テクスチャー配列へのポインタ
        DWORD                   m_dwNumMaterials;   // マテリアル(質感)配列の要素数
        LPD3DXBUFFER            m_pbufMaterials;    // マテリアル(質感)配列データオブジェクト(データ本体はビデオメモリかも)
        D3DXVECTOR3             m_vObjectCenter;    // メッシュの重心点
        FLOAT                   m_fObjectRadius;    // メッシュを囲む球体の半径
        BOOL                    m_bUseHWNPatches;
        UINT                    m_dwNumSegs;        // NPatch アルゴリズム(表面が滑らかになる)を使用する際の分割数
        D3DXMATRIX              m_matWorld;
        float                   m_fAngleY;
};
リスト2 クラス宣言の改造

【ステップ3−5 新規クラスの編集 Part3】
解説
今度は、MeshTest.cpp をダブルクリック
するとまずは、リスト3の部分を書き換える。


図5 MeshTest.cpp を開く


解説

左の3行の中の、"stdafx.h" と "mission1.h" の間に、

#include <d3d8.h>
#include <d3dx8.h>
#include <GameMain.h>

以上3行を書き加え、リスト4のようにする。

リスト3 ヘッダファイルのインクルード

リスト4 ヘッダファイルの追加インクルード

【ステップ3−6 新規クラスの編集 Part4】
解説
次にこの部分、
これは、VisualC++ が作ってくれたプログラム・・・
といっても空の関数2つ。
次に、ここに手をつける。

リスト5 コンストラクタ・デストラクタ


【ステップ3−7 処理1・コンストラクタの制作】
解説
CMeshTest::CMeshTest()
{
    GetAppPathName(m_strMeshPath);
    m_dwNumMaterials = 0;
    m_pbufMaterials = NULL;
    m_pMaterials = NULL;
    m_pMeshEnhanced = NULL;
    m_pMeshSysMem = NULL;
    m_ppTextures = NULL;
    m_bUseHWNPatches = FALSE;
    m_dwNumSegs = 2;
    m_fAngleY = 0.0f;
}
左のように、コンストラクタを改造する
基本的には、ステップ3−4で宣言した
メンバ変数を初期化しているだけである。

ただし、
GetAppPathName(・・・);
は関数呼び出しである。

この関数は、現在作成中のアプリケーションが起動した時
アプリケーションのファイルのあった位置(ファイルパス)
を取得しておくものである。

この関数を、リスト7に示す。
リスト6 コンストラクタ



static void GetAppPathName(TCHAR *strCmdPath)
{
int l,i;
TCHAR *str = strCmdPath,*c;


// まずは変数宣言
lstrcpy( str, ::GetCommandLine());
// 起動したアプリケーションの絶対パス文字列を取得。
l = strlen(str);
if (str[0]=='\"'){
for(i = 0; i < l; ++i){
str[i] = str[i+1];
}
--l;
}
// " で始まっていたら、一文字つめる。
c = strrchr(str,'\\');
if (c){
*c=0;
}else{
strCmdPath[0]=0;
}
// 最後に\が出てくる所以降を削除
}
リスト7 アプリケーションパスの取得


【ステップ3−8 処理2・デストラクタの制作】
解説
CMeshTest::~CMeshTest()
{
	this->InvalidateDeviceObjects();
	this->DeleteDeviceObjects();
}
ここでは、このクラス内の後述する関数。
  • InvalidateDeviceObjects();
  • DeleteDeviceObjects();
を呼び出して、このクラスが抱える全てのオブジェクトを廃棄している。
リスト8 デストラクタ



【ステップ3−9 処理3・デバイス依存メモリオブジェクト生成】
解説
HRESULT	CMeshTest::InitDeviceObjects(LPDIRECT3DDEVICE8 lpd3ddev)
{
    LPDIRECT3DVERTEXBUFFER8 pVB = NULL;
    BYTE*      pVertices = NULL;
    LPD3DXMESH pTempMesh;
    HRESULT    hr;
    TCHAR    strMeshFilename[512];
    InvalidateDeviceObjects();
    DeleteDeviceObjects();
// この処理が再度呼ばれても良いように、
// 廃棄してから初期化。
    _tcscpy(strMeshFilename,m_strMeshPath);
    _tcscat(strMeshFilename,"\\");
    _tcscat(strMeshFilename,"tiger.x");
    hr = D3DXLoadMeshFromX( strMeshFilename, D3DXMESH_SYSTEMMEM, lpd3ddev, 
        NULL, &m_pbufMaterials, &m_dwNumMaterials, 
        &m_pMeshSysMem );
    if (FAILED(hr)){
        return    hr;
    }
// X ファイル tiger.x をロード
    // vertex buffer を読み出す為にロックする。
    hr = m_pMeshSysMem->GetVertexBuffer( &pVB );
    if( FAILED(hr) )
        return hr;
    hr = pVB->Lock( 0, 0, &pVertices, 0 );
    if( FAILED(hr) )
    {
        if ( pVB != NULL){ pVB->Release();	pVB=NULL;	}
        return hr;
    }
    hr = D3DXComputeBoundingSphere( pVertices, m_pMeshSysMem->GetNumVertices(), 
                                    m_pMeshSysMem->GetFVF(), &m_vObjectCenter, 
                                    &m_fObjectRadius );
    pVB->Unlock();
    if ( pVB != NULL){ pVB->Release(); pVB=NULL;    }
    if( FAILED(hr) )
    {
        return hr;
    }
// 中心点(m_vObjectCenter)の算出
// メッシュを囲む球形の半径の算出。
    if( 0 == m_dwNumMaterials )
    {
        return E_FAIL;
    }
// テクスチャー無しはサポートしない。
    m_pMaterials = (D3DXMATERIAL*)m_pbufMaterials->GetBufferPointer();
    m_ppTextures = new LPDIRECT3DTEXTURE8[m_dwNumMaterials];
        ZeroMemory(m_ppTextures,sizeof(LPDIRECT3DTEXTURE8)*m_dwNumMaterials);
    for (DWORD i = 0; i < m_dwNumMaterials ; ++i)
        m_pMaterials[i].MatD3D.Ambient = m_pMaterials[i].MatD3D.Diffuse;

// マテリアル(質感)配列および
// テクスチャ配列の取得
//※(2004/02/09 アンビエント光源対策)
//マテリアルのディフューズ色を
//アンビエント色にコピーしている。
    // 頂点フォーマットの修正
    if( !(m_pMeshSysMem->GetFVF() & D3DFVF_NORMAL) )
    {
        hr = m_pMeshSysMem->CloneMeshFVF( m_pMeshSysMem->GetOptions(), 
                                          m_pMeshSysMem->GetFVF() | D3DFVF_NORMAL, 
                                          lpd3ddev, &pTempMesh );
        if( FAILED(hr) )
            return hr;

        D3DXComputeNormals( pTempMesh );
// 頂点フォーマットに
// 法線ベクトルを追加。
// 頂点フォーマット・法線については、
// DirectX8 ドキュメントを参照の事。
        if ( m_pMeshSysMem != NULL)
            m_pMeshSysMem->Release();
        m_pMeshSysMem = pTempMesh;
    }
    return S_OK;
// 変更後のメッシュに入れ替え。
}
リスト9 デバイス依存メモリオブジェクト生成


【ステップ3−10 処理4・デバイス依存デバイスオブジェクト生成】
解説
HRESULT CMeshTest::RestoreDeviceObjects(LPDIRECT3DDEVICE8 lpd3ddev)
{
    HRESULT    hr;
    D3DCAPS8    d3dCaps;

    InvalidateDeviceObjects();
    if (m_pMeshSysMem == NULL){
        return E_FAIL;
    }

    lpd3ddev->GetDeviceCaps(&d3dCaps);

    for( UINT i=0; i < m_dwNumMaterials; i++ )
    {
        TCHAR tmp[512];
        TCHAR strTexturePath[512] = _T("");
        strcpy( tmp, m_pMaterials[i].pTextureFilename );
        GetFileName(tmp);
        _tcscpy(strTexturePath,m_strMeshPath );
        _tcscat(strTexturePath,"\\");
        _tcscat(strTexturePath,tmp);
        if( FAILED( D3DXCreateTextureFromFile( lpd3ddev, strTexturePath, 
                                               &m_ppTextures[i] ) ) )
            m_ppTextures[i] = NULL;
    }

    m_bUseHWNPatches = (d3dCaps.DevCaps & D3DDEVCAPS_NPATCHES);
    hr = GenerateEnhancedMesh( lpd3ddev, m_dwNumSegs );
    if( FAILED(hr) )
        return hr;
    return S_OK;
}
ここでは、
マテリアル配列の要素ごとに設定されている
テクスチャーの読み込みをした後、
後述の関数、GenerateEnhancedMesh を呼び出している。

GetFileName は、ファイル名からパス部分をカットする。

GenerateEnhancedMesh および GetFileName をリスト11に示す。
リスト10


HRESULT CMeshTest::GenerateEnhancedMesh( LPDIRECT3DDEVICE8 lpd3ddev, UINT dwNewNumSegs )
{
    LPD3DXMESH pMeshEnhancedSysMem = NULL;
    LPD3DXMESH pMeshTemp;
    HRESULT    hr;

    if (m_bUseHWNPatches){
        // ハードウェアがサポートするなら、ポリゴンを分割してビデオカードへ。
        hr = m_pMeshSysMem->CloneMeshFVF( D3DXMESH_WRITEONLY | D3DXMESH_NPATCHES, 
                                m_pMeshSysMem->GetFVF(), lpd3ddev, &pMeshTemp );
        if( FAILED(hr) )
            return hr;
    }else{
        // ハードウェアがサポートするなら、頂点データをビデオカードへ。
        hr = m_pMeshSysMem->CloneMeshFVF( D3DXMESH_WRITEONLY, 
                                 m_pMeshSysMem->GetFVF(), lpd3ddev, &pMeshTemp );
    }
    if (m_pMeshEnhanced != NULL){
        m_pMeshEnhanced->Release();
        m_pMeshEnhanced = NULL;
    }
    
    m_pMeshEnhanced = pMeshTemp;
    m_dwNumSegs     = dwNewNumSegs;
    return S_OK;
}

static void GetFileName(TCHAR *str)
{
    int     l,i;
    char    *c;
    l = strlen(str);
    if ((str[0]=='\"')&&(str[l-1]=='\"')){
        for(i = 0; i < (l - 1) ; ++i){
            str[i] = str[i+1];
        }
        --l;
        str[l] = 0;
    }
    c = strrchr(str,'\\');
    if (c){
        c++;
        i = 0;
        while('\0' != (str[i++] = *c++));
    } 
}
リスト11 GenerateEnhancedMesh 呼び出し


【ステップ3−11 処理5・デバイス依存デバイスオブジェクト消去】
解説
HRESULT CMeshTest::InvalidateDeviceObjects()
{
    if (m_pMeshEnhanced != NULL){
        m_pMeshEnhanced->Release();
        m_pMeshEnhanced = NULL;
    }
    if (m_ppTextures != NULL){
        for( UINT i = 0; i < m_dwNumMaterials; i++ ){
            if (m_ppTextures[i] != NULL){
                m_ppTextures[i]->Release();
                m_ppTextures[i] = NULL;
            }
        }
    }
    return  S_OK;
}
ここでは読み込んだテクスチャーおよび デバイス上の頂点データの 後始末をしている。
ポインターが NULL なら始末はしない。
後始末をすると、ポインターをNULL に している。

これにより、このルーチンは連続して 呼び出されても同じポインタを開放する事は無く 安全である。
リスト12



【ステップ3−12 処理6・デバイス依存メモリオブジェクト消去】
解説
HRESULT CMeshTest::DeleteDeviceObjects()
{
    if (m_ppTextures != NULL){
        delete [] m_ppTextures;
        m_ppTextures = NULL;
    }
    if (m_pMeshSysMem != NULL){
        m_pMeshSysMem->Release();
        m_pMeshSysMem = NULL;
    }
    if (m_pbufMaterials != NULL){
        m_pbufMaterials->Release();
        m_pbufMaterials = NULL;
    }
    m_dwNumMaterials = 0L;
    return  S_OK;
}
ここでは、InitDeviceObject の処理で
準備したものをすべて処分している。
ここでは読み込んだメッシュの 後始末をしている。
ポインターが NULL なら始末はしない。
後始末をすると、ポインターをNULL に している。

これにより、このルーチンは連続して
呼び出されても安全である。
リスト13



【ステップ3−13 処理7・フレーム毎処理】
解説
void    CMeshTest::Update(float timeElapsed)
{
    D3DXMATRIX    matTrans, matRotateY;
    m_fAngleY += timeElapsed * (D3DX_PI / 240.0f);
    if (m_fAngleY > D3DX_PI*2.0f)
        m_fAngleY -= D3DX_PI*2.0f;
    D3DXMatrixRotationY(&matRotateY,m_fAngleY);
    D3DXMatrixTranslation(&matTrans,0.0f,0.0f,1.0f);
    m_matWorld =  matRotateY * matTrans;
}
引数:
timeElapsed: 前にこの処理が呼ばれてからの経過時間

処理内容:
Y軸に沿ってキャラクタを回転させた後、
Z軸の正方向に 1.0 だけ移動させる、
というマトリックス m_matWorldを作成。

この m_matWorld は描画時に頂点データを
世界座標に移動させるのに使用される。
リスト14



【ステップ3−14 処理8・レンダリング処理】
解説
void    CMeshTest::Render(LPDIRECT3DDEVICE8 lpd3ddev)
{
    lpd3ddev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
    lpd3ddev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
    lpd3ddev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
    lpd3ddev->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG2);
    lpd3ddev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

    lpd3ddev->SetRenderState(D3DRS_ALPHABLENDENABLE,FALSE);
    lpd3ddev->SetRenderState(D3DRS_LIGHTING, TRUE );
    lpd3ddev->SetTransform( D3DTS_WORLD, &m_matWorld );
    if (m_bUseHWNPatches){
        float fNumSegs;
        fNumSegs = (float)m_dwNumSegs;
        lpd3ddev->SetRenderState(D3DRS_PATCHSEGMENTS, *((DWORD*)&fNumSegs));
    }
    if (m_pMeshEnhanced){
        for( UINT i = 0; i < m_dwNumMaterials; i++ ){
            lpd3ddev->SetMaterial( &m_pMaterials[i].MatD3D );
            lpd3ddev->SetTexture( 0, m_ppTextures[i] );
            m_pMeshEnhanced->DrawSubset( i );
        }
    }
    if (m_bUseHWNPatches){
        lpd3ddev->SetRenderState(D3DRS_PATCHSEGMENTS, 0);
    }
}
引数に、Direct3Dデバイスへのポインタをとり、
与えられたデバイスに対して、
メッシュをレンダリングする。

メッシュの頂点データは物体座標系で示されており。
このデータについては、書き換えをしていない。

代わりにUpdate() 関数内で作成した m_matWorld
(フレーム毎に計算される。)
を使用して頂点座標を世界座標系へ変換してから
描画する事で、アニメーションを実現している。
といっても、具体的には、

SetTransform(D3DTS_WORLD,&m_matWorld);

として変換行列を登録すれば良いだけ。

カメラの位置は、ここでは設定していない。
GameMain.cpp の中で設定されたものを
そのまま使用している。
リスト15



【ステップ3−15 スタティック関数の宣言】
解説

今回、スタティック関数を2つ作成したので、
(GetAppPathName / GetFileName)
MeshTest.cpp 冒頭部(リスト16)の直後に、
リスト17のように関数の宣言を書いておこう。
リスト16


リスト17


【ステップ3−16 メッシュデータの準備】

最後にメッシュのデータを用意しよう。
幸いDirectX SDK には多くのサンプルが付いているので、とりあえずこれを活用しよう。

DirectX SDK のインストールされたフォルダの下の、
samples\Multimedia\media\の下にある、
tiger.x および tiger.bmp を今回使用する。

図6の要領でコピーされたい。
図6 ファイルのコピー

今度は、これをプログラムを生成しているフォルダ
(Debug/Release)へ貼り付ける。

図7 ファイルの貼り付け


とりあえず、今回はこれで終了。

完成版を、以下に置く。

次回はアプリケーション内でこのクラスを呼び出してみる。

戻る