ホーム < ゲームつくろー! < DirectX技術編 < Xファイルにカスタムテンプレートデータを保存してみよう!

その33 Xファイルにカスタムテンプレートデータを保存してみよう!


 その32で、Xファイルのカスタムテンプレートからデータを読み込む方法を見てきました。面倒なロード作業を統一した方法ででき、またバイナリデータの読み込みも担ってくれる機能は素晴らしいものです。しかし、厳格なフォーマットに従ってデータを作成するのもこれまた面倒な作業であることは間違いありません。そのため、DirectXには自分が作成したテンプレートに合わせてXファイルを自動生成してくれるインターフェイス群がちゃんと存在しています。
 この章では、それらインターフェイスを使ってカスタムテンプレートに従ってデータをXファイルフォーマットで保存する方法を見ていきましょう。



@ 保存作業自体は簡単です

 保存作業の取っ掛かりは、ロード時と同じIDirectXFileオブジェクトの生成から始まります。これは、DirectXFileCreate関数を用います。

DirectXFileCreate関数からIDirectXFileインターフェイスを取得
IDirectXFile *pDXFile;
DirectXFileCrate( &pDXFile );

インターフェイスを取得したら、保存する型であるテンプレートを登録します。これもロード時と同じですね。

IDirectXFile::RegisterTemplates関数
pDXFile->RegistTemplats( g_pTEMPLATE_PERSONID, strlen( g_pTEMPLATE_PERSONID ) );

第1引数にテンプレートを定義した文字列を渡します。第2引数はその文字列の長さです(詳しくはその32をご覧下さい)。これが成功すれば下準備はおしまいです。

 次に生成するのはIDirectXFileSaveObjectインターフェイスです。このインターフェイスは保存ファイルとの連係を取ると共に、実際のデータを登録するIDirectXFileDataインターフェイスを生成します。まずは、IDirectXFileSaveObjectですが、これはIDirectFile::CreateSaveObject関数を用います。

IDirectXFile::CreateSaveObject関数
HRESULT CreateSaveObject(
    LPCSTR szFileName,
   DXFILEFORMAT dwFileFormat,
   LPDIRECTXFILESAVEOBJECT* ppSaveObj
);

szFileNameには保存するファイル名を指定します。何でも結構です。
dwFileFormatには保存形式をフラグで指定します。保存形式には「テキスト(DXFILEFORMAT_TEXT)」と「バイナリ(DXFILEFORMAT_BINARY)」があります。また「圧縮(DXFILEFORMAT_COMPRESSED)」するかどうかも設定できます。テキストファイルで圧縮形式にするならば、DXFILEFORMAT_TEXT | DXFILEFORMAT_COMPRESSEDと論理和で設定します。圧縮してバイナリにすると、もう何だか良くわからないものになりまして、安全性が向上します。
ppSaveObjにIDirectXFileSaveObjectインターフェイスへのポインタが返ります。

 取得できたIDirectXFileSaveObjectインターフェイスから、さらに実際にセーブ作業を行うIDirectXFileDataインターフェイスを取得します。これにはIDirectXFileSaveObject::CreateDataObject関数を用います。実は、この関数の使い方が今回の最大の山場なんです。

IDirectXFileSaveObject::CreateDataObject関数
HRESULT CreateDataObject(
    REFGUID rguidTemplate,
    LPCSTR szName,
    const GUID *pguid,
    DWORD cbSize,
    LPVOID pvData,  
    LPDIRECTXFILEDATA *ppDataObj
);

rguidTemplateには保存するテンプレートに付けられているGUIDを渡します。
szNameは保存するテンプレートデータの名前です。テンプレートの名前ではありませんので注意してください。ここにNULLを入れると、名前の無いデータになります。
pguidにはデータオブジェクトに定義付けられているGUIDへのポインタを渡します。そういうデータオブジェクトが無い場合はNULLでかまいません。
cbSizeには保存するデータのサイズを指定します。これは、テンプレートに定義されている変数(必須メンバ)全てのデータサイズです。1つ1つ個別にセーブすることはできません。これが肝なんです。
pvDataには必須メンバ全てがその順番で格納されているバッファへのポインタを渡します。このバッファには、cbSizeだけデータがずらっと並んでいる事になります。文字列も、整数も少数も全部です。これについて考えるのが実はこの章の中心です。
ppDataObjにIDirectXFileDataオブジェクトへのポインタが返ります。

 この関数の第4引数及び第5引数には、テンプレートに収める全てのデータのサイズとデータ列を渡します。データ列は一列になっていなければなりません。今は、これがある物として先に話を進めます。

 正しくデータを格納できたら、ppDataObjにIDirectXFileDataインターフェイスが取得できています。後はIDirectXFileSaveObject::SaveData関数を用いてファイルに書き込むだけです。

IDirectXFileSaveObject::SaveData関数
HRESULT SaveData(      
    LPDIRECTXFILEDATA pDataObj
);

これで、Xファイル形式に従ったファイルが生成されています。あの面倒なファイルを自動生成してくれるわけですから、感動ものです。もちろん、生成されたファイルはロードすることが出来ます。セーブとロードが揃えば、ゲーム製作もぐっと進歩することになりますね。



A データを一列に並べて保持するCMemoryStockerクラス

 このセーブの肝はなんと言っても、セーブするためのデータをメモリブロックにずらっとシーケンシャルに並べる作業です。それこそ、整数から少数から配列から何から何まで並べます。セーブ時にはセーブすべきデータは全部わかっていますから、そのサイズも自ずと固定されるとは言え、確保したメモリブロックに対してポインタ演算をしながらmwmcpy関数でコピーしていく作業は実に厳しいものがあります。

 もし、次のような格納方法があったら、とても楽だとは思いませんか?

Buffer << &pName << Data.Position.x << Data.Position.y << Data.Position.z << ...

STLのostreamやMFCのCArchiveクラスにそっくりなんですが、これによってBufferというオブジェクトの中にデータがどんどん追加されて、長いデータ列を自動的に形成します。ついでにサイズも計算してくれていて、セーブ後にオブジェクトが無くなったら確保したメモリもさっくりと消してくれる。こんなクラスをここでは大胆にも作ってしまいます!

 作成するクラスの名前はCMemoryStockerクラスとしましょう。このクラスは「<<」演算子をオーバーロードして、右辺にあるデータを連続的に格納していきます。内部では不定長にメモリを伸ばしていく作業を行います。上記のように連続した演算子の使用をサポートするためには、ちょっとした工夫が必要です。これは、実装を見て頂いた方が早いでしょう。こうなります。

CMemoryStocker& operator <<( int &val)
{
   AddData( (void*)&val, sizeof( int ) );
   return *this;
}

<<演算子をオーバーロードし、その戻り値としてCMemoryStockerオブジェクトのアドレスを返すようにします。内部で引数のデータの格納を終えたら、自分自身のポインタを返すようにします。この実装をしますと、上の例で、

Buffer << &pName << Data.Position.x << Data.Position.y << Data.Position.z << ...

まずこの太文字の部分が最初に処理されて、戻り値であるBuffer自身と置き換わります。

Buffer << Data.Position.x << Data.Position.y << Data.Position.z << ...

この処理が繰り返され、最後まで全部格納して終わることになります。<<演算子の右辺に基本型(少なくともXファイルフォーマットにある型)を全部取るようにしてしまえば、Xファイル形式の保存は実質変数の正しい列挙で終わってしまいます。

 CMemoryStocker::AddData関数は、第1引数のポインタの先にあるデータを第2引数分だけ内部のメモリの先に付け足す関数です。この関数の実装はこうなります。

CMemoryStocker& AddData( void* dataptr, size_t size ){
   // 残量メモリをチェックし、足りなければ拡張
   size_t tmpRemain = m_Remain;    // 確保した残りのメモリ量
   int cnt = 0;
   while( tmpRemain < size ){   // 付け足そうとするサイズが足りなければ拡張を検討する
      tmpRemain += m_Unit;   // m_Unitは拡張単位です
      cnt++;
   }

   // カウント数分だけメモリを拡張
   BYTE *p = (BYTE*)realloc( m_pBuf, m_FullSize+cnt*m_Unit );   // m_FullSizeは現在の確保量です
   if(!p) return *this; // 確保失敗

   // 増加部分を初期化
   // p+mFulSizeでポインタの最後尾、そこから確保分だけメモリを初期化
   ZeroMemory( p+m_FullSize, cnt*m_Unit);

   // 情報を格納
   memcpy( p+m_Pos, dataptr, size );

   // メモリ情報を更新
   m_Pos+=size; // 現在位置を更新
   m_FullSize += cnt*m_Unit;
   m_Size += size;                  // 現在の書き込み量です
   m_Remain = m_FullSize - m_Size;  // 残量です

   m_pBuf = p; // ポインタを更新

   return *this;
};


 ポイントは「既存のメモリの後ろにどのようにデータを追加するか」です。そういう目線でプログラムをご覧下さい。
 m_pBufという変数には確保しているメモリの先頭アドレスが格納されています。確保量はm_FullSize、書き込み量はm_Size、そして残量はm_Remainとして計算されています。これから書き込もうとする情報のサイズが現在の残量よりも多い場合は、メモリ領域を拡張する必要があります。whileループでその拡張量を計算し、realloc関数によってメモリ拡張を行っています。今回はこの拡張作業が必須であるため、通常のnewではなくて低レベル関数であるmallocやrealloc関数を用いないといけません。あとは拡張部分に情報をコピーして、残量や増量を再計算して関数を抜けます。

 この関数の戻り値はCMemoryStockerのアドレスで自分自身を返しています。ということは、この関数を次のようにも利用できます。

Buffer << &pName << Burrer.AddData( pPos, sizeof(D3DXVECTOR3) ) << ...

サイズが分かっている構造体などをごっそりと保存するわけです。つまり、この関数は「マニピュレータ」にもなるわけです。こういうことが出来るということは、Xファイルだけでなく独自のセーブ機構などにもこのクラスは利用できます。



B 文字列はポインタで

 保存する時にはまりやすいのが文字列の扱いです。IDirectXFileSaveObjectは文字列のポインタを辿って、その先にある文字列(ナル文字付き)を読み取ります。ですから、保存情報に「文字列そのもの」を置いてはいけません。文字列へのポインタを置かなければならないんです。

○ これはエラー
12345 "This is a Test." 0.55 25

○ これが正解 (0x0032a452に文字列がある)
12345 0x0032a452 0.55 25

CMemoryStockerクラスの<<演算子でこれをサポートしても良いですし、AddData関数を直接呼び出して文字列へのポインタ自体を格納するようにしても良いでしょう。



 Xファイルでの読み書きに一貫性を持たせれば、ゲーム製作がかなり楽になってきます。また、Xファイルは圧縮バイナリをサポートしますので、データをさらけ出す危険性が減ります。何よりも、フォーマットの解析という一番面倒な部分を殆ど隠蔽してくれているのが嬉しいですね。この章の実装例については、サンプルプログラムで公開いたしますので、振るってコピペして試してみて下さい(^-^)