<戻る

DirectX用アプリケーションクラスとウィンドウクラスの作成

 DirectXはWindows上で動きます。しかし、Windowsのメッセージを直接処理して動いていません。Windows側のメッセージ処理等のクラス化については、Windows API TIPs編の「WinMain関数からメインウィンドウハンドルを取得するまでのおさらい」でさらっと触れました。ここではDirectX側の作業環境を整える話をします。


 やりたい事は、Windows側の仕事はそれ用のクラスに任せてしまって、それ以外のDirectXの仕事を集中的にこなせるクラスおよびフレームワークを作成する事です。クラス図を描くと次のようなイメージでしょうか。


 一つずつクラスを説明します。
 CApplicationBaseクラスはメッセージループなどアプリケーション全体の流れを制御する基底クラスです。ウィンドウを作成するCWindowBaseクラスを集約しています。CDXAppBaseクラスはCApplicationBaseクラスから派生し、DirectX用のアプリケーションの基底クラスになります。このクラスはDirectX用のゲーム作成の基底となるCDXBaseクラスを集約しています。CWindowBaseクラスはDirectX用のウィンドウを作成します。CDXBaseクラスはCApplicationからhInstanceを、CWindowBaseからhWndを貰い、独自にDirectXの動作をします。多重度に関しては人によって色々違うと思いますが、ここでは上のように考えました。

 このクラス図全体がDirectXを動かすためのフレームワークになっています。次よりそれぞれのクラスの具体的な実装を説明していきます。


@ CApplicationBaseクラスの構築

 CApplicationBaseクラスは、ウィンドウクラスの登録、メインウィンドウの作成、メッセージループを担当しアプリケーション全体を制御するクラスです。上のクラス図を見て分かるようにCWindowBaseクラスを集約しています。このクラスは唯一WinMain関数で実体が定義されます。WinMain関数に書く事は次の2行です。

// アプリケーションクラスのヘッダー宣言
// このヘッダー内でpAppというCApplicationBaseクラスへの
// グローバルなポインタ変数が宣言されています。
#include "ApplicationBase.h"

WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCommandLine, int cmdShow)
}
CApplicationBase tmpApp;   // 実体を定義するとpAppにこれ自身へのポインタが格納
pApp->Run(hInstance);   // アプリケーション開始
}

 アプリケーションオブジェクトを生成し、ただ実行するだけのシンプルな実装です。WinMain関数はこの位シンプルにしたいのです。「初期化とかその他とかは?」となりますが、最低限の初期化はコンストラクタで、それ以外はCApplication::Run関数内で行います。
 CApplicationBase::Run関数の中では「ウィンドウの登録、ウィンドウハンドルの取得、ウィンドウハンドル取得後の初期化、メッセージループの実行、後片付け」を行います。ウィンドウハンドル取得後の初期化というのは、ウィンドウを必要とするオブジェクトの初期化をするための部分です。CDXBaseクラスの初期化などはここで行う事になるでしょう。メッセージループを抜ければ、オブジェクトは自然破棄されます。後片付けはアプリケーションが終わる時に必要な処理を記述する部分です。

 それぞれのパートは全て仮想関数として定義します。Run関数自体は仮想関数として定義しません。

ウィンドウの登録 RegistWndClass
ウィンドウハンドルの取得 CreateMainWnd
取得後の初期化 Initialize
メッセージループの実行 Loop
後片付け ReleaseApp

Run関数内では上の関数を順番に実行していきます。イメージとしては次のような感じです。

int CApplicationBase::Run(HINSTNACE hInstance)
{
m_hInstance = hInstnace; // インスタンスハンドルの保持
i
f(!RegistWndClass())

// ウィンドウクラスの登録
return F_FAILED;
i
f(!CreateMainWnd())

// メインウィンドウの作成
return F_FAILED;

if(!Initialize())

// アプリケーションの初期化
return F_FAILED;

Loop();

ReleaseApp();

return S_OK;

// メッセージループ

// アプリケーション終了処理

}

 RegistWndClass関数内ではウィンドウクラスの登録を行います。CreateMainWnd関数の中ではメインウィンドウのハンドルを取得しウィンドウを作成します。これらについて詳しくはWindows API Tips編「WinMain関数からメインウィンドウハンドルを手に入れるまでのおさらい」をご覧下さい。

 Initialize関数内でウィンドウハンドル取得後のオブジェクトの初期化を行います。こういう位置に初期化関数があると何かと便利な事が多いです。

 メッセージループはLoop関数内で行います。Loop関数の内部はおおよそ次のような感じです。

int CApplicationBase::Loop()
{
MSG msg;
while(GetMessage(&msg, NULL, 0, 0)){
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}

 メッセージループ部分を仮想関数にする理由は、通常のWindowsアプリケーションの場合、メッセージを取得する関数としてGetMessage関数を用いるのですが、DirectXによるゲームなど常に更新し続けるアプリケーションの場合、ここをPeekMesage関数に切り替える必要があるためです。GetMessage関数は関数の内部でメッセージが飛び込んでくるのを待ってしまい、whileループが止まってしまいます。PeekMesage関数は、メッセージが無かった場合は直ちに関数から抜け出てくれます。
 「じゃ、最初からPeekMessage関数にすればいいのでは?」と思ってしまいますが、PeekMesage関数は直ぐに抜け出てしまうので、これはwhile関数の無限ループをするのと同じ処理になります。よって、メッセージが無く、何もしていなくてもCPUの使用率は100%になってしまいます。これでは他のアプリケーションに多大な付加を与えてしまいます。よって、「ゲーム用のウィンドウを作成する時にはLoop関数内をPeekMesage関数で書き換える」方が良く、それはLoop関数を仮想関数として定義する事で可能になります。

 ReleaseApp関数はデストラクタとほぼ同じ役目になると思います。デストラクタが終わった後は問答無用でオブジェクトがメモリから無くなりますから、その一歩前で何か作業が出来ると助かる事もあるため用意しました。


CApplicationBaseクラスのヘッダーファイルは次のようになります。

#include <windows.h>
#include "WindowBase.h"
#include "smart_ptr.h"

class CApplicationBase
{
// メンバ変数
privare:
   HINSTANCE  m_hInstance;   // アプリケーションハンドル
   WNDCLASSEX  m_WndClassEx;   // ウィンドウクラス構造体

protected:
   vector<smart_ptr<CWindowBase>>  m_spWndAry;  // ウィンドウクラス配列

// コンストラクタ・デストラクタ
public:
   CApplicationBase();
   virtual ~CApplicationBase();

// メンバ関数
public:
   // 実行
      int  Run(HINSTANCE hinstance);
   // グローバルなウィンドウプロシージャ関数
      static  LRESULT CALLBACK GlobalWindowProc(
                                                     HWND hWnd,
                                                     UINT msg,
                                                      WPARAM wParam,
                                                      LPARAM lParam
                                                     );
   // インスタンスハンドルを取得
      HINSTANCE GetHInstance(){
         return m_hInstance;
      }

   // ウィンドウクラス構造体を取得
      void GetWndClass(WNDCLASSEX* wc){
         *wc = m_WndClassEx;
      }

   // ウィンドウのスマートポインタ配列へのポインタを取得
   // これはGlobalWindowProc関数が用います。
      vector<smart_ptr<CWindowBase> >* GetWndSmPtr(){
         return &m_spWndAry;
      }

// 仮想関数
protected:
   // ウィンドウクラスの登録
      virtual BOOL  RegistWndClass();
   // メインウィンドウの作成
      virtual BOOL  CreateMainWnd();
   // アプリケーション独自の初期化
      virtual BOOL Initialize();
   // メッセージループ
      virtual void  Loop();
   アプリケーションの終了処理
      virtual void  ReleaseApp();
};

 クラス図のCApplicationBaseクラスとCWindowBaseクラスは1対多の関係になっているので、m_spWndAry配列となっています。 CApplicationBase::Intialize関数、CApplicationBase::ReleaseApp関数の実装部分は空っぽにします。Loop関数の実装は先ほど示したようなデフォルトのメッセージループにします。ウィンドウクラスの登録とメインウィンドウの作成はWindows API Tips編「WinMain関数からメインウィンドウハンドルを手に入れるまでのおさらい」をご覧下さい。
 実装(ApplicationBase.cpp)内では、コンストラクタ内に次のような定義をします。

// グローバルでアプリケーションクラスへアクセス可能にする
CApplicationBase *pApp = NULL;

CApplicationBase::CApplicationBase()
{
   pApp = this;
}


 これはアプリケーションクラスの実体が宣言された瞬間に、その実体へのポインタがグローバル変数であるpAppに格納されるように仕込んでいます。いったい何だと思うかもしれません。静的に宣言されたCApplicationBase::GlobalWindowProc関数の内部からは、ウィンドウクラスを通してメッセージを分配する必要があります。そのためには、アプリケーションクラスにグローバルでアクセスしてGetWndSmPtr関数を呼ぶ必要があります。pAppはそのためにあるのです。
 CApplicationBase::GlobalWindowProc関数の実装は次の通りです。

LRESULT CALLBACK CApplicationBase::GlobalWindowProc(
                                                     HWND hWnd,
                                                     UINT msg,
                                                      WPARAM wParam,
                                                      LPARAM lParam
)
{
   // アプリケーションクラスからCWindowBaseオブジェクト配列へのポインタを取得
   if(pApp){
      vector<smart_ptr<CWindowBase> > *tmp = pApp->GetWndSmPtr();
      // ウィンドウハンドルを比較し、
      // 一致したウィンドウクラスのローカルウィンドウプロシージャを呼び出す
      int size = tmp->size();
      for(int i=0; i<size; i++){
         if((*tmp)[i].GetPtr()->HWND() == hwnd)
            return (*tmp)[i]..GetPtr()->LocalWindowProc(hWnd, msg, wParam, lParam);
      }
   }

   return L0;  // 正常終了を返す
}


 pAppを通してCWindowBaseポインタ変数配列を取得し、ウィンドウハンドルを比較して、一致したCWindowBaseオブジェクトのローカルウィンドウプロシージャ関数を呼び出して、メッセージを移譲しています。この方法は最速ではありませんが、殆ど問題ない速度で動きます(DirectXの速度にも殆ど影響しません)。この実装とほぼ同様の実装は参照文献「アドベンチャーゲームプログラミング」及び「ロールプレイングゲームプログラミング」にも掲載されております。

 他のメンバ関数の実装については、だいたい説明してありますので実装は省略します。

 


A CWndBaseクラスの構築

 このクラスはウィンドウを作成し、ウィンドウハンドルを保持します。またローカルウィンドウプロシージャ関数(以下LWP関数)を持ちます。LWP関数はアプリケーションがただ1つだけ持つグローバルウィンドウプロシージャ関数(以下GWP関数)からメッセージを渡してもらい、メッセージハンドラ関数でウィンドウに対して様々な行動を起こさせます。

 ここで述べておきたい事はCWndBase::LocalWindowProc関数の実装部分です。ウィンドウプロシージャで何をするか分からないからといって空っぽにしておくと、アプリケーションを終了させる事が出来ません。ですから、最低限WM_CLOSEに関する動作だけは定義しておきます。

// ローカルウィンドウプロシージャ
LRESULT CWndBase::LocalWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
   // デフォルトでは終了のみ
   switch(msg)
   {
   case WM_CLOSE: // ウィンドウが閉じられた
      PostQuitMessage(0);  // 終了宣言
      break;
   default:
      return DefWindowProc(hWnd, msg, wParam, lParam);
   break;
   }

   return 0;
}


 このクラスはウィンドウを作成しウィンドウハンドルを取得するCreate関数を持っています。

// ウィンドウハンドルの取得
BOOL CWndBase::Create(
  HINSTANCE hInstance,   // インスタンスハンドル
  const char *classname   //  ウィンドウクラスの名前
)
{
   // 引数の登録
   m_cs.hInstance = hInstance;
   strcpy(&m_cs.lpszClass, classname, strlen(classname));

   // CREATESTRUCT構造体の変更
   PreCreate(&m_cs);

   // ウィンドウハンドルの取得
   m_hWnd = CreateWindowEx(
      m_cs.dwExStyle,
      m_cs.lpszName,
      m_cs.lpszName,
      m_cs.style,
      m_cs.x,
      m_cs.y,
      m_cs.cx,
      m_cs.cy,
      m_cs.hwndParent,
      m_cs.hMenu,
      m_cs.hInstance,
      m_cs.lpCreateParams
   );

   if(!m_hWnd)
      return FALSE;

   return TRUE;
}


 PreCreateという関数は、CREATESTRUCT構造体(ウィンドウ作成に必要な情報を格納する構造体)に設定されているデフォルト値を変更する時に使用します。構造体が参照渡しされているので、関数内で構造体の中身を変えると呼び出し側にも反映されます。後はウィンドウを作成し、成功すればTUREが返ります。


B CDXBaseクラスの構築

 CDXBaseクラスはDirectXを用いたゲームの根っこ、大本にあたるクラスです。このクラスを派生させてゲームの根幹となる部分を作ります。
 このクラスの持つメンバ関数はInit関数、Release関数、そしてStep関数です。Init関数はゲームの初期化を行います。Release関数は終了処理です。そしてStep関数はゲームの1ステップで行う事を定義します。

 ゲームによってこのクラスの内部動作は異なるでしょうから、このクラスのメンバ関数はすべて仮想関数として定義されます。

 ヘッダーは次のように定義されます。

class CDXBase
{
public:
   // 引数付きコンストラクタ
   CDXBase(HWND hWnd, HINSTNACE hInstance)
   {
      m_hWnd = hWnd;
      m_hInstnace = hInstance;
   }
   ~CDXBase(){};

protected:
   HWND m_hWnd;   // 画面となるウィンドウハンドル
   HINSTANCE m_hInstance;   // ハンドルインスタンス

public:
   // 初期化
   virtual HRESULT Init() = 0;   // 純粋仮想関数

   // 終了処理
   virtual HRESULT Release(){};

   // ステップ関数
   virtual HRESULT Step() = 0;   // 純粋仮想関数
};

 このクラスは純粋仮想関数が含まれているので、派生を前提としています。ゲームは初期化され、ステップを踏み、そして終了する。そのフレームがこのクラスにすべて含まれています。このクラスの使い方は、次に示すCDXappBaseクラスで説明しましょう。


C CDXAppBaseクラスの構築

 CDXAppBaseクラスはCApplicationBaseクラスの派生クラスです。CDXBaseクラスを集約し、そのStepを実際にループ内で実行するクラスです。
 まずCDXBaseポインタ変数であるm_pDXObjをメンバ変数として定義します。そしてLoop関数をオーバーライドします。

void CDXAppBase::Loop()
{
   MSG msg;
   
   // CDXObjオブジェクトが生成されていない場合はアプリケーション終了
   if(!m_DXObj)
      return;

   // メッセージループ
   // メッセージ処理をしていない時にはCDXObjオブジェクトの動作をさせる。

   while(1){
      if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE){
         if(msg.message == WM_QUIT)
            break;
         TranslateMessage(&msg);
         DispatchMessage(&msg);
      }
      else
      {
        // DirectXによるゲームの1ステップ動作を行う
         m_DXObj->Step();
      }

}

 ループに入る前にオブジェクトが生成されている事をチェックします。PeekMessage関数を用いてメッセージが無い時にはCDXAppBase::Step関数が実行されるようにします。こうすることでウィンドウズのメッセージ処理とDirectXの実行が平行して行われます。


C CDXWndBaseクラスの構築

 最後にDirectX用のウィンドウを管理するCWndBaseからの派生クラスCDXWndBaseクラスを作成します。エスケープキーを押した時にゲームを終了させる処理を加えます。絶対そうしなければならないというものではありませんが、そうしている実装が圧倒的です。実装はCDXWndBase::LocalWindowProc関数に対して行います。

// ウィンドウプロシージャ
LRESULT CDXWndBase::LocalWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
   switch(msg)
   {
   case WM_KEYDOWN: // キーが押された
      if(wParam == VK_ESCAPE){
         PostQuitMessage(0);
      }
      break;
   default:
      CWndBase::LocalWindowProc(hWnd, msg, wParam, lParam);
      break;
   }

   return 0;
}

 注意するのはエスケープキー以外のメッセージについては親クラスのLocalWindowProc関数に移譲する点です(case文のdefault以下)。このような、クラス内で扱わないメッセージへ対処は必ず親クラスに移譲します。


 少し端折り気味ではありますが、これでDirectXを集中して使用する下準備ができました。後はCDXBaseクラスを派生させ、DirectXの処理を集中させる事が出来ます。実際はCDXAppBaseクラスも派生が必要になります。これがわずらわしい場合は、CDXAppBaseクラス自体にCDXBaseの機能を内包させてしまうのも良いかもしれません。ただ、クラスは大きくしすぎると大抵困った事になりますし、再利用性が下がりますから、その粒度はほどほどに抑えた方が良いでしょう。
 この章のクラスについての完全な実装は余裕が出来ましたら本サイトに掲載したいと思います。