7-3 ジョイスティックの使い方

前回まででDirectInputの基本的な使い方を説明したので、
ここではジョイスティックからボタンや十字キーの入力情報を取得する方法を説明します。

■ジョイスティックについて

ゲームコントローラーといえばファミコン(古すぎ)やDSのような十字キーやABボタンを思い浮かべると思います。

Wikiより

これは全ての入力がONかOFFかというボタンのみのコントローラーですが、
プレステやXBox、PSPやVitaなどにはキノコと言われる倒した角度によって値が変わるスティックと言うものが付いています。

Wikiより

スティックは飛行機ゲームやレースゲームのように微妙なコントロールが必要なゲームで使われますが、
これをアナログ方式と言って代わりにON/OFFのみのボタンのことをデジタル方式と言います。

通常のコントローラーの方向キー(十字キー)は、ハットスイッチと呼ばれる方向を持った入力として認識します。

実際には上下左右それぞれが押されると、その組み合わせにより上を0度として
右回りに合計8方向の角度(度)を100倍した値が入るものですが(上記の場合は4500)、
USBコンバーターを通したPS2などのコントローラーの場合、
方向キーはハットスイッチではなくアナログ値になっていることがあります。

一見ONとOFFのみのためデジタル方式ではないかと思われるかもしれませんが、
十字キーに関してはアナログ入力の場合があるため注意が必要です。

ちなみに通常アナログ形式では「マイナス→原点0→プラス」というデータ値を持ちますが、
これを十字キーにたとえると何も押されていない状態を原点0として、
左を押すとマイナスの最大値、右を押すとプラスの最大値を返すという仕組みになっています。
※厳密にはマイナスやプラスという概念は自分で設定することでそうなります


■あそびとは

アナログ方式には欠点があり手を離せば必ず原点0に戻るとは限りません。

例えばプレステのアナログコントローラーでスティックをグリグリ動かしたあとに手を離すと、
真ん中あたりには戻りますが完全にど真ん中に戻ることはほぼありえません。
これはコントローラー内のバネの特性やスティック自体の粘性によるもので、
ハードウェアの仕様となります。

プログラムからこの値を取得してみるとこの微妙に傾いた状態の値が取れてしまうため、
仮にレースゲームなどでまっすぐ進んでいるつもりがだんだん曲がっていってしまうという現象が起こります。

ゲームを作る側としてはこの微妙に傾いた状態でも0と判定しなければなりません。
こういった少しのズレでも0と判断する間のことを「あそび」と言い、
どのくらいあそびにするかによってゲームの操作感覚が変わります。

例えばアクセルをアナログ入力する場合、まだ走らないつもりでちょっと足を置いただけなのに加速してしまったり、
逆にあそびを多くしすぎてしまい踏んでもまったく動かなかったりすると、
プレイヤーが意図しない動作つまり操作感が悪いとなってしまい、このゲームは面白くないと思ってしまうわけです。

そして一番厄介な問題は、同一製品のコントローラーでも性能が同じということは保障されません。
このためプログラム側でこの性能差を吸収するため、キャリブレーションという機能を実装する必要があります。

■アナログ値の範囲

上の方でアナログの範囲は「マイナス→原点0→プラス」と説明しましたが、
DirectInputではマイナスとプラスの値を任意に設定することが出来ます。
※全てをプラス値にするということも可能です

なお、逆に値を設定しないことで回転制限の無いコントローラーも使用出来るようですが、
通常のコントローラーは限界が必ず存在するためここでは必ず値を設定することにしています。

またアナログ値の範囲についてはここでは-1000~+1000を使っています。
これにより分解能は左右とも1000となるため、PSのコントローラーなど一般的なものであれば、
十分精度が期待出来るかと思います。

ちなみにアナログの十字キーに関して補足すると、それぞれのキーの最大値が返るということは、
ここでは左を押すと-1000、右を押すと+1000が返るという意味になります。

■軸の概念

アナログに関して左右を-1000~+1000として話をしていましたが、
これを正面から見た場合は一直線上で変動しているのが分かります。

分かりやすくシーソーに例えると、
以下のように原点にいる場合は釣り合った状態となります。

真ん中
0

そしてここではこの釣り合った状態を中心ということで0と定義します。

これを左か右に倒すと以下のようになります。

-1000 +1000

左に全開に倒したときを-1000、右に全開に倒したときを+1000としていますが、
この範囲でシーソーが動くことで-1000~+1000の範囲で値が動くことになります。

さてよく見ると真ん中に棒がありますが、この棒は左右に回転はしますがその場からは移動はしていません。
実はこの棒を「軸」と言い、上のように左右のみのシーソーならば1つの軸で構成されているというのが分かります。



通常のコントローラーは普通は上下左右と4方向の向きに対して動きます。

ということは上下用にも軸をもう1つ追加すればよいことになります。
これを立体的に斜めに見た場合は以下のようになります。

ここでは紫の棒をX軸、赤の棒をY軸とします。
※厳密にはこれらの棒は同じ高さでくっついてるものとなります(つまり「+」のような感じ)

これで上下左右に動くようになり、軸を2つ使っているため2軸と言います。
言い換えるとPSのコントローラーのキノコ部分は2軸で成り立っていると言えます。

ちなみに実はZ軸というのも存在し、上の絵で言うと黒の軸も回転するようなコントローラーです。
なお、現時点でそのようなコントローラーは存在しないため、代わりに例えばPSのコントローラーの右側のもう1つのキノコを
Z軸などに割り当てていることがあり、特にXYZ軸セットで1つのコントローラーになっているわけではありません。

なおWindows上で認識するほとんどのコントローラーは、基本的にX軸とY軸に十字キーが割り当てられるため、
上下左右のみを使うのであれば固定で指定してしまっても問題無いと思われます。

■専用コントローラーのスクラッチ

USBコンバーター経由で接続したPS用ビーマニコントローラー(5key、7key共に)のスクラッチはY軸に割り当てられています。
右に回すとプラス値、左に回すとマイナス値が返ります。

なお、USBコンバーターの種類によっては通常のボタンとしても認識するようですが、
全てのコンバーターがそのような入力情報を返すという保障は無いため必ず確認が必要となります。

繋がっているコントローラーをテストしたい場合は、
コントロールパネル内の「デバイスとプリンター」を選び、
該当するコントローラーを右クリックして「ゲームコントローラーの設定」から確認出来ます。


ゲームコントローラーのダイアログが表示されたらその中のプロパティボタンを押すことで、
以下のような入力状態を確認する画面が表示されます。



ちなみにIIDX用コントローラーで試したところ、スクラッチをさっと回すと「ON・・OFF」というように、
ONになったあと少し置いてOFFになるようで必ず数十ミリ秒間はON状態が続くという仕様でした。
またぐるぐる回している間はずっとONのままだったので、
タイムアウト時間内に次のONが入ることで連続でONと認識しているようです。

恐らく入力の取りこぼしを考慮して確実にONが取れるようにこのような仕様になっているのだと思われますが、
逆にスクラッチがOFFになるまで時間がかかるため、回転を止めても一定時間押された状態となり、
例えば選択画面でスクラッチを使用する場合は応答の悪さにより選択がしづらくなると思われます。

なお右回転から左回転に一瞬で切り替えた場合は、逆方向へ値が即座に切り替わるようなので、
切り返しの応答性は問題無いようです。

基本的に専用コントローラーはアナログ仕様ではなくON/OFFのデジタル仕様と思ったほうがよく、
例えば曲選択時で回転させる速度によって選曲速度を変えるということは出来ません。
※分解して確認したところ物理的にはロータリーエンコーダーのような作りになってはいるが、
 その信号線はUSBコンバーター側からは出力されていないため回転状態を取得することは出来ないようです


■ジョイスティックデバイスの取得方法について

ジョイスティックの概念が分かったところでここではジョイスティックを扱う方法を説明します。

まずジョイスティックとはPCに1つだけしか接続できないという仕様ではありません。
つまり同時に複数のジョイスティックが接続されていることも想定しなければなりません。

DirectInputでは現在接続されているジョイスティックを全て列挙させ、
特定のジョイスティックだった場合にそれを使用するといった手法を使います。

列挙はコールバック方式となり、DirectInputの列挙関数に自分で定義したコールバックのアドレスを指定することで、
ジョイスティックが見つかるごとにそのコールバックが呼び出されるという仕様です。

このコールバック内で特定のジョイスティック(名前が一致など)だった場合に、
そのジョイスティックを使用するようなプログラムを書きます。

またはもっと簡単な方法として、基本的にはジョイスティックはPCに何個も付けている人はあまり居ないので、
最初に見つかったジョイスティックだけを使うといったことも可能です。
※このサンプルでは最初に見つかったジョイスティックを使います

■コールバック関数

ここでは接続されているジョイスティックを列挙する際、
見つかったジョイスティック1個ずつに対して呼び出されるコールバック関数を定義してみます。

コールバック関数というのはあくまでも引数と戻り値の型が一緒であれば良いため、
関数名や引数の変数名は任意につけることが出来ます。
ちなみにこれをMSではプレースホルダと言うようです。

列挙用のコールバックは以下のように定義されています。

typedef BOOL (FAR PASCAL * LPDIENUMDEVICESCALLBACKA)(LPCDIDEVICEINSTANCEA, LPVOID);


少々分かりにくいですがこれを適当に関数化すると以下のようになります。

BOOL PASCAL EnumJoyDeviceProc( LPCDIDEVICEINSTANCE lpddi,LPVOID pvRef )
{
    // なんかの処理
}

これは引数と戻り値が一緒で、関数名と引数の変数名を自分で適当に定義したものです。


さて上記の関数内では実際に何をすればよいのかですが、
まずこの関数はジョイスティックが1個見つかるたびに呼び出されます。
また引数のlpddiにはその時のジョイスティックの情報が入っています。
そしてこのジョイスティックの情報から目当てのジョイスティックを判定したりします。

次に戻り値ですが次のジョイスティックを続けて列挙させたい場合はDIENUM_CONTINUEを、
該当するジョイスティックが見つかったなどでもう列挙は必要無くなった場合はDIENUM_STOPを返します。
(戻り値がBOOLなんだからTRUEかFALSEでいいはずなのになんでわざわざ定義してるのか謎w)


ここではひとまず複数のジョイスティックについては考えず、
一番最初に見つかったジョイスティックのみを使うようにしてみます。

具体的にはコールバックが呼ばれたらその時のジョイスティックデバイスを構築し、
さらに上で説明したアナログの最低値と最高値の範囲をセットすることで、
列挙が完了した段階でジョイスティックが使用可能な状態となるようにプログラムします。


ではまずジョイスティック用のデバイスを記録する変数をグローバル変数に定義します。

LPDIRECTINPUTDEVICE8    lpJoystick = NULL;      // ジョイスティックデバイス

ここでは列挙後にこの変数に値が入っていなかった場合はデバイスの作成が出来なかった
という判定をさせるために、同時にNULLで初期化しています。(詳細は後述)


次がメインとなる列挙関数内の処理です。

// 1つのデバイスごとに呼び出されるコールバック関数
BOOL PASCAL EnumJoyDeviceProc( LPCDIDEVICEINSTANCE lpddi,LPVOID pvRef )
{
    DEBUG( "コールバック呼び出し\n" );

    // ジョイスティックデバイスの作成
    HRESULT ret = lpDI->CreateDevice( lpddi->guidInstance,&lpJoystick,NULL );
    if( FAILED(ret) ) {
        DEBUG( "デバイス作成失敗\n" );
        return DIENUM_STOP;
    }

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

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

    // 入力範囲のセット
    DIPROPRANGE diprg;
    diprg.diph.dwSize       = sizeof(diprg);
    diprg.diph.dwHeaderSize = sizeof(diprg.diph);
    diprg.diph.dwHow        = DIPH_BYOFFSET;
    diprg.lMax              = 1000;
    diprg.lMin              = -1000;

    // X軸
    diprg.diph.dwObj = DIJOFS_X;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // Y軸
    diprg.diph.dwObj = DIJOFS_Y;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // Z軸
    diprg.diph.dwObj = DIJOFS_Z;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // RX軸
    diprg.diph.dwObj = DIJOFS_RX;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // RY軸
    diprg.diph.dwObj = DIJOFS_RY;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // RZ軸
    diprg.diph.dwObj = DIJOFS_RZ;
    lpJoystick->SetProperty( DIPROP_RANGE,&diprg.diph );

    // 起動準備完了
    lpJoystick->Poll();

    // 構築完了なら
    DEBUG( "インスタンスの登録名 [%s]\n",lpddi->tszInstanceName );
    DEBUG( "製品の登録名         [%s]\n",lpddi->tszProductName );
    DEBUG( "構築完了\n");

    // 最初の1つのみで終わる
    return DIENUM_STOP;         // 次のデバイスを列挙するにはDIENUM_CONTINUEを返す
}


最初の部分のデバイスの作成とそのデバイスの入力データ形式をセット、
排他制御のセットという部分はキーボードデバイスで行った処理とほとんど同じです。

ただしCreateDeviceの第1引数はこのコールバックで渡されたLPCDIDEVICEINSTANCEguidInstanceを渡しています。
これにより列挙されたジョイスティックデバイスを構築出来たことになりますが、
実は最初からGUIDが分かっているのであれば特に列挙は必要無く、
キーボード入力のサンプルのようにCreateDeviceを直接呼び出して構築することも出来ます。


さてデバイスが作成出来たら次に入力データの形式をセットしていますが、
ここでは既に定義済みのジョイスティック形式としてc_dfDIJoystickをセットしています。
これでこのデバイスはジョイスティックデバイスであると認識され、
以降はジョイスティックの入力に合った情報を取得できるようになります。

ちなみに実はジョイスティックには2種類の入力フォーマットがあり、そのもう1つがc_dfDIJoystick2と定義されています。
実際にはボタンやアナログの数が増えただけなので、PS2などの標準コントローラーを使う分には特に意味はありませんが、
入力の多いデバイスを使用する場合はこちらの方を使う必要があります。
c_dfDIJoystickc_dfDIJoystick2のどちらを使用しても取得出来る分は取得できます

次の排他制御に関してはキーボードと同じく他のアプリにフォーカスが移行したら入力が止まるように設定しています。
なおジョイスティックはキーボードと違ってアプリを終了するような処理は行わないので、
フォーカスが無くても動作するように特に排他制御をしなくてもよいかも知れません。
この場合はSetCooperativeLevelをコメント化してしまえばOKです。

次が一番重要なジョイスティックのアナログに関する設定となりますが、
ここではSetPropertyを呼び出してアナログの各軸に対して最低値と最高値をそれぞれ-1000から+1000に設定しています。
そしてこの処理は全ての軸に対して行う必要があります。

c_dfDIJoystickはアナログは計6軸あるため、それぞれの軸に対してSetPropertyを呼び出して範囲指定を行っています。
またこの時どの軸に対して範囲を指定するかとして、diprg.diph.dwObjにオフセットマクロを使用して軸を特定しています。
c_dfDIJoystick2では全ての軸に対してオフセットマクロが定義されていないため、自前で定義を追加するか、
 またはコントローラーにどんな軸が存在しているかをコールバックを使って列挙することが出来るので、
 これを使って存在する全ての軸に対して範囲をセットします
 (EnumObjectsDIDFT_AXISを指定して呼び出す/詳しくはMSDNを参照)


もしデバイスの名前などで使用するデバイスか判定を行いたい場合は、
LPCDIDEVICEINSTANCEtszInstanceNametszProductNameを確認してください。
これは単純な文字列なのでwcscmpなどの文字列比較関数で判定できます。
※プロジェクトがUNICODEであればこれらの文字列はUNICODEとなりますが、
 ANSI版の場合はSJISとなるのでこの場合はstrcmpなどの関数を利用します


ここではコールバックの最後にDIENUM_STOPを返すことで、2個目以降は列挙しないようにしています。
もし複数のジョイスティックに対応する必要があればDIENUM_CONTINUEを返し、
さらに個数分のLPDIRECTINPUTDEVICE8を準備してください。

勘違いをしないように補足しますが、ジョイスティックが1つも無いとこのコールバック関数は一切呼ばれません。
また全てのジョイスティックの列挙が終わればもうこの関数は呼ばれません。
つまり列挙をやめるには必ずどこかでDIENUM_STOPをしないといけないというわけではありません。

■列挙関数

コールバック関数が出来たので次に実際にジョイスティックの列挙と、
最初に見つかったデバイスの構築を行ってみましょう。

なお、列挙はIDirectInput8インターフェースに実装された関数で行います。
このため必ず先にIDirectInput8を作成しておきます。

以下はIDirectInput8から列挙関数を呼び出すプログラムです。

// メインルーチン
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
        :

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

    // ジョイスティックの列挙
    ret = lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL,EnumJoyDeviceProc,NULL,DIEDFL_ATTACHEDONLY );
    if( FAILED(ret) ) {
        DEBUG( "ジョイスティックの列挙失敗\n" );
        lpDI->Release();
        return -1;
    }

    if( !lpJoystick ) {
        // ジョイスティックが1つも見つからない
        DEBUG( "ジョイスティックが1つも見つからない\n" );
        lpDI->Release();
        return -1;
    }


列挙関数の第1引数にはジョイスティックの列挙ということでDI8DEVCLASS_GAMECTRLを指定します。

第2引数には上で作ったコールバック関数のポインタを指定します。

第3引数にはコールバックの第2引数にそのまま値が渡されますが、
例えば何らかのデータをコールバックに渡したり逆にそこに値を返してもらうといったことが出来ます。
特に何も使わない場合はNULLを指定しておきましょう。

第4引数には現在繋がっているデバイスのみを列挙するようにDIEDFL_ATTACHEDONLYのみ指定しています。
現在使用できないデバイスも列挙することも可能ですが、これらのデバイスは実際には使えないため、
このフラグによって動作可能なデバイスのみを列挙するようにしています。
※フォースフィードバック対応のデバイスのみを列挙するには、
 「DIEDFL_ATTACHEDONLY|DIEDFL_FORCEFEEDBACK」とすれば良いですが、
 ここではフォースフィードバックは扱わないため必要があればMSDNなどで確認してください

EnumDevices関数は全ての列挙が終わるまで制御を返しません。
つまり、この関数から返る時というのはコールバックにてDIENUM_STOPを返したときか、
全ての列挙が完了した場合、もしくはそもそもジョイスティックが1つも無かった場合となります。

なお重要なこととして、この関数は1つもジョイスティックが無かった場合でも成功を返します。
つまり戻り値でデバイスが存在しているかどうかの判定は出来ません。

戻り値とはあくまでもこの関数の処理に対するエラー値であるため、
ジョイスティックが存在するかどうかは別な方法で判定する必要があります。

ちなみにこのサンプルでは最初に見つかったデバイスを構築するようになっていますが、
この時正しく構築されていればグローバル変数のlpJoystickにデバイスのポインタが入っているはずです。
そこでEnumDevicesから戻った後にlpJoystick変数がNULLでないかチェックすることで、
ジョイスティックが存在していたか、またジョイスティックが正しく初期化されたかを判断しています。

■ジョイスティックからの入力

ジョイスティックデバイスが構築されたらあとはそれに対してデータを取得するだけです。

これはキーボードの入力で行った処理とほぼ同様で、まずAcquireを呼び出して動作を開始させ、
それ以降は毎フレームGetDeviceStateを呼び出して入力データを取得するだけです。

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

    // メインループ
    MSG msg;
    while(1) {

        :

        // データ取得前にPollが必要なら
        if( dc.dwFlags&DIDC_POLLEDDATAFORMAT ) {
            lpJoystick->Poll();
        }

        // ジョイスティックの入力
        DIJOYSTATE joy;
        ZeroMemory( &joy,sizeof(joy) );
        HRESULT ret = lpJoystick->GetDeviceState( sizeof(joy),&joy );
        if( FAILED(ret) ) {
            // 失敗なら再び動作開始を行う
            lpJoystick->Acquire();
        }

        :


ジョイスティックからデータを取得するためそのデバイスに対してGetDeviceStateを呼び出しています。
※その前にあるPollについてMSDNの説明によると、Pollが必要なデバイスの場合に呼び出しておかないと
 データが更新されないということですが、逆にPollの必要が無いデバイスで呼び出しても問題は無いようです

引数にはこのジョイスティックに合わせたデータフォーマットに準じた構造体を指定しなければなりませんが、
今回はc_dfDIJoystickとしたので取得データはDIJOYSTATE構造体となります。

ちなみにc_dfDIJoystick2とした場合はDIJOYSTATE2としなければなりません。

以前説明したキーボードの場合はc_dfDIKeyboardとし、この時はBYTE型の256個の配列を指定しましたが、
このようにGetDeviceStateには設定したフォーマットに合わせて正しいバッファを指定する必要があります。


DIJOYSTATEには以下のようなパラメータがあります。

プロパティ 説明 USBコンバーター使用例(PS2)
LONG lX; X軸の位置 左アナログスティックの左右
LONG lY; Y軸の位置 左アナログスティックの上下
LONG lZ; Z軸の位置 右アナログスティックの左右
LONG lRx; X軸の回転値 右アナログスティックの上下
LONG lRy; Y軸の回転値 未使用
LONG lRz; Z軸の回転値 未使用
LONG rglSlider[2] 拡張版の位置情報(?) 未使用
DWORD rgdwPOV[4] 向き
※上を0度として時計回りの角度×100が入る
※未押下時は0xFFFFFFFF
十字キー
BYTE rgbButtons[32] 32個分のボタン ○×△□、各LRボタンなど

例えばjoy.lXにはアナログコントローラーの左右の状態が-1000~+1000の範囲で入っています。

またjoy.rgbButton[10]には△ボタンのON/OFF状態が入っていたりします。
ちなみにボタンのON/OFFは必ず上位1bitが立っているかで判定しなければならないため、
実際にはjoy.rgbButton[10]&0x80として上位ビットをマスクして判定します。

ここではボタンの[10]に△ボタンと書きましたが、USBコンバーターによっては違う箇所に入っていたりします。
このためそのコンバーターに合わせてボタン番号をその都度設定しなおさなければなりませんが、
通常はこういった操作をキーコンフィグという形でユーザーに設定させるような画面を作って対応します。


■デバイスの終了処理

プログラムを終了する場合などはデバイスを削除しなければなりません。
これはキーボードと同じで単純にReleaseを呼び出すだけです。

    lpJoystick->Release();
    lpDI->Release();


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

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

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

このサンプルは1つもジョイスティックが繋がっていないと起動できません。