DirectX Class
Chapter 16: 振動機能の利用
* Chapter List *
* Others *
ゲームパッドの振動機能
今回から思い切ってデザインを一新してみました。
特に何かあったわけではありませんが、最初のデザインからもう1年以上経っていますので、
そろそろ新しいデザインでいこうかなと思いました。
4時間ほどでぱぱっと作りましたので、どこかおかしいかもしれません。
しかし、行間や文字間を空けているので、前よりは読みやすくなったと思います。

ゲームパッドの振動機能を利用してみましょう。
これによって今までよりも臨場感溢れるゲームを開発することができます。

最近のゲームでは最早当たり前となった振動機能ですが、今までの視覚聴覚に加えて触覚というチャネルも利用するため、
今までにない臨場感となるでしょう。

DirectXではこの振動機能のことを『フォースフィードバック』と呼びます。

尚、振動機能を持っているゲームパッドでなければ意味がありませんので、
この講座のサンプルを試される方は、お使いのゲームパッドが振動機能に対応しているかどうか確認してください。
対応していなければ諦めるか買うかしてください。
何でもタダでできると思ったら大間違いです。

ちなみに、振動機能があっても反応しない場合があります。
これは、ゲームパッドのドライバが正常にインストールされていない可能性があります。
Windowsは基本的に、未知のUSBデバイスが接続されると、一番合いそうなドライバを自動で設定してくれます。
しかし、このドライバは性能が完全でない場合が多く、振動機能をサポートしていないという可能性が考えられます。
その場合はドライバをインストールしてからお試しください。
「何か知らないけど買ってきてつないだら使えたにょ☆」
という方はご注意ください。
グローバルなオブジェクトの宣言
いつものLPDIRECTINPUT8LPDIRECTINPUTDEVICE8DIDEVCAPSに加えて、
エフェクトオブジェクトLPDIRECTINPUTEFFECT、フォースフィードバックのDWORDを宣言する必要があります。
具体的なコードについては以下のようになります。

LPDIRECTINPUT8       g_lpDI       = NULL;
LPDIRECTINPUTDEVICE8 g_lpDIDevice = NULL;
LPDIRECTINPUTEFFECT  g_lpDIEffect = NULL;
DIDEVCAPS            g_diDevCaps;
DWORD                g_dwNumForceFeedbackAxis;

これで宣言は終了です。
続いて、初期化の方法を見ていきましょう。
DirectInputの初期化
まず、DirectInputオブジェクトを作成します。
これは前回までと同じコードになります。

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

続いて、使用可能なジョイスティックを列挙してデバイスを作成しますが、
ここは前回と少し違い、列挙するデバイスを指定する第4引数に『DIEDFL_FORCEFEEDBACK』を指定します。
これはフォースフィードバック機能のあるジョイスティックを列挙することを意味します。

g_lpDI->EnumDevices( DI8DEVCLASS_GAMECTRL , EnumFFDevicesCallback ,
                     NULL , DIEDFL_FORCEFEEDBACK | DIEDFL_ATTACHEDONLY );

また、このメソッド内で指定しているコールバック関数『EnumFFDevicesCallback』は以下のようになっています。
BOOL CALLBACK EnumFFDevicesCallback( const DIDEVICEINSTANCE *pInst , VOID *pContext )
{
	HRESULT hr;
	
	hr = g_lpDI->CreateDevice( pInst->guidInstance , &g_lpDIDevice , NULL );
	if ( FAILED( hr ) ) return DIENUM_CONTINUE;
	
	return DIENUM_STOP;
}

フォースフィードバックを利用可能なジョイスティックが見つかったらこの関数が呼ばれます。
デバイスの作成を試みて、成功すればそこで終了し、失敗すれば次を試すという単純なものです。
尚、g_lpDI->EnumDevicesの正常終了後であっても、結局デバイスが作成されたかどうかを自分で確認する必要があります。
デバイスが作成された場合にはg_lpDIDeviceがNULL以外となっています。

そして、データフォーマットの設定、協調レベルの設定を行います。
これは前章と全く同じです。

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

続いて、デバイスのプロパティを設定します。
ここは前章までと少し違います。
第2引数に前回はウィンドウハンドルを渡していましたが、今回はg_dwNumForceFeedbackAxisのアドレスを渡します。

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

さて、『EnumAxesCallback』は以下のようになっています。

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;
	
	DWORD *pdwNumForceFeedbackAxis = (DWORD*)pContext;
	if ( ( pdidoi->dwFlags & DIDOI_FFACTUATOR) != 0 ) (*pdwNumForceFeedbackAxis)++;
	
	return DIENUM_CONTINUE;
}

軸の最大値、最小値を設定するまでは前章とほとんど同じですが、
今回はフォースフィードバックの軸をカウントしています。

以上でフォースフィードバックのためのジョイスティックの設定は終わりです。
続いて振動のためのエフェクトを作成してみましょう。
エフェクトの作成
エフェクトを作成するには、まず軸と方向を定義する必要があります。
それは以下のようにします。

DWORD rgdwAxes[2]     = { DIJOFS_X , DIJOFS_Y };
LONG  rglDirection[2] = { 1 , 1 };

ちなみにこのrglDirectionの値を変化させると、左右の振動のバランスを変えられます。
どちらかを0にすると、0にしたほうは振動しません。
{ 0 , 1 }なら右手のみ振動し、{ 1 , 0 }なら左手のみ振動します。

次に、DIEFFECT構造体に値を設定します。

DICONSTANTFORCE cf;
DIEFFECT        eff;

ZeroMemory( &eff , sizeof( eff ) );
eff.dwSize                  = sizeof( DIEFFECT );
eff.dwFlags                 = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS;
eff.dwDuration              = INFINITE;
eff.dwSamplePeriod          = 0;
eff.dwGain                  = DI_FFNOMINALMAX;
eff.dwTriggerButton         = DIEB_NOTRIGGER;
eff.dwTriggerRepeatInterval = 0;
eff.cAxes                   = g_dwNumForceFeedbackAxis;
eff.rgdwAxes                = rgdwAxes;
eff.rglDirection            = rglDirection;
eff.lpEnvelope              = 0;
eff.cbTypeSpecificParams    = sizeof( DICONSTANTFORCE );
eff.lpvTypeSpecificParams   = &cf;
eff.dwStartDelay            = 0;

細かい話は抜きにします。
エフェクトは今の構造体を利用して、以下のように作成します。

g_lpDIDevice->CreateEffect( GUID_ConstantForce , &eff , &g_lpDIEffect , NULL );

アプリケーション終了時にエフェクトを開放するのをお忘れなく。
開放は以下のようにして行います。

if ( g_lpDIEffect != NULL ) g_lpDIEffect->Release();

エフェクトの開放はデバイスの開放よりも前に行ってください。

これで準備は全て整いました。
次はエフェクトの開始と終了を見てみましょう。
エフェクトの開始と終了
エフェクトを開始するには、以下のメッソドを使用します。

g_lpDIEffect->Start( 1 , 0 );

このメソッド一発でエフェクトが始まります。
第1引数にはエフェクトを再生する回数を指定します。
今回はエフェクトの設定が無限回になっていますので、1を指定すればOKです。
第2引数にはエフェクトの再生方法を指定します。
0を指定すると、すでに他のエフェクトが開始されている場合に、そのエフェクトと混合されます。
BGMが鳴っているところに効果音が混ざって聞こえるのと同じ原理だと理解してください。
DIES_SOLOを指定すると、他のエフェクトを全て停止してからエフェクトが実行されます。
他のエフェクトと混ざって欲しくないときに使用します。

エフェクトを終了するには、以下のメソッドを使用します。

g_lpDIEffect->Stop();

これでエフェクトを終了させることができます。
サンプル
このサンプルを実行すると、真っ白なウィンドウが表示されます。
そしてゲームパッドの『 1 』ボタンを押すとゲームパッドが振動を始め、『 2 』ボタンを押すと停止します。

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

//-----------------------------------------------------------------
//
//    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       = NULL;
LPDIRECTINPUTDEVICE8 g_lpDIDevice = NULL;
LPDIRECTINPUTEFFECT  g_lpDIEffect = NULL;
DIDEVCAPS            g_diDevCaps;
DWORD                g_dwNumForceFeedbackAxis;

BOOL                 g_effectExist = FALSE;


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



//-----------------------------------------------------------------
//    Main.
//-----------------------------------------------------------------
int WINAPI WinMain( HINSTANCE hInst , HINSTANCE hPrevinst ,LPSTR nCmdLine , int nCmdShow )
{
	MSG  msg;
	HWND hWnd;
	
	hWnd = InitApp( hInst , nCmdShow );
	if ( !hWnd ) return FALSE;
	
	if ( !InitDirectInput( 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 , "Force Feedback 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;
	DIPROPDWORD dipdw;
	
	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 , EnumFFDevicesCallback ,
	                          NULL , DIEDFL_FORCEFEEDBACK | DIEDFL_ATTACHEDONLY );
	if ( FAILED( hr ) || ( g_lpDIDevice == NULL ) ){
		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;
	}
	
	hr = g_lpDIDevice->EnumObjects( EnumAxesCallback ,
	                                (VOID*)&g_dwNumForceFeedbackAxis ,
	                                DIDFT_AXIS );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't set axis." , "Error" , MB_OK );
		return FALSE;
	}
	if ( g_dwNumForceFeedbackAxis > 2 ) g_dwNumForceFeedbackAxis = 2;
	
	if ( !CreateEffect( hWnd ) ){
		MessageBox( hWnd , "Can't create effect." , "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;
}



//------------------------------------------------------------------------------
//    Force Feedback Callback.
//------------------------------------------------------------------------------
BOOL CALLBACK EnumFFDevicesCallback( const DIDEVICEINSTANCE *pInst , VOID *pContext )
{
	HRESULT hr;
	
	hr = g_lpDI->CreateDevice( pInst->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;
	
	DWORD *pdwNumForceFeedbackAxis = (DWORD*)pContext;
	if ( ( pdidoi->dwFlags & DIDOI_FFACTUATOR) != 0 ) (*pdwNumForceFeedbackAxis)++;
	
	return DIENUM_CONTINUE;
}



//-----------------------------------------------------------------
//    Create Effect.
//-----------------------------------------------------------------
BOOL CreateEffect( HWND hWnd )
{
	DWORD           rgdwAxes[2]     = { DIJOFS_X , DIJOFS_Y };
	LONG            rglDirection[2] = { 1 , 1 };
	DICONSTANTFORCE cf;
	DIEFFECT        eff;
	HRESULT         hr;
	
	ZeroMemory( &eff , sizeof( eff ) );
	eff.dwSize                  = sizeof( DIEFFECT );
	eff.dwFlags                 = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS;
	eff.dwDuration              = INFINITE;
	eff.dwSamplePeriod          = 0;
	eff.dwGain                  = DI_FFNOMINALMAX;
	eff.dwTriggerButton         = DIEB_NOTRIGGER;
	eff.dwTriggerRepeatInterval = 0;
	eff.cAxes                   = g_dwNumForceFeedbackAxis;
	eff.rgdwAxes                = rgdwAxes;
	eff.rglDirection            = rglDirection;
	eff.lpEnvelope              = 0;
	eff.cbTypeSpecificParams    = sizeof( DICONSTANTFORCE );
	eff.lpvTypeSpecificParams   = &cf;
	eff.dwStartDelay            = 0;
	
	hr = g_lpDIDevice->CreateEffect( GUID_ConstantForce , &eff ,
	                                    &g_lpDIEffect , NULL );
	if ( FAILED( hr ) ){
		MessageBox( hWnd , "Can't create effect." , "Error" , MB_OK );
		return FALSE;
	}
	
	return TRUE;
}



//-----------------------------------------------------------------
//    Cleanup DirectInput.
//-----------------------------------------------------------------
BOOL CleanupDirectInput()
{
	g_lpDIDevice->Unacquire();
	
	if ( g_lpDIEffect != NULL )
		g_lpDIEffect->Release();
	
	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;
	
	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;
	
	if ( js.rgbButtons[0] & 0x80 ){
		if ( !g_effectExist ){
			g_lpDIEffect->Start( 1 , 0 );
			g_effectExist = TRUE;
		}
	}
	if ( js.rgbButtons[1] & 0x80 ){
		if ( g_effectExist ){
			g_lpDIEffect->Stop();
			g_effectExist = FALSE;
		}
	}
	
	return TRUE;
}

実行結果

クリックすると実物大で表示されます。