7-2 DirectInputの使い方

DirectInputはキーボードやマウスの入力はもちろん、
ゲームコントローラーも扱うことが出来るライブラリです。

Windowsでは入力が行われるとOS上で何らかの処理が行われてからプログラムに結果が渡されます。
これだと処理のためにいくらかのCPU処理をすることになるため、
ゲームなど毎回値を取るごとにこの処理が行われるとなると、
全体的にはオーバーヘッドとなったり入力の遅延が発生してしまうことになります。

そこでこの入力を直接プログラムに渡せるようにしたのがDirectInputです。
例えばキーボードのドライバから受けた入力情報をそのままプログラムに渡すことが出来るため、
オーバーヘッドの無いリアルタイムな処理が可能となります。

なお、ゲームコントローラーによっては形や入力ボタン数などが違ったりするものがありますが、
DirectInputではこれらを同じインターフェースで取得出来るようになっています。
さらにDirectInputでは振動モーターなどのついたコントローラーに対して、
フォースフィードバック機能もサポートされています。

キーボードやマウス、ジョイスティックを扱うAPIはWindows標準関数にもありますが、
DirectInputではこれらを統一されたインターフェースで操作出来るため、
原理が分かれば簡単にこれらの入力デバイスを扱うことが出来ます。

ということでここではDirectInputの基本的な使い方を説明します。

なおこのサイトではゲームの特性上、マウスやフォースフィードバックについては説明していません。
そのため必要ならば自分で調べてください。

■IDirectInput8インターフェース

DirectInputにはいくつかバージョンがありますが、
ここではDirectInput8を使ったプログラムについて説明しています。

なおDirectInputはDirectSoundと同様にCOMで出来ていますが、
全て専用の関数が用意されているため特にCOMを意識する必要はありません。


まずはDirectInputを使うにはdinput.hをインクルードします。
そしてdinput8.libdxguid.libをリンクするように設定します。

1つ注意しなければならないのは、dinput.hではどのバージョンのDirectInputを使うのか、
不明な場合には現在のDirectXのSDKにあったバージョンが使われます。
これでは新しいDirectXを使っている場合にバージョン違いが起こることが考えられるため、
dinput.hをインクルードする前に以下のようにバージョンを定義してからヘッダをインクルードします。

#define DIRECTINPUT_VERSION     0x0800          // DirectInputのバージョン指定
#include <dinput.h>

これでコンパイル時にワーニングが出なくなります。

またlibファイルのリンクをプロジェクトの設定で行うことも出来ますが、
ここでは#pramgaを使ってソースコード上にリンク定義を行います。


次に、DirectInputを扱うためにはIDirectInput8オブジェクトが必要です。
IDirectInput8とはキーボードやジョイスティックなどの実際のデバイスを管理している大元となるため、
基本的にはアプリ上に1つだけあれば問題ありません。

IDirectInput8が構築出来たらそれ以降はこのオブジェクトに対して他の機能を呼び出すような形になります。

まずIDirectInput8のポインタはLPDIRECTINPUT8と定義されているので、
これをグローバル変数などに定義します。

LPDIRECTINPUT8    lpDI = NULL;        // DirectInputオブジェクト


次に実際にIDirectInput8オブジェクトを作成します。

    // IDirectInput8の作成
    HRESULT ret = DirectInput8Create( hInstance,DIRECTINPUT_VERSION,IID_IDirectInput8,(LPVOID*)&lpDI,NULL );
    if( FAILED(ret) ) {
        // 作成に失敗
        DEBUG( "DirectInput8の作成に失敗\n" );
        return -1;
    }

この関数の引数に指定するのは、まずアプリのインスタンスとSDKで定義されているDirectInputのバージョン、
取得したいIDirectInput8インターフェースのIID、そしてDirectInput8の構築に成功した場合にそのポインタの入る、
先ほど定義したLPDIRECTINPUT8の変数のポインタとなります。

なおDirectInput自体はCOMで出来ているため、終了時には必ずIDirectInput8のオブジェクトをReleaseしなければなりません。
この説明は最後にしますのでとりあえずオブジェクトの作成は出来たとして次に行きます。


■IDirectInputDevice8インターフェース

IDirectInput8オブジェクトが出来たら次にここから入力デバイスに対するIDirectInputDevice8オブジェクトを取得します。

入力を行うためのデバイス1つにつき1つのIDirectInputDevice8が必要となり、
たとえばキーボード用に1つ、マウス用に1つ、ジョイスティックが2つあれば2つという形で、
それぞれにIDirectInputDeviec8オブジェクトを用意しなければなりません。


とりあえずここではキーボードについてオブジェクトを取得してみます。
まず、キーボード用にIDirectInputDevice8のポインタをグローバルに定義します。
なおIDirectInputDevice8のポインタはLPDIRECTINPUTDEVICE8と定義されているので、
ここではこれを使って定義してみます。

LPDIRECTINPUTDEVICE8    lpKeyboard = NULL;             // キーボードデバイス


そしてキーボードデバイスをIDirectInput8インターフェースから取得するには以下のようにします。

    // IDirectInputDevice8の取得
    ret = lpDI->CreateDevice( GUID_SysKeyboard,&lpKeyboard,NULL );
    if( FAILED(ret) ) {
        DEBUG( "キーボードデバイスの作成に失敗\n" );
        lpDI->Release();
        return -1;
    }

第1引数は入力デバイスのGUIDとなっており、ここではデフォルトのシステムキーボードということで
GUID_SysKeyboardを定義しています。

ここには本来各デバイスで決められたGUIDを指定しなければならず、
ジョイスティックなどでは少々面倒な処理をしてこのGUIDを取得する必要があります。
なおこれはジョイスティック部分で詳しく説明するので、ここではとりあえず記憶に留めて置いてください。

第2引数には取得したデバイスオブジェクトが入る変数のポインタを指定します。
ここではキーボード用に定義したlpKeyboardのポインタを渡しています。

この関数が成功するとlpKeyboardにはキーボードデバイスのオブジェクトが入ります。



■入力データ形式のセット

上記のCreateDeviceでは各入力デバイスごとにIDirectInputDevice8を用意しなければならず、
そのオブジェクトはどのような入力デバイスで、またどういうデータが取得できるのか
この時点ではまだ分かりません。

このため次にこのデバイスがどういったデータ形式でデータを受け取るかを、
指定してあげる必要があります。

キーボードのデータフォーマットの場合は以下の用になります。

    // 入力データ形式のセット
    ret = lpKeyboard->SetDataFormat( &c_dfDIKeyboard );
    if( FAILED(ret) ) {
        DEBUG( "入力データ形式のセット失敗\n" );
        lpKeyboard->Release();
        lpDI->Release();
        return -1;
    }

SetDataFormatの引数にはデータ形式を記述した構造体をセットすることになっていますが、
ここではdinput.hに定義されているデフォルトのキーボード形式の定数をセットしています。

特殊な入力デバイスはこの構造体をきちんと自分で作成しなければなりませんが、
このサイトではキーボードとジョイスティックのみを扱うため、
すべてdinput.hに定義されているデフォルトのデータ形式が使えます。


■排他制御

排他制御とは例えば他のアプリが手前にある時にも入力を待つようにするのか、
それともいったん停止して入力をしないようにするのかを制御するものです。

前者の場合でどのような場合でも常に入力を受け取ってしまうとなると、
ESCキーでゲームを終了するようなプログラムになっていると意図せずゲームが終了してしまうことがあります。

例えばメモ帳を開きここで漢字変換を行おうと文字を入力中、入力を間違えたので戻そうとESCキーを押してしまったとします。
するとメモ帳上の漢字変換はキャンセルされますが、一緒に後ろにあるゲームも終了してしまうことになります。

これではWindows本来のフォーカスという概念が崩壊してしまうため、
最低でもキーボードだけは別ウインドウがアクティブの時は反応しないようにしなければなりません。

そこでキーボードに対して以下のように排他制御を設定するようにします。

    // 排他制御のセット
    ret = lpKeyboard->SetCooperativeLevel( win.hWnd,DISCL_FOREGROUND|DISCL_NONEXCLUSIVE|DISCL_NOWINKEY );
    if( FAILED(ret) ) {
        DEBUG( "排他制御のセット失敗\n" );
        lpKeyboard->Release();
        lpDI->Release();
        return -1;
    }

この関数の第1引数には自分のウィンドウハンドルを指定します。
※先にウィンドウを作っておく必要があります

第2引数には排他制御のフラグを指定しますが、ここでは手前にある場合のみ入力を受け付けるようにし、
さらに同時にWindowsキーを使えないように設定しています。
※Windowsキーを押すとスタート画面が開いたりして、フォーカスが変わってしまいます

ひとまずこの設定にしておけばキーボード入力の誤作動はなくなります。


■実際に取得してみる

以上でキーボードの入力を取ることが出来るようになったので、
ここでは実際に毎フレームの中でキーの入力をしてみます。

上記のプログラムでキーボードは使えるようになりましたが、
これはあくまでも使えるようになっただけであって、実はまだそのままでは取得は出来ません。

取得をするにはそのデバイスに対して入力開始を宣言する必要があります。

    // 動作開始
    lpKeyboard->Acquire();


これ以降キーの入力が可能となりますが、実は上で紹介した排他制御により
アプリのフォーカスが失われてしまうと再び入力が停止してしまいます。
するとまた入力が出来ない状態となってしまうため、入力が取れなくなったら再び再開させる必要があります。

これを踏まえメインループでキーボードの入力に失敗したら再び再開する処理を考慮したプログラムは以下のようになります。

        // キーの入力
        BYTE key[256];
        ZeroMemory( key,sizeof(key) );
        ret = lpKeyboard->GetDeviceState( sizeof(key),key );
        if( FAILED(ret) ) {
            // 失敗なら再開させてもう一度取得
            lpKeyboard->Acquire();
            lpKeyboard->GetDeviceState( sizeof(key),key );
        }

入力データを取得するにはGetDeviceState関数を使用します。

引数にはデバイスに合ったデータ形式のバッファサイズと、実際に取得したデータを格納するバッファ(構造体など)を指定しますが、
キーボードの場合は256個分のBYTE配列が入力データとなるので、このバッファを用意してそのポインタを渡しています。

GetDeviceStateが成功すればその時の全てのキーの状態が入りますが、
もし失敗した場合はフォーカスが外れている可能性があるのでAcquireを呼び出して動作を開始させ、
再びGetDeviceStateを呼び出して入力を行っています。

なお、完全にフォーカスが失われている場合はこのAcquireも失敗しますが、
最初にキーバッファを0クリアしているため、もし失敗した場合は何も押されていない状態となるので、
そのまま次のキー入力判定を行うことが出来ます。

■キー入力判定

GetDeviceStateは実はWindows関数のGetKeyboardStateにかなり似ています。

GetKeyboardStateでも256個分のBYTE配列を渡しており、
実は押下判定ビットも同じく上位ビットが立っているかどうかで判定します。

しかしGetKeyboardStateと異なるのがキーコードの扱いです。

GetKeyboardStateの判定ではVK_****という定数と、直接'A'~'Z''0'~'9'を指定していましたが、
DirectInputの場合はDIK_****という定数を使います。

またアスキーコードに関してもDIK定数が存在し、例えばAキーはDIK_Aというように
全てのキーコードがdinput.hに定義されています。
※VKとDIKのキーコードはまったく異なるため混同しないようにしてください

以下はESCキーが押されているかを判定するプログラムです。

        // ESCAPEなら終了
        if( key[DIK_ESCAPE]&0x80 )
            break;


■終了処理

DirectInputはCOMなので使い終わったら開放しなければなりません。

ここではIDirectInputDevice8とIDirectInput8を使用したので作成した順の逆に開放します。

    lpKeyboard->Release();
    lpDI->Release();

※実はCOMの仕様的に厳密には逆の順番にする必要はありません

■サンプルのソースファイル

上記のプログラムのサンプルを用意しました。

VisualStudio2010のプロジェクト
※VC2012、VC2013はこちら
TestDirectInput_vc2010.zip
VisualStudio2015のプロジェクト TestDirectInput_vc2015.zip

このサンプルではESCキーを押すと終了することが出来ます。

またSetCooperativeLevelをコメント化してみることで、
フォーカスを変えてもESCキーが反応してしまうことを確認できます。