★ DirectX Class ★ * Chapter 14: ジョイスティックの利用 *
< ジョイスティック入力の処理 >
前回のキーボードの入力に引き続き、今回はジョイスティックの入力処理を見ていきます。
『 ジョイスティック 』という言葉を聞き慣れない方もおられると思いますが、ゲームのコントローラのことです。
電気屋さんではゲームパッドと呼ばれたりしていますね。
DirectInputではゲームのコントローラのことを『 ジョイスティック 』と呼びます。
これが使えればゲームらしくなってきますね。

DirectInputを使うと、ジョイスティックの入力を簡単に扱うことができます。
また、最近のゲームは大抵アナログスティックで操作するようになっていますが、
DirectInputではこのようなアナログ入力も処理することができます。
また、最近ではもう珍しくなくなったゲームパッドの振動も扱うことができます。
但し、今回は簡単のため、アナログスティックも振動も扱いません。

それではジョイスティックの入力をDirectInputで処理するプログラムを見ていきましょう。
前回のキーボードと同様に、『 dinputex.h 』が必要です。
< 準備 >
プリプロセッサ部はキーボードのときと全く同じです。
また、今回はジョイスティックを扱うので、ジョイスティックをパソコンに接続し、
コントロールパネル等から正常に認識されていることを確認してください。
まずここで認識されていなければ、プログラム以前の問題ですので。

ジョイスティックは色々ありますが、USBで接続する¥2,000~¥4,000くらいものが適当だと思います。
あまり安いのはオススメできません。
また、ボタンの数も様々ですので、ご自身の作成されるゲームや今後購入予定のゲームのことを考えて選ばれると良いでしょう。
筆者がよく選ぶのは、プレイステーションと同じ数ボタンのあるものです。
左に十字キー、右に4ボタン、LとRの位置にそれぞれ2ボタンずつ、そして真ん中に2ボタンあるものです。
大抵10~14ボタンくらいのものを買っていると思います。

ちなみに筆者がこの講座を書いているときに使用しているジョイスティックは12ボタンです。
このシルバーのものを使用しています。
特別オススメというわけではありませんが、同じ価格帯のものにくらべ比較的壊れにくく、
またボタンの感触も固すぎす、柔らかすぎず、しっくりきます。
筆者の個人的感想ですが。

通販サイトで色々見てみるのも良いと思います。

Joshin
ビックカメラ
< グローバル変数 >
DirectInputオブジェクト、及びDirectInputデバイスのオブジェクトを宣言します。
さらに、DIDEVCAPSという構造体も宣言します。

LPDIRECTINPUT8       g_lpDI;
LPDIRECTINPUTDEVICE8 g_lpDIDevice;
DIDEVCAPS            g_diDevCaps;

グローバル変数は以上です。
< DirectInput/DirectInputデバイスの作成 >
まず最初にDirectInputオブジェクトを作成します。
これはキーボードのときと全く同じコードになります。

DirectInput8Create( hInst , DIRECTINPUT_VERSION , IID_IDirectInput8 ,
                    (void**)&g_lpDI , NULL );

続いて、デバイスを作成しますが、これはデバイスを列挙してデバイスを作成してくれるコールバック関数にて行います。
そのコールバック関数を呼び出すコードは以下の通りです。

g_lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL , EnumJoysticksCallback ,
	                 NULL , DIEDFL_ATTACHEDONLY );

第1引数には作成したいデバイスの種類を定数で設定します。ジョイスティックのときは 『 DI8DEVCLASS_GAMECTRL 』で良いようです。
第2引数にはコールバック関数を指定します。但しこのコールバック関数は自作する必要があります。
第3引数はよく分かりませんがNULLで良いようです。
第4引数は現在どんな状態のジョイスティックを検出するかを定数で指定します。

第4引数には以下のような定数を指定することができます。
他にもありますが、自分で調べてください。

定数 意味
DIEDFL_ATTACHEDONLY 現在接続されているジョイスティックのみ検出します。
DIEDFL_ALLDEVICES インストールされている全てのジョイスティックを検出します。
恐らくそのPCに接続されたことのある全てのジョイスティックが検出されるのだと思います。
DIEDFL_FORCEFEEDBACK 振動するジョイスティックのみ検出します。

さて、このコールバック関数は以下のように作成します。

BOOL CALLBACK EnumJoysticksCallback( const DIDEVICEINSTANCE *pdidInstance , VOID *pContext )
{
	HRESULT hr;
	
	hr = g_lpDI->CreateDevice( pdidInstance->guidInstance , &g_lpDIDevice , NULL );
	
	if ( FAILED( hr ) ) return DIENUM_CONTINUE;
	
	return DIENUM_STOP;
}

よく分かりませんが、サンプルにこう書いてありました!
それでその通りに書いたら動いたのでこれで良いと思います!
どういう理由でこういうコードになってるのとか細かいことは大人になれば自然に分かります。
小人とか人魚とかネッシーとか幽霊とかチュパカブラとか100キロババァとか、大人になればいないって事が分かったように。

でも・・・もし万が一、100キロババァとか本当にいたら怖いですね。
トイレ入ってるときに出てきたらどうしよう。
狭い私の一人暮らしのユニットバスに100キロで走るババァが出るんですよ?
人間が2人入るだけでも狭く感じるユニットバスを時速100キロで走るんですよ?
人間相当の質量を持ったものがユニットバスの壁にぶち当たれば当然穴が空くでしょうし、筆者に当たれば筆者は即死確実です。
かするだけでも大怪我につながる恐れがあります。
あな恐ろしや。

このコールバック関数の中には 『 CreateDevice 』がありますが、ここでデバイスの作成を行っています。
また、デバイスの作成に失敗した場合は次なるジョイスティックでデバイスの作成を試みるわけです。
そしてデバイスの作成が成功すると、そこでジョイスティックを調べるのをやめます。

次に、作成したデバイスに対してデータフォーマットを設定します。

g_lpDIDevice->SetDataFormat( &c_dfDIJoystick );

第1引数には、dinputex.h内で定義されている『 c_dfDIJoystick 』のアドレスを指定します。

そして前回同様協調レベルを設定します。

g_lpDIDevice->SetCooperativeLevel( hWnd , DISCL_EXCLUSIVE | DISCL_FOREGROUND );

前回同じなので解説は省略です。
続いて、デバイスの能力を取得します。

g_lpDIDevice->GetCapabilities( &g_diDevCaps );

最後に、デバイスに対して十字キーの範囲等を指定します。
これもデバイスの作成のときと同様にコールバック関数を呼び出して行います。

g_lpDIDevice->EnumObjects( EnumAxesCallback , (VOID*)hWnd , DIDFT_AXIS );

第1引数のEnumAxesCallbackがコールバック関数です。
第2引数は親ウィンドウのハンドルです。
第3引数は設定する項目を定数で指定します。
ゲームパッドのときはこの通りで問題ないと思います。

さて、第1引数で指定したコールバック関数は以下の通りになっています。

BOOL CALLBACK EnumAxesCallback( const DIDEVICEOBJECTINSTANCE *pdidoi , VOID *pContext )
{
	HRESULT     hr;
	DIPROPRANGE diprg;
	
	diprg.diph.dwSize       = sizeof( DIPROPRANGE );
	diprg.diph.dwHeaderSize = sizeof( DIPROPHEADER );
	diprg.diph.dwHow        = DIPH_BYID;
	diprg.diph.dwObj        = pdidoi->dwType;
	diprg.lMin              = 0 - 1000;
	diprg.lMax              = 0 + 1000;
	hr = g_lpDIDevice->SetProperty( DIPROP_RANGE , &diprg.diph );
	
	if ( FAILED( hr ) ) return DIENUM_STOP;
	
	return DIENUM_CONTINUE;
}

DIPROPRANGE構造体に各種値を入れ、
g_lpDIDevice->SetProperty( DIPROP_RANGE , &diprg.diph )でデバイスに設定しています。
失敗した場合は『 DIENUM_STOP 』を返し、成功した場合は『 DIENUM_CONTINUE 』を返します。

ここで重要なのは、『 diprg.lMin 』と『 diprg.lMax 』です。
これはジョイスティックの十字キーの値です。
diprg.lMin 』には最小値を設定します。
最小値とは、『 上 』または『 左 』ボタンが押されたときの値で、
diprg.lMax 』には最大値を設定します。
最大値とは、『 下 』または『 右 』ボタンが押されたときの値です。

そして、入力ができることを確認します。
それは以下のコードで実現します。

HRESULT hr;

hr = g_lpDIDevice->Poll();
if ( FAILED( hr ) ){
	hr = g_lpDIDevice->Acquire();
	while( hr == DIERR_INPUTLOST ){
		hr = g_lpDIDevice->Acquire();
	}
}

g_lpDIDevice->Poll()でデバイスからデータを取得できることを確認し、
確認できなかった場合にはg_lpDIDevice->Acquire()をアクセス権が取得できるまで繰り返します。

これで設定は完了です。
< 入力受付の開始・終了/DirectInputの開放処理 >
キーボードのときと全く同じです。
< 実際に入力を受け取る >
実際に入力を受け取るには以下のコードを実行します。

DIJOYSTATE js;

g_lpDIDevice->Poll();
g_lpDIDevice->GetDeviceState( sizeof( DIJOYSTATE ) , &js );

このコードを実行すると、DIJOYSTATE構造体にデータが入ります。

第1引数には受け取る構造体のサイズを、
第2引数には構造体のアドレスを渡します。

十字キーとそれ以外のボタンの扱いは少し異なります。
十字キーのデータはDIJOYSTATE構造体のメンバ『 lX 』、『 lY 』に入れられます。
lXには十字キーの左右のボタンの状態が入ります。
lYには十字キーの上下のボタンの状態が入ります。
この値は設定の際に決めた最大値と最小値によって決まります。
今回の例では設定の際に、最大値を1000、最小値を-1000としましたので、
十字キーの入力状態を調べる具体的なコードは以下のようになります。

if ( js.lY == -1000 ) /* 上ボタンが押されているとき */
if ( js.lY ==  1000 ) /* 下ボタンが押されているとき */
if ( js.lX == -1000 ) /* 左ボタンが押されているとき */
if ( js.lX ==  1000 ) /* 右ボタンが押されているとき */

それ以外のボタンに関しては、DIJOYSTATE構造体のメンバrgbButtons[32]に値が入ります。
これは1~32番までのボタンの配列で、
1番のボタンが押されていれば、rgbButtons[0]に値が、
2番のボタンが押されていれば、rgbButtons[1]に値が、
3番のボタンが押されていれば、rgbButtons[2]に値が、
それぞれ入っています。
実際のボタン番号と、配列の添え字が一つずつずれていることに留意してください。
また、この値はキーボードのときと同様、0x80との論理積が真となります。
具体的には以下のようにして取得します。

if ( js.rgbButtons[0] & 0x80 ) /* ボタン1が押されているとき */
if ( js.rgbButtons[1] & 0x80 ) /* ボタン2が押されているとき */
if ( js.rgbButtons[2] & 0x80 ) /* ボタン3が押されているとき */
if ( js.rgbButtons[3] & 0x80 ) /* ボタン4が押されているとき */
    ・
    ・
    ・

ボタン用の配列は32個までしか用意されていませんので、32ボタンまでしか対応していないものと思われます。
しかし32ボタンを超えるゲームパッドを必要とするゲームもあまりありませんし、
そんなに多かったら操作に困りますので、32ボタンまでで大丈夫だと思います。
< サンプル >
このサンプルを実行すると、真っ白なウインドウが表示されます。
そしてゲームパッドの各ボタンを押すと、ウィンドウのタイトルバーに押されたキーが表示されます。
但し、このサンプルを起動するにあたって、ゲームパッドをパソコンで接続し、
認識してからサンプルソフトを立ち上げてください。
プログラムを見てみれば分かりますが、そうしないとゲームパッドが認識されません。

また、キーボードのときには同時押し制限により、いくつかキーを同時に押すと反応しない場合がありましたが、
今回は同時押し制限はないのでゲームパッドのボタンをいくつ同時に押しても大丈夫です。
確認してみてください。

今回もウインドウハンドルをグローバルにしていますが、
タイトルバーに表示するためですので気にしないでください。

★まとめ★
これでゲームパッドでなければ操作しにくいアクションなどのゲームも作れるようになりました。
頑張ってください!

ポイントはピンク色で示してあります。

//-----------------------------------------------------------------
//
//    DirectInput Sample Program.
//
//-----------------------------------------------------------------
#define INITGUID

#include <stdio.h>
#include <windows.h>
#include <dinput.h>
#include <dinputex.h>

#pragma comment( lib , "dinput8.dll" )



//-----------------------------------------------------------------
//    Grobal Variables.
//-----------------------------------------------------------------
LPDIRECTINPUT8       g_lpDI;
LPDIRECTINPUTDEVICE8 g_lpDIDevice;
DIDEVCAPS            g_diDevCaps;

HWND                 g_hWnd;



//-----------------------------------------------------------------
//    Prototypes.
//-----------------------------------------------------------------
HWND    InitApp( HINSTANCE , int );
BOOL    InitDirectInput( HWND );
BOOL    ReadInput();
BOOL    CleanupDirectInput();
LRESULT CALLBACK WndProc( HWND , UINT , WPARAM , LPARAM );
BOOL    CALLBACK EnumJoysticksCallback( const DIDEVICEINSTANCE* , VOID* );
BOOL    CALLBACK EnumAxesCallback( const DIDEVICEOBJECTINSTANCE* , VOID* );



//-----------------------------------------------------------------
//    Main.
//-----------------------------------------------------------------
int WINAPI WinMain( HINSTANCE hInst , HINSTANCE hPrevinst , LPSTR nCmdLine , int nCmdShow )
{
	MSG msg;
	
	g_hWnd = InitApp( hInst , nCmdShow );
	if ( !g_hWnd ) return FALSE;
	
	if ( !InitDirectInput( g_hWnd ) ) return FALSE;
	
	while( msg.message != WM_QUIT ){
		if ( PeekMessage( &msg , NULL , 0 , 0 , PM_REMOVE ) ){
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}else{
			ReadInput();
		}
		Sleep( 1 );
	}
	
	return msg.wParam;
}



//-----------------------------------------------------------------
//    Initialize Application.
//-----------------------------------------------------------------
HWND InitApp( HINSTANCE hInst , int nCmdShow )
{
	WNDCLASS wc;
	HWND hWnd;
	char szClassName[] = "DirectInput Test";
	
	wc.style         = CS_HREDRAW | CS_VREDRAW;
	wc.hInstance     = hInst;
	wc.hCursor       = LoadCursor( NULL , IDC_ARROW );
	wc.hIcon         = LoadIcon( NULL , IDI_APPLICATION );
	wc.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
	wc.lpszClassName = szClassName;
	wc.lpszMenuName  = NULL;
	wc.lpfnWndProc   = WndProc;
	wc.cbWndExtra    = 0;
	wc.cbClsExtra    = 0;
	if ( !RegisterClass( &wc ) ) return FALSE;
	
	hWnd = CreateWindow( szClassName , "Direct3D Test" , WS_OVERLAPPEDWINDOW ,
	                     CW_USEDEFAULT , CW_USEDEFAULT , 640 , 480,
	                     NULL , NULL , hInst , NULL );
	if ( !hWnd ) return FALSE;
	
	ShowWindow( hWnd , nCmdShow );
	UpdateWindow( hWnd );
	
	return hWnd;
}



//-----------------------------------------------------------------
//    Initialize DirectInput.
//-----------------------------------------------------------------
BOOL InitDirectInput( HWND hWnd )
{
	HINSTANCE hInst;
	HRESULT   hr;
	
	hInst = (HINSTANCE)GetWindowLong( hWnd , GWL_HINSTANCE );
	
	hr = DirectInput8Create( hInst , DIRECTINPUT_VERSION , IID_IDirectInput8 ,
	                         (void**)&g_lpDI , NULL );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't create DirectInput object." , "Error" , MB_OK );
		return FALSE;
	}
	
	hr = g_lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL , EnumJoysticksCallback ,
	                        NULL , DIEDFL_ATTACHEDONLY );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't create Device." , "Error" , MB_OK );
		return FALSE;
	}
	
	hr = g_lpDIDevice->SetDataFormat( &c_dfDIJoystick );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't set data format." , "Error" , MB_OK );
		return FALSE;
	}
	
	hr = g_lpDIDevice->SetCooperativeLevel( hWnd , DISCL_EXCLUSIVE | DISCL_FOREGROUND );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't set cooperative level." , "Error" , MB_OK );
		return FALSE;
	}
	
	g_diDevCaps.dwSize = sizeof( DIDEVCAPS );
	hr = g_lpDIDevice->GetCapabilities( &g_diDevCaps );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't create device capabilities." , "Error" , MB_OK );
		return FALSE;
	}
	
	hr = g_lpDIDevice->EnumObjects( EnumAxesCallback , (VOID*)hWnd , DIDFT_AXIS );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't set property." , "Error" , MB_OK );
		return FALSE;
	}
	
	hr = g_lpDIDevice->Poll();
	if ( FAILED( hr ) ){
		hr = g_lpDIDevice->Acquire();
		while( hr == DIERR_INPUTLOST ){
			hr = g_lpDIDevice->Acquire();
		}
	}
	
	return TRUE;
}



//------------------------------------------------------------------------------
//    Joysticks Callback.
//------------------------------------------------------------------------------
BOOL CALLBACK EnumJoysticksCallback( const DIDEVICEINSTANCE *pdidInstance , VOID *pContext )
{
	HRESULT hr;
	
	hr = g_lpDI->CreateDevice( pdidInstance->guidInstance , &g_lpDIDevice , NULL );
	
	if ( FAILED( hr ) ) return DIENUM_CONTINUE;
	
	return DIENUM_STOP;
}



//------------------------------------------------------------------------------
//    Axes Callback.
//------------------------------------------------------------------------------
BOOL CALLBACK EnumAxesCallback( const DIDEVICEOBJECTINSTANCE *pdidoi , VOID *pContext )
{
	HRESULT     hr;
	DIPROPRANGE diprg;
	
	diprg.diph.dwSize       = sizeof( DIPROPRANGE );
	diprg.diph.dwHeaderSize = sizeof( DIPROPHEADER );
	diprg.diph.dwHow        = DIPH_BYID;
	diprg.diph.dwObj        = pdidoi->dwType;
	diprg.lMin              = 0 - 1000;
	diprg.lMax              = 0 + 1000;
	hr = g_lpDIDevice->SetProperty( DIPROP_RANGE , &diprg.diph );
	
	if ( FAILED( hr ) ) return DIENUM_STOP;
	
	return DIENUM_CONTINUE;
}



//-----------------------------------------------------------------
//    Cleanup DirectInput.
//-----------------------------------------------------------------
BOOL CleanupDirectInput()
{
	g_lpDIDevice->Unacquire();
	
	if ( g_lpDIDevice != NULL )
		g_lpDIDevice->Release();
	
	if ( g_lpDI != NULL )
		g_lpDI->Release();
	
	return TRUE;
}



//-----------------------------------------------------------------
//    Window Proc.
//-----------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hWnd , UINT msg , WPARAM wp , LPARAM lp )
{
	switch( msg ){
		case WM_DESTROY:
			CleanupDirectInput();
			PostQuitMessage( 0 );
			break;
		default:
			return DefWindowProc( hWnd , msg , wp , lp );
	}
	
	return 0L;
}



//-----------------------------------------------------------------
//    Read Input.
//-----------------------------------------------------------------
BOOL ReadInput()
{
	DIJOYSTATE js;
	HRESULT    hr;
	int        i;
	char       titlebar[32];
	char       subbuf[4];
	
	if ( NULL == g_lpDIDevice ) return FALSE;
	
	hr = g_lpDIDevice->Poll();
	if ( FAILED( hr ) ) return FALSE;
	
	hr = g_lpDIDevice->GetDeviceState( sizeof( DIJOYSTATE ) , &js );
	if ( FAILED( hr ) ) return FALSE;
	
	titlebar[0] = '\0';
	
	if ( js.lY == -1000 ) strcat( titlebar , "↑" );
	if ( js.lY ==  1000 ) strcat( titlebar , "↓" );
	if ( js.lX == -1000 ) strcat( titlebar , "←" );
	if ( js.lX ==  1000 ) strcat( titlebar , "→" );
	
	for( i=0;i<32;i++ ){
		if ( js.rgbButtons[i] & 0x80 ){
			sprintf( subbuf , ",%d" , i );
			strcat( titlebar , subbuf );
		}
	}
	
	SetWindowText( g_hWnd , titlebar );
	
	return TRUE;
}

実行結果

クリックすると実物大で表示されます。
執筆: 2009/07/20 (MON)