ホーム < ゲームつくろー! < DirectX技術編 < 仮想コントローラと言う考え方

その2 仮想コントローラという考え方


 実に10ヶ月ぶりになるDirectX技術編DirectInput編の更新です。
 その1で取り上げたDirectInputの実装はDirectInput7でも8でもそれほど難しくはありません。マニュアルに詳しい初期化方法も載っていますし、教本も沢山あります。しかし、初期化してゲームパッドやキーボードの情報を取れるようになった後のお話しは案外等閑な気がします。ゲーム製作がある程度進んで、さぁキャラクタを動かそうかと思った時、「ん?」と思ってしまうのがユーザの入力情報をキャラクタに伝える方法なんです。 

 Windows環境にはゲームパッドやキーボード、マウスなど沢山の入力デバイスがあります。それに対してゲームの仕様では「方向キーと入力ボタン」のようにゲーム内で概念的に使用される「仮想コントローラ」が定義されます。入力デバイスからの入力情報は「仮想コントローラ」を通して適切に値が変換され、キャラクタに伝えられます。この機構はプログラマが作る必要があります。

 この章ではそんな仮想コントローラについて考えてみたいと思います。



@ 仮想コントローラのイメージ

 仮想コントローラはゲーム内で想定するゲームコントローラです。タイピングゲームなどはちょっと形態が異なりますが、一般的なゲームパッドがその実体イメージとなるかと思います。ゲームパッドと言うと、方向ボタンがあって、幾つかの入力ボタンがあります。PS2の標準コントローラには左右2つのアナログコントローラがありますが、PC用に一般的に販売されているゲームパッドの方向ボタンは大抵1つです。入力ボタンの数は本当に様々ですが、4ないし6つくらいが一般的でしょうか。これを想定すると下の図のようになるでしょうか:

 ゲームパッドのボタン、キーボードの1つそしてマウスの左クリックボタンなどの情報は、この仮想ゲームボタンの1つとして集約されます。こうすることで、プログラムは入力デバイスの種類を気にしなくて良くなります。これを模式図で表すと次のような感じです:

 では早速上の機構を実現するインターフェイスを考えてみることにしましょう。



A 仮想ゲームコントローラインターフェイス

 仮想ゲームコントローラインターフェイス(IGameController)は@で示した図のように1つの方向キーと複数の入力ボタンを持っています。入力ボタンの数は多少余裕を持たせて今回は16個まで対応するようにします。これらのボタンは独自の列挙型(GAMECONTROLER_BTN)で識別する事にします:

仮想ゲームコントローラボタン列挙型
////////////////////////////////////
// 仮想ゲームコントローラボタン列挙型
////////////////
enum GAMECONTROLLER_BTN
{
   GCBTN_0,
   GCBTN_1,
   GCBTN_2,
   GCBTN_3,
   GCBTN_4,
   GCBTN_5,
   GCBTN_6,
   GCBTN_7,
   GCBTN_8,
   GCBTN_9,
   GCBTN_10,
   GCBTN_11,
   GCBTN_12,
   GCBTN_13,
   GCBTN_14,
   GCBTN_15,
   GCBTN_DIRECT = 0xff00, // 方向ボタン
   GCBTN_NO = 0xffff
};

 GCBTN_0〜GCBTN_15が入力ボタン、DCBTN_DIRECTが方向ボタンに対応します。どれにも値しない事を示すGCBTN_NOを設けるのも大切です。各ボタンはそれぞれがオブジェクトとして振舞えますから、IGameButtonインターフェイスとしておきます(これについては後述)。

 IGameControllerインターフェイスの役目は、

 ・ 入力デバイスを使えるよう初期化
 ・ デバイスの押し下げ情報を取得更新
 ・ ユーザに仮想ボタンの情報を通知

これだけです。ユーザへ仮想ボタンの情報を知らせる作業を仮想ボタン(IGameButtonインターフェイス)を渡すことで代替するならば、IGameControllerインターフェイスのメソッドは次のように宣言されます:

IGameControllerインターフェイス宣言部
////////////////////////////////////////
// 仮想コントローラインターフェイス
// IGameController
/////////////////
interface IGameController : public IUnknown
{
public:
   // 公開メソッド
   virtual bool Init( void *pdata=NULL, void* pdata2=NULL ) = 0;
   virtual bool GetButton( GAMECONTROLLER_BTN BtnID, ComGameButton& cpButton) = 0;
   virtual bool Update() = 0;
};

 初期化後はGetButtonメソッドで仮想ボタンを取得して諸々の情報取得やボタン機能の変更を行います。シンプルです。



B 仮想ボタン

 IGameControllerインターフェイスから取得できる仮想ボタン(IGameControllerインターフェイス)は1つのボタンについての情報を管理します。仮想ボタンに必要な機能は

 ・ 仮想ボタンの現在の押し下げ情報を通知
 ・ ボタンに対応する入力デバイスの変更
 ・ 使用の有無
 ・ ボタンから得られる値のスケール変換

と言ったところでしょうか。これらに対応するメソッドを持ったインターフェイスの宣言をまとめると次のようになります:

IGameButtonインターフェイス宣言部
////////////////////////////////////////
// ゲームボタンインターフェイス
// IGameButton
/////////////////
interface IGameButton : public IUnknown
{
public:
// 公開メソッド
   virtual D3DXVECTOR3 GetVal( D3DXVECTOR3* grad=NULL ) = 0;
   virtual bool SetConfig(GAMECONTROLLER_ID id) = 0;
   virtual void Enable( GAMECONTROLLER_TYPE type, bool flag ) = 0;
   virtual void SetScale( FLOAT scale ) = 0;
};

 ボタンの押し下げ情報を取得するGetValメソッドは、上の例ではD3DXVECTOR3型として取っていますが、独自の構造体でもかまいません。D3DXVECTOR3型にしたのは、アナログコントローラによる微妙な値や方向を返す必要があるためです。戻り値には今の状態、引数のgradポインタには前の状態が返ります。

 SetConfigメソッドは仮想ボタンと入力デバイスのキー(ボタン)を関連付けます。ここで指定したキーの情報がGetValメソッドで取得できるようになります。うまく工夫すると、複数の入力デバイスを仮想ボタンオブジェクト1つで集約できるようになります。そのために、入力デバイスを独自のGAMECONTROLLER_ID列挙型で全部判定するようにします。これはキーボードもマウスもゲームパッドも含めるため巨大な列挙型となります。マウスとゲームパッドについてはそれほど問題ありませんが、キーボードについてはDirectInputが定義する「キーボードデバイスの列挙型」に従うようにします。これはDirectInputによって取得されるキーボードのキー情報配列(256バイト)との相性が抜群なためです。GAMECONTROLLER_IDの例を以下に示します:

仮想ゲームコントローラボタン列挙型
////////////////////////////////
// ゲームコントローラ識別ID
////////////////
enum GAMECONTROLLER_ID
{
   GC_KEY_A = DIK_A,
   GC_KEY_B = DIK_B,
   GC_KEY_C = DIK_C,
   GC_KEY_D = DIK_D,
   GC_KEY_E = DIK_E,
   GC_KEY_F = DIK_F,
   GC_KEY_G = DIK_G,
   GC_KEY_H = DIK_H,
   GC_KEY_I = DIK_I,
   GC_KEY_J = DIK_J,
   GC_KEY_K = DIK_K,
   GC_KEY_L = DIK_L,
   GC_KEY_M = DIK_M,
   GC_KEY_N = DIK_N,
   GC_KEY_O = DIK_O,
   GC_KEY_P = DIK_P,
   GC_KEY_Q = DIK_Q,
   GC_KEY_R = DIK_R,
   GC_KEY_S = DIK_S,
   GC_KEY_T = DIK_T,
   GC_KEY_U = DIK_U,
   GC_KEY_V = DIK_V,
   GC_KEY_W = DIK_W,
   GC_KEY_X = DIK_X,
   GC_KEY_Y = DIK_Y,
   GC_KEY_Z = DIK_Z,
   GC_KEY_UP = DIK_UP,
   GC_KEY_DW = DIK_DOWN,
   GC_KEY_LF = DIK_LEFT,
   GC_KEY_RI = DIK_RIGHT,
   GC_KEY_RET = DIK_RETURN,
   GC_KEY_ESC = DIK_ESCAPE,
   GC_MS_LF = 0x0101,
   GC_MS_RI = 0x0102,
   GC_MS_WHEEL = 0x0103,
   GC_PAD_0 = 0x0200,
   GC_PAD_1 = 0x0201,
   GC_PAD_2 = 0x0202,
   GC_PAD_3 = 0x0203,
   GC_PAD_4 = 0x0204,
   GC_PAD_5 = 0x0205,
   GC_PAD_6 = 0x0206,
   GC_PAD_7 = 0x0207,
   GC_PAD_8 = 0x0208,
   GC_PAD_9 = 0x0209,
   GC_PAD_10 = 0x020a,
   GC_PAD_11 = 0x020b,
   GC_PAD_12 = 0x020c,
   GC_PAD_13 = 0x020d,
   GC_PAD_14 = 0x020e,
   GC_PAD_15 = 0x020f,
   GC_PAD_DIRECT = 0x02f1,
   GC_NO = 0xffff
};


 キーボードの値はDIK_**というDirectInputで定義されている列挙型の値をそのまま用いています。DirectInputではキーボードの値を256バイトの配列情報としてごそっと取得します。DIK_**というのはキーボードの押し下げ情報を取得する時のオフセット値に相当しています。例えば[D]キーが押されているかどうかはKeyVct[ DIK_D ]とする事で一発で取得できるわけです。大変便利に出来ています。GC_MS_LF以降はマウスとゲームパッドの情報になります。これは独自に値を設定しているだけですが、ちゃんとデバイスを判定できるよう工夫しています。キーボードは0x00**、マウスは0x01**そしてゲームパッドは0x02**です。最後にGC_NOを忘れずに。



C 仮想コントローラからオペレーションフレームクラスへ

 仮想コントローラ及び仮想ボタンを用いる事によって、「入力」という概念が導入されます。これはキャラクタを操作したり、メニューから項目を選んだり、文章を読み進めたりなど、多種多様な場所で利用できます。いずれも「対象に対してプレイヤーからの入力情報を伝える」という枠組みになっています。そういう「やる事は似ているが動作定義が異なる」仕組みは「フレーム化」すると扱いやすくなります。

 今回の仮想コントローラのボタン配置は固定しています。仮想ボタン1つ1つの押し下げ情報は個別に取得できます。ですから、後はボタンの状態からどういった動作をさせるかを定義する「場所」を設けてあげれば、ユーザからキャラクタへの意思疎通が可能になります。クラス設計の概念図をご覧下さい:

 Updateメソッドが呼ばれると、仮想コントローラのボタン情報がDispatchメソッドに収集されます。Dispatch(送出)メソッドは取得したボタン情報をOPEMESSAGE構造体(ボタン情報を整理した構造体です)としてまとめ、MessageProcメソッドに送ります。MessageProcメソッドは引数のOPEMESSAGE構造体から仮想ボタンを識別して、対応するメッセージハンドラ関数にボタン情報を引き渡します。メッセージハンドラ関数の内部ではボタン情報に対する処理を実装しますが、ここを仮想関数としておくことで、派生クラスで様々な使い方ができるようになります。まとめると、Dispatchメソッドが取得役、MessageProc関数が配達役、そしてメッセージハンドラ関数が実行役になっているわけです。この機構を「オペレーションインターフェイス(クラス)」としてまとめます。

IOperationインターフェイス宣言部
/////////////////////////////////////////
// オペレーティングインターフェイス
// IOperation
/////////////////
interface IOperation : public IUnknown
{
   virtual bool MessageProc( OPEMESSAGE& msg ) = 0;
   virtual bool Dispatch() = 0;
   virtual DWORD Direct( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button0( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button1( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button2( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button3( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button4( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button5( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button6( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button7( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button8( OPEMESSAGE& msg ) = 0;
   virtual DWORD Button9( OPEMESSAGE& msg ) = 0;
   virtual void SetGameController( ComGameController cpGameController ) = 0;
   virtual bool Update () = 0;
};

 メッセージハンドラ関数の個数は必要な分で十分です。上は私が持っているゲームパッドがボタン10個であるために0〜9になっています(^-^;。ハンドラ関数の名前を特定の名前にしても良いと思います。

 このインターフェイスをフレームとして、派生クラスにおいて操作対象となるオブジェクトやメニューなどをコンポーネントとして持たせ、各メッセージハンドラをオーバーライドすれば、このインターフェイスだけでオブジェクトを動かす事ができるようになります。テストなどにもうってつけです(^-^)



 ゲームにおいてユーザ入力は絶対に存在します。コンシューマ機と違いWindows下では入力デバイスが複数種類あるために、このような仮想コントローラを1つはさむ必要が出てきます。入力情報をメッセージとしてまとめるIOperationインターフェイスを設ける事で、入力情報を操作対象となるオブジェクトに適切に伝える事ができるようになります。ここで紹介した仕組みは実装の一例です。皆さんなりの仮想コントローラを実装して、入力周りをすっきりさせましょう(^-^)v