ホーム < ゲームつくろー! < DirectX技術編

その49 Direct3D用デバッグフォントクラスを作成しよう


 ゲーム作成でデバッグは絶対に絶対に必要です。Visual Studioは非常に強力なデッバグ支援機能を持っています。しかし、実行しながら数値をリアルタイムで表示する事はできません。そういうブレークポイントを設けて毎回ステップするのがあまりにも辛い場合は、画面に数値を表示させてその挙動を観察する事が良くあります。

 Windowsアプリケーションの場合はTextOut関数のような簡易出力機能が有用ですが、DirectXはちょっとその辺りの支援に乏しく思います。

 そこでこの章ではDirectX用のデバッグフォント出力を簡単に行えるクラスを作ってみたいと思います。



@ printf方式かiostream方式か

 簡易にテキストを出力方式としてはprintf関数があります。またファイルなどに出力する方式としてiostream方式もあります。printf方式は「%」で型判断をして画面に数値などを出力できます。一方iostream方式は「<<」などのシフト演算子をオーバーロードして入出力用に使用しています。

 printf方式はかなり直感的に出力できますが、書式付文字列の解析が必要になります。iostream方式はソースがやや乱雑になってしまいますが、型チェックを入れることができるので、書式付文字列のような解析は必要なくなります。どちらも一長一短があるのですが、今回の実装では正直あまり面倒な事はしたくありません。となるとiostream方式が楽かなと思いますのでそちらを採用する事にします。



A ID3DXFontを使います

 ID3DXFontは簡易に文字を表示するのにもってこいです。オブジェクトを一度作成してしまえば、使うのはとても簡単です。デバッグ用フォントを表示するのにも重宝します。面倒な面はフォントの作成と数字の扱い、表示位置の指定などです。デバッグ用フォントは別に凝る必要は何もありません。ですから、固定長でデフォルトクオリティーでデフォルトフォントで十分です。細かい事は後ほど考慮します。



B 表示位置は「ブロック単位」で

 フォントの表示位置ですが、これはブロック単位で考えたいと思います。要は固定長フォントサイズを1ブロックとして、左から何個目、右からいくつめに表示するという指定方法にしたいと言う意味です。デバッグ用のフォントを1ピクセル単位で設定する必要は何もありません。逆にブロック単位にすると、表示させる時に楽になります。



C インターフェイスは簡易で

 ではインターフェイスを考えてみます。…と言ってもiostream方式ですから「<<」の実装が主です。メソッドは最小限で使いやすさを目指してみたいと思います。「<<」演算子の右辺には各種数値と文字列が来るだけで一先ずは十分でしょう。また位置を決めるメソッドであるSetPosメソッドなどの設定メソッドは幾つか必要になると思います。



D クラス宣言と演算子の実装

 作成するクラスはCDebugFontクラスとします。クラス宣言と演算子の実装は次のようになります:

namespace IKD
{
// CDebugFont用フラグ
enum _DFP_
{
   DF_draw // 描画指示フラグ
};


// Direct3D用デバッグフォントクラス
class CDebugFont
{
private:
   static TCHAR      c[24];   // 処理用バッファ
   IDirect3DDevice9* pDev;    // 描画デバイス
   ID3DXFont*        pFont;   // フォントオブジェクトポインタ
   D3DXFONT_DESC     D3DFD;   // フォント属性
   int               Block_x; // 表示位置
   int               Block_y;
#if _UNICODE
   wstring           str;     // デバッグ文字列保持バッファ(UNICODE用)
#else
   string            str;     // デバッグ文字列保持バッファ(ANCI用)
#endif

public:
   CDebugFont( IDirect3DDevice9* pdev ) :
      pDev( pdev ),
      pFont( NULL ),
      Block_x( 0 ),
      Block_y( 0 )
   {
      if(pDev){
         // デフォルトフォント作成
         pDev->AddRef();
         D3DFD.Height = 14;
         D3DFD.Width = 7;
         D3DFD.Weight = 500;
         D3DFD.MipLevels = D3DX_DEFAULT;
         D3DFD.Italic = false;
         D3DFD.CharSet = DEFAULT_CHARSET;
         D3DFD.OutputPrecision = OUT_DEFAULT_PRECIS;
         D3DFD.Quality = DEFAULT_QUALITY;
         D3DFD.PitchAndFamily = FIXED_PITCH | FF_MODERN;
         memset( D3DFD.FaceName, 0, sizeof( D3DFD.FaceName ) );

         D3DXCreateFontIndirect( pDev, &D3DFD, &pFont );
      }
   }

   ~CDebugFont(){
      if(pDev) pDev->Release();
      if(pFont) pFont->Release();
   }

   // int型
   CDebugFont& operator <<( int val ){
      _stprintf(c, _T("%d"), val );
      str += c;
      return *this;
   }

   ... 以下続く

}

} // end namespace IKD


 要点だけを抜き出してあります。クラスの内部には描画先のデバイスとフォントオブジェクトを保持しています。これはコンストラクタで確実に渡さなければなりません。コンストラクタ内でフォントを作成していますが、注目はmemset以下です。FaceNameには通常フォントの名前を指定するのですが、上のように空設定にするとデフォルトのフォントが指定されます。プラットフォームにフォントが入っていないとまずいのでこうしています。

 デストラクタでは保持しているオブジェクトを手放します。

 演算子の実装はセオリー通りです。内部では_stprintf関数を用いています。これはUnicodeとANCI文字の両方に対応するsprintfです。ここで引数の数値を文字列に変換して、バッファstrに追加するわけです。他の型についても同様な演算子のオーバーロードすると、「<<」を続ける事で文字列が繋がっていきます。

 肝心の描画部分を次に見てみましょう:

CDebugFont& operator <<( IKD::_DFP_ param ) {
   switch( param )
   {
      case IKD::DF_draw:
      // BeginSceneテスト
      bool IsBegin = false;
      if( FAILED( pDev->BeginScene() ) )
         IsBegin = true; // すでにBeginSceneが呼ばれている

      // 現在蓄えられている文字列を描画
      if( !pFont ) {
         str = _T("");
         return *this;
      }

      RECT R;
      SetRect( &R, 0, 0, 0, 0 );
      pFont->DrawText( NULL, str.c_str(), -1, &R, DT_LEFT | DT_CALCRECT , 0xffffffff );
      OffsetRect( &R, Block_x * D3DFD.Width, Block_y * D3DFD.Height );
      pFont->DrawText( NULL, str.c_str(), -1, &R, DT_LEFT , 0xffffffff );

      // 文字バッファをクリア
      str = _T("");

      if( !IsBegin )
         pDev->EndScene();
   }
   return *this;
}

描画はIKD::_DFP_列挙型を「<<」演算子に置くと行われます。switch内の太文字部分に注目してください。ここではIDirect3DDevice9::BeginSceneを呼んで戻り値をチェックしています。IDirect3DDevice9::BeginSceneは既に呼ばれている場合にのみエラーを返します。ここでエラーになると言う事は、すでに描画プロセスに入っているので、EndSceneを呼ばないように工夫する必要があります。呼ばれていない場合は、ここで描画モードになり、描画したら直ぐにやめます。こうする事で、いつでもどこでもデバッグ文字が出力できます。文字の実際の描画部分は、特に説明する事はありません。ブロック単位(Block_x、Block_y)になっているくらいです。描画終了後、忘れずにデバッグ文字をクリアします。これで使いまわしできるようになります。


 位置を指定するメソッドとしてSetPosメソッドを用意します。内部ではBlock_xとBlock_yに値を代入するだけです。ここで特記したいのは、同じ働きをする演算子を定義する点です:

// 位置設定簡易指定用演算子
// debugfont( 10, 20 )と位置指定できます
CDebugFont& operator ()( int x, int y ) {
   return SetPos( x, y );
}

 「()」演算子はオブジェクトを関数であるかのように振舞わせる事ができます。またこの演算子の戻り値はCDebugFont自体なので、

CDebugFont DF( pDev );
DF(5,8) << "DebugText " << DebugValue << IKD::DF_draw;

のようにDF(5,8)として位置を指定しつつデバッグ文字や値を登録して描画まで行わせる事ができます。ここまですると利便性は高いかなと思います。



E クラス公開します

 今回のDirect3D用のデバッグ出力クラスであるCDebugFontクラスを公開します。ダウンロードはこちらからどうぞ。使い方は上の使用例がすべてを物語っていますので特に示しませんのでご了承くださいませ。



 デバッグ文字をウィンドウアプリケーションのように気楽にできるようになると、実行しながら様子を見るというデバッグ手法が確立します。これが出来ると出来ないとで、ゲームの作りやすさは天地の差がありますので、ご活用下さい。