ホーム < ゲームつくろー! < DirectX技術編 < 入力デバイスは早めに実装してしまおう

その1 入力デバイスは早めに実装してしまおう



 Direct Inputは、キーボードやマウス、ゲームパッドなどの外部入力装置を扱うインターフェイスを提供します。外部入力装置はゲーム製作をする時に早めに実装した方が何かと便利です。例えば、数値の上げ下げは、プログラムで書くと何かと面倒です。しかし、それをゲームパッドの方向キーなどに割り当てると、デバッガ作業などはとても楽になります。

 ここの章では、実際のゲームだけでなくデバッガ作業にも一役立つ裏方さんである「Direct Input」の初期化作業についてまとめます。

 この初期化作業ですが、非常に長くて面倒です。クラス化する事を強くお勧めします。とにかく面倒の一言。それがDirectInputの初期化です。


 ところで、Direct Inputの実装には根本の部分で困った事があります。それは「VC++6」だと、2005年5月時点の最新バージョンであるDirect Input8が使えないのです。実際に実装してコンパイルすると、突然、

「dinput8.lib(dilib5.obj) : fatal error LNK1103: デバッグ情報が壊れています; モジュールを再コンパイルしてください」

というリンカエラーが発生します。これは、dinput8.libライブラリの中に定義されている実装部分に、VC++6で解読できない部分が含まれているため、「モジュールが壊れている」と診断されてしまうのだろうと判断されます。元々DirectX9はVC++を正式にサポートしていません。つまり、このエラーはこの「サポート外」の影響です。このエラーの回避方法は私の知る限りありません。

 Direct Input8が使えないのであれば、バージョンを落とすしか手はありません。よって、ここでは1つ前のバージョンであるDirect Input7を使って、長〜い初期化を実装していきます。


@ IDirectInput7インターフェイスの取得

 DirectX9がインストールされた状態でDirectInput7を使用する場合、少し下準備が必要です。
 まず、次のようなマクロ定数を定義します。

#define DIRECTINPUT_VERSION 0x0700

 これはdinput.hを読み込む前に宣言しておきます。こうすると、Direct Inputのバージョンは7となり、それ用の設定がコンパイル時に成されます。

次に、Direct Inputの設定に必要なヘッダーと、実装部であるライブラリを読み込みます。

#include <dinput.h>
#pragma comment(lib, "DX7/dinput.lib")

 このdinput.libはDirectX7に付属している物で、ルートディレクトリの下にDX7というフォルダを設け、そこに格納しておきます。DirectX9にも同名のライブラリファイルがありますが、これはVC++で使用できません。注意して下さい。

 Direct Input7の初期化の最初はIDirectInput7のインターフェイスを取得する事です。

if(FAILED(DirectInputCreateEx(
   hinst,
   DIRECTINPUT_VERSION,
   IID_IDirectInput7,
   (void**)&m_lpDirectInput, NULL)))
      return E_FAIL;

DirectInputCreateEx関数はIDirectInput7のインターフェイスを取得するための関数です。

HRESULT DirectInputCreateEx(
HINSTANCE hinst,
DWORD dwVersion,
REFIID riidltf,
LPVOID* ppvOut,
LPUNKNOWN punkOuter
)

hinstはアプリケーションのインスタンスハンドルです。
dwVersionはDirectInputのバージョン番号を入れるところですが、ここには最初にマクロ定数として定義したDIRECTINPUT_VERSIONを入れます。
riidltfはIDirectInput7のインターフェイスに割り振られたユニークIDを入れます。これはすでにIID_IDirectInput7というマクロ定数が用意されていますので、それを入れます。
ppvOPutにIDirectInput7インターフェイスへのポインタが返ります。
punkOuterにはIDirectInput7を内包するCOMアプリケーションのIUnknownインターフェイスポインタを渡します。よほど変わった事をしない限り、ここはNULLで大丈夫です。


 関数が成功すると、IDirectInput7のインターフェイスへのポインタが取得できます。


A 入力デバイスの列挙

 次に行う事は、パソコンに接続されている各種入力デバイスを扱うインターフェイスを取得する事です。ここでは少し面倒な作業をしなければなりません。
 パソコンにどのような入力デバイスが接続されているかはわかりません。そのため、まず「デバイスを列挙」する必要があります。そのための関数が、IDirectInput7::EnumDevices関数です。

HRESULT IDirectInput7::EnumDevices(
DWORD dwDevType,
LPDIENUMCALLBACK lpCallback,
LPVOID pvRef,
DWORD dwFlags,
)


dwDevTypeは列挙したいデバイスのタイプを指定します。代表的なタイプは次の通りです。
 DIDEVTYPE_MOUSE: マウスデバイス
 DIDEVTYPE_KEYBOARD : キーボードデバイス
 DIDEVTYPE_JOYSTICK: ジョイスティックデバイス
 DIDEVTYPE_DEVICE: その他のデバイス
これはIDirectInput8のEnumDevices関数で指定できるマクロ定数と違いますので注意してください。
lpCallbackにはDIEnumDevicesCallbackというコールバック関数へのポインタを渡します。このコールバック関数にデバイスの種類が返されます。
pvRefへはコールバック関数へ渡す32bitのポインタを渡します。ここへはコールバック関数で取得できるDIDEVICEINSTANCE構造体への配列を渡すと後々が楽になります。
dwFlagsは取得するデバイスを限定する時に設定するフラグです。DIEDFL_ATTACHEDONLYを設定すると、現在接続されているデバイスのみが列挙されます。


 入力デバイス列挙の例です。

pDI->EnumDevices(
   DIDEVTYPE_JOYSTICK,
   &DIEnumDevCallback,
   &m_DeviceInfoAry,
   DIEDFL_ATTACHEDONLY);

DIEnumDevCallbackは静的に宣言されたコールバック関数で、次のように設定します。

BOOL WINAPI DIEnumDevCallback(
LPCDIDEVICEINSTANCE lpddi,
LPVOID pvRef
)
{
// 第2引数がDIDEVICEINSTANCE構造体のvecor STLだとします
vector<DIDEVICEINSTANCE>* ptr = (vector<DIDEVICEINSTANCE>*)pvRef;
DIDEVICEINSTANCE tmp = *lpddi;

// 配列に格納
ptr->push_back(tmp);

// 列挙を続ける場合はDIENUM_CONTINUE、止める場合はDIENUM_STOPを指定
// 全ての列挙が終了すると自動的にコールバックも終了するので、
// 止める理由が無ければDIENUM_CONTINUEにする。
return DIENUM_CONTINUE;
}

もしジョイスティックが2つ以上接続されていれば、DIDEVICEINSTANCE構造体配列に2つのデバイス情報が格納されます。ここで重要なのはDIDEVICEINSTANCE構造体のメンバ変数guidIsntanceに格納されるデバイスIDです。このIDを用いて入力デバイスを扱うIDirectInputDevice7インターフェイスを取得します。


B IDirectInputDevice7インターフェイスの取得

 IDirecInputDevice7インターフェイスは、PCに接続されている入力インターフェイスとのやり取りをしてくれる大切なインターフェイスです。これを取得するにはIDirectInput7::CreateDeviceEx関数を用います。

HRESULT IDirectInput7::CreateDeviceEx(
REFGUID rguid,
REFIID riid,
LPVOID* pvOut,
LPUNKNOWN pUnkOuter
)


rguidにデバイスの列挙の結果取得されたデバイスIDを渡します。
riidにはインターフェイスのバージョンを定義します。ここにはIID_IDirectInputDevice7を入れます。
pvOutにIDirectInputDevice7インターフェイスへのポインタが返ります。
pUnkOuterにはIDirectInput7を内包するCOMアプリケーションのIUnknownインターフェイスポインタを渡します。よほど変わった事をしない限り、ここはNULLで大丈夫です。


設定例は次のようになります。

pDI->CreateDeviceEx(
   m_DeviceInfoAry[i].guidInstance,
   IID_IDirectInputDevice7,
   (void**)(m_DeviceAry[i]),
   NULL
);


これでデバイスを扱う下準備が出来ました。次はデバイスからの情報を格納するフォーマットを設定します。


C デバイスのフォーマット設定

 IDirectInputDevice7によって、デバイスにアクセスする事が可能となりましたが、アクセスするデバイスによって返す情報は異なります。ここでは、その情報を格納するフォーマットを設定します。それには、IDirectInputDevice7::SetDataFormat関数を用います。

HRESULT IDirectInputDevice7::SetDataFormat(
LPCDIDATAFORMAT lpdf
)


lpdfにはDIDATAFORMAT構造体へのポインタを渡します。DIDATAFORMAT構造体は入力デバイスからの情報を格納するための柔軟な形式を持っています。しかし、その分扱いが少し面倒でもあります。そのため、デフォルトの設定をする事が出来ます。以下の変数を使用します。
 c_dfDIKeyboard : キーボード
 c_dfDIMouse : マウス1
 c_dfDIMouse2 : マウス2
 c_dfDIJoystick : ジョイスティック1
 c_dfDIJoystick2 : ジョイスティック2

この設定をすると、IDirectInputDevice7インターフェイスの内部にデータを格納する領域が設けられ、入力デバイスの情報が格納されるようになります。

 次にデバイスの「協調レベル」を設定します。これを設定しないと入力デバイスから正しく情報を得る事が出来ません。


D 協調レベルの設定

 協調レベルとは、入力デバイスの優先権の事です。例えばキーボードはゲーム以外でも使用する入力デバイスです。しかしゲームを行っている最中は、キーボードの入力権を一時占有し、その情報を得なければなりません。協調レベル設定はその占有の強さを決めます。
 協調レベルを設定するにはIDirectInputDevice7::SetCooperativeLevel関数を用います。

HRESULT IDirectInputDevice7::SetCooperativeLevel(
HWND hwnd,
DWORD dwFlags
)


hwndにはウィンドウハンドルを渡します。このウィンドウハンドルが優先権を持つ事になります。
dwFlagsには優先レベルを設定します。
DISCL_BACKGROUNDとDISCL_FOREGROUNDは入力デバイスの使用権利の条件を決めます。DISCL_BACKGROUNDにすると、ウィンドウが非アクティブな状態でも権利を横取りできてしまいます。一方DISCL_FOREGROUNDは、ウィンドウがアクティブの時にのみ権利を得ることが出来ます。
DISCL_EXCLUSIVEとDISCL_NONEXCLUSIVEは、入力デバイスの優先権の強さを決定します。DISCL_EXCLUSIVEにすると、デバイスを占有してしまいます。つまり、他のアプリケーションがデバイスを使いたくても、占有中にはそれを邪魔し許可しません。DISCL_NONEXCLUSIVEにすると、邪魔せず共有するようになります。

 通常、ゲームをしている最中はウィンドウがフォアにある時のみなので、DISCL_FOREGROUNDが設定されます。また、他のアプリケーションがデバイスを使いたい時に邪魔をしないようにDISCL_NONEXCLUSIVEを設定するのが普通です。

pDI->SetCooperativeLevel(
   hWnd,
   DISCL_FOREGROUND | DISCL_NONEXCLUSIVE
);


 ここまでがほぼ全ての入力デバイスに対して共通の設定です。ここから先はデバイスの種類によって設定方法が少しずつ異なります。
 ここでは、コンシューマ機や業務用ゲーム機などにあるようなジョイパッドをまず説明します。


D ジョイパッドを設定

 ジョイパッドは家庭用ゲーム機にあるようなハンディタイプの物もあれば、業務用ゲーム機のスティックタイプなど様々です。それらの多くは「On」および「Off」のデジタルな信号で入力を感知しますが、最近はアナログスティックも増えてきました(コンシューマ機ではもうおなじみです)。

 ジョイパッドのデータフォーマットとして、ここでは、

HRESULT IDirectInputDevice7::SetDataFormat(c_dfDIJoystick);

とします。c_dfDIJoystick変数を設定すると、ジョイスティックのフォーマットはDIJOYSTATE構造体となります。DIJOYSTATE構造体はアナログな方向及び、32個のボタンの情報を得る事が出来ます。

 入力があった時には、それを数値に変換しなければなりませんが、その数値はプログラマが決めます。そのような入力デバイスの値は「属性」としてまとめられます。

 入力デバイスの属性を決めるにはIDirectInputDevice7::SetProperty関数を用います。

HRESULT IDirectInputDevice7::SetProperty(
REFGUID rguidProp,
LPDIPROPHEADER pdiph
)


rguidPropはどのような属性を設定するかを指定します。設定できる属性は沢山あるのですが、ここでは軸モードDIPROP_AXISMODEと軸の値DIPROP_RANGEを用いる事にします。
pdiphへはその設定値を格納した構造体へのポインタを渡します。


 この関数は扱いが結構面倒です。一つずつ説明します。まず、軸モードには「絶対軸」と「相対軸」があります。絶対軸は倒した位置がそのまま数値化されるモードで、ジョイスティックは普通このモードにします。一方相対軸は入力値が前回からの差分で表されます。例えばマウスは自分の絶対位置を保持していません。マウスは前回の位置からのズレを伝えるだけです。よって、マウスデバイスは相対軸で設定します。
 軸の値は、倒した時の軸の絶対的な値を決定します。例えば、アナログスティックを左にいっぱいに倒した時を1000、右に倒した時を-1000などと設定します。

 さて、まずは軸モードから設定しましょう。軸モードの定義はDIPROPDWORD構造体で行います。

struct DIPROPDWORD{
DIPROPHEADER diph;
DWORD dwData;
};

構造体の中にDIPROPHEADER構造体がさらに入っています。

struct DIPROPHEADER{
DWORD dwSize;
DWORD dwHeaderSize;
DWORD dwObj;
DWORD dwHow;
};


dwSizeにはこの構造体を含んでいる構造体のサイズを格納します。上の場合だとDIPROPDWORD構造体の大きさをsizeof(DIPROPDWORD)で算出して代入します。
dwHeaderSizeにはこのDIPROPHEADER構造体自体のサイズを格納します。sizeof(DIPROPHEADER)で計算します。
dwObj
にはdwHowでの設定値によって色々な値が入ります。
dwHow dwObjに何の値が入っているのかをフラグで指定します。DIPH_DEVICE を指定した場合、dwObjは意味を持ちません。この場合dwObjには0を入れる必要があります。DIPH_BYOFFSETの場合、dwObjにはデータフォーマットのオフセット値が入ります。データフォーマットは1つの大きなメモリ領域です。よってある値を取り出すにはデータフォーマットの先頭からのオフセット値が必要になります。それを設定する時にこのフラグを使います。

 軸モードを設定する場合、dwHowにはDIPH_DEVICEを設定し、dwObjには0を代入します。そして、dwDataに絶対軸を指定するDIPROPAXISMODE_ABSを設定すれば、構造体の準備は完了です。後はこの構造体をSetProperty関数に渡せば、軸モードが設定されます。

// 軸のモードを絶対軸に設定
DIPROPDWORD diprop;
diprop.diph.dwSize = sizeof(DIPROPDWORD); // 使用される構造体のサイズ
diprop.diph.dwHeaderSize = sizeof(DIPROPHEADER); // DIPROPHEADER構造体のサイズ
diprop.diph.dwHow = DIPH_DEVICE; // 対象(ここではデバイス)
diprop.diph.dwObj = 0; // デバイスを対象とする時はいつも0
diprop.dwData = DIPROPAXISMODE_ABS; // 絶対値モードに設定

m_DeviceAry[i]->SetProperty(DIPROP_AXISMODE, &diprop.diph); // 絶対軸に設定


 続いて、軸の値を設定します。

 軸の値の設定は正直面倒です。というのは、軸(スティックやボタン)毎に値を全て決める必要があるからです。ボタンが10個くらいあって方向キーが4方向などとなった場合、上のような作業を10回以上繰り返さなければなりません。また、ボタンが幾つあるのか、という基本的な情報を手に入れなければなりません。このためには「オブジェクトの列挙」を行わなければなりません。

 まず、オブジェクトの列挙は、IDirectInput7::EnumObjects関数を用います。

HRESULT EnumObjects(
LPDIENUMDEVICEOBJECTSCALLBACK lpCallback,
LPVOID pvRef,
DWORD dwFlags
)


lpCallbackにはDIEnumDeviceObjectsCallBackというやけに長いコールバック関数へのポインタを渡します。このコールバック関数の内部で入力デバイスに存在するオブジェクトが列挙されます。
pvRefにはコールバック関数に渡す32bitの値を入れます。コールバック関数の内部ではDIDEVICEOBJECTINSTANCE構造体がオブジェクトの数だけ取得できるので、この構造体の配列へのポインタを渡すのが楽ですね。
dwFlagsは列挙するオブジェクトを限定するフラグです。例えばボタン情報だけでよいのであれば、DIDFT_BUTTON、方向キーの情報ならばDIDFT_AXISなど、非常に沢山のフラグがあります(マニュアルを参照してみてください)。


 ここではジョイスティックにある全てのオブジェクトの情報を列挙する事にしましょう。よって、次のようにします。

m_DeviceAry[i]->EnumObjects(
   DIEnumDevObjCallback,
   &aDIoi,
   DIDFT_ALL
);

aDIoiはDIDEVICEOBJECTINSTANCE構造体配列へのアドレスです。
 コールバック関数であるDIEnumDevObjCallback関数は、次のようになります。

// デバイスのオブジェクトを列挙するコールバック関数(static宣言)
BOOL WINAPI DIEnumDevObjCallback(
LPCDIDEVICEOBJECTINSTANCE lpddoi,
LPVOID pvRef
)
{
// 第2引数をvector<DIDEVICEOBJECTINSTANCE>へのポインタに型変換
vector<DIDEVICEOBJECTINSTANCE> *tmp = (vector<DIDEVICEOBJECTINSTANCE>*)pvRef;

// 配列にオブジェクトの情報を格納
tmp->push_back(*lpddoi);

// 列挙は全て終われば自動的に終了するので、
// 列挙し続ける
return DIENUM_CONTINUE;
}

コールバック関数の中でオブジェクトの情報を格納する構造体を配列に格納しています。

 これでオブジェクトの列挙が出来たので、次に軸の値を設定します。ここでは簡単のため、方向キーのみに同じ値を設定しましょう。

 軸の値はDIPROPRANGE構造体に格納します。

struct DIPROPRANGE{
DIPROPHEADER diph;
LONG lMin;
LONG lMax;
};

 DIPROPHEADER構造体のdwHowにはDIPH_BYIDフラグを設定します。このフラグが設定されるとdwObjにはオブジェクトのIDが設定されていると判断されます。オブジェクトのIDは列挙の時に取得したDIDEVICEOBJECTINSTANCE構造体のdwTypeメンバ変数に格納されています。
 lMinには軸の最小値、lMaxには最大値が入ります。4方向のアナログスティックの場合、左および下がlMin、右および上がlMaxの値に相当します。lMin>lMaxとなる値は設定できません。

 具体的な設定は次のようになります。

// 軸の値の設定
for(int j=0; j<aDIoi.size(); j++){
DIPROPRANGE diproprg;
diproprg.diph.dwSize = sizeof(DIPROPRANGE); // 使用される構造体のサイズ
diproprg.diph.dwHeaderSize = sizeof(DIPROPHEADER); // DIPROPHEADER構造体のサイズ
diproprg.diph.dwHow = DIPH_BYID; // 対象(ここでは軸)
diproprg.diph.dwObj = aDIoi[j].dwType; // 対象となる軸
diproprg.lMin = -255; // 最小値
diproprg.lMax = 255; // 最大値

// 設定
m_DeviceAry[i]->SetProperty(DIPROP_RANGE, &diproprg.diph);
}

 上の例だと軸の値は-255〜255ですが、これはLONGで収まる範囲であれば幾つでもかまいません。


 軸モードと軸の値を設定し終わると、ようやくジョイパッドから情報を得る事ができるようになります。あともう少しです。


E ジョイパッドから情報を取得

 ジョイパッドから情報を取得するには、まずIDirectInputDevice7::Poll関数を呼び出します。この関数は「ポーリング」するデバイスに対してデータ取得の準備をさせます。ポーリングというのは、定期的にデバイスの値を取ることを言います。
 この関数を呼んだ後、デバイスへのアクセス権を取得するIDirectInputDevice7::Acquire関数を呼び出します。これによって、デバイスから値を得る権利を得ます。この関数が失敗する時は、他のアプリケーションがデバイスを占有しているか、デバイスが「ロスト」している可能性があります。それは、この関数の戻り値で知る事ができます。
 アクセス権をうまく取得できたら、実際に入力値を取得します。取得する関数はIDirectInputDevice7::GetDeviceState関数です。この関数が呼び出された瞬間に入力されている情報が構造体に格納されます。

 実際に情報を得る部分は次のように実装されます。

// 入力デバイスのアクセス権を取得
HRESULT hr;
m_DeviceAry[i]->Poll();
hr = m_DeviceAry[i]->Acquire();
while(hr == DIERR_INPUTLOST)
   hr = m_DeviceAry[i].m_Dev.GetPtr()->Acquire();

// 情報を取得
DIJOYSTATE dijs;
ZeroMemory(&dijs, sizeof(DIJOYSTATE));
m_DeviceAry[i]->GetDeviceState(sizeof(DIJOYSTATE), &dijs);

 whileループの部分は入力デバイスがロストしている場合に、それが再度取得されるまで回しています。アクセス権を取得できたらGetDeviceState関数で実際に入力値を取得しています。

 取得した値は、プログラマが自由に解釈してゲームに反映させる事が出来ます。


 これでジョイスティックからの入力情報を利用して色々と遊べる状態になりました。実際にゲームでも使用できる状態です。
 ジョイスティックを使って、色々なデバッグ作業を少し楽にする事ができます。例えば、ボタンを押すとある値が増加し、話すと減少するように仕込めば、アプリケーションが動いた状態でのリアルタイムデバッグが可能になります。特にDirect Graphicsなどは3Dオブジェクトなどを色々な方向に回したり遠ざけたりする必要があり、ジョイスティック無しだとデバッグしにくいものです。

 ここまでの作業を見てきて分かるように、DirectInputを初期化して使える状態にするまでには、大変面倒な設定を行っていかなくてはなりません。こういう面倒な事はクラスに任せるのがセオリーです。デフォルトの値で初期化もろもろをすべてやってくれるクラスを用意し、プログラマは、

  CDXJoystick  JS;
  JS.Init(Hinstance, hWnd);
  DWORD stickinfo = JS.GetStickInfo();

くらいで済んでしまう状態にします。こうなると、入力デバイスがとても扱いやすくなります。このクラス化についてはDirectXクラス構築編で検討する予定です。



(2005. 12. 2追記)
F アクセス権の開放

 取得した権利は開放するものですが、Acquire関数で権利を得て必要な情報を取った後にすぐUnacquire関数で権利を開放してはいけません。これは私がはまったバグなのですが、権利の取得と開放を繰り返すと、デバイスへ全然アクセスしてくれなくなります。キーボードなど反応もせずです。よって、Acquire関数はループごとに呼び出しますが、Unacquire関数はアプリケーションが終了した時に呼び出します。これ、マニュアルにはちゃんと書いてあるんですよね。皆さんが同じ鋲を踏みませんように・・・。



G 実はVC++でもDirectInput8は使えました

 最初に「DirectInput8が使えない」と書いたのですが、その後色々試した結果使えることが判明しました。まず、「DirectX9.0」の本体をちゃんとDLしてインストールします。なんとなくアップデートだけを入れてはいけません。次に最新のアップデート版を入れます。この原稿を書いている段階で私は本体と「DirectX9.0 (October2005)」を入れましたが、あっさり使えました。どうやら前回はアップデート版(DirectX9.0 (October2005))だけでプログラムを組んでいたため、不具合がおきていたようです。DirectXのバージョンやインストール方法って、とてもわかりにくいと感じているのは私だけでしょうか・・・。