DirectInputによるゲームパッド実装
-初期化、終了編-


概要

このページではDirectInputによるゲームパッド実装の初期化と終了処理の説明をします。
本来は入力情報取得の部分まで行いたいのですが、長くってしまったので分けました。

今回使用するDirectInputはDirectXの入力関連を扱うAPIで、
ゲームパッドの他にキーボードやマウスなどの様々な入力デバイスを扱うことができます。
DirectInputのバージョンは8で止まっており、2020年1月の段階で更新されておらず、
現在はXInputという後継のAPIが推奨されています。

サンプル

サンプルはここからダウンロードできます。
環境については以下の内容となっています。

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9
説明 ゲームパッドを接続して起動しないと動作しません。

環境設定方法

DirectInput機能を使用するためにいくつかの設定を行う必要があります。

パス設定

DirectXのincludeファイルやlibファイルがどこにあるかの設定が必要です。
設定はメニューの「デバッグ」=>「プロジェクト名のプロパティ」で行います。
	
directx_0033
プロパティウィンドウが表示されるので「構成プロパティ」=> 「VC++ディレクトリ」を
クリックしてインクルードディレクトリとライブラリディレクトリに
パス設定を行います。

directx_0017

共通

パスの設定はまず設定したい項目をクリックすると「↓」アイコンが右端に
表示されるのでそちらをクリックします。

directx_0018
クリック後「編集」と書かれた項目が新しく表示されるので、クリックします。

directx_0019
パス設定をするウィンドウが新たに表示ます。
この画面の上部にある白い部分をダブルクリックすると右端に「…」が
表示されるのでそちらをクリックします。

directx_0020
参照先ディレクトリを決めるためのウィンドウが表示されるので、
DirectXが置かれている場所まで移動してパスを設定します。

インクルードディレクトリ

インクルードディレクトリではDirectXディレクトリの中にある
Includeディレクトリを指定します。

directx_0021

ライブラリディレクトリ

ライブラリディレクトリではDirectXディレクトリの中にある
Libファイルの中の「x86」か「x64」いずれかを指定します。

directx_0022
これはVisualStudio上段に「x86」または「x64」と書かれている項目があるので
そちらを確認して書かれている方を指定してください。

directx_0023

libファイルの設定

パスの設定が完了したら次はlibファイルの設定を行います。
方法はプロパティで指定する方法とコードで指定する方法があります。

プロパティ指定

プロパティ指定はメニューの「デバッグ」=>「プロジェクト名のプロパティ」=>
「リンカ」=>「入力」項目の「追加の依存ファイル」で行います。

directx_0024
ここにDirectInputでは「dinput8.lib」と「dxguid.lib」を追加します。

directx_0032

コード指定

コード指定の場合はファイルに以下のコードを追加します。

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

DirectXGraphicsを使う場合

上で説明している設定はあくまでDirectInputを使用するために必要な設定です。
DirectGraphicsを使用する場合は別の設定が必要となりますので、
こちらで設定を確認してください。

DirectInputの構成

DirectInputはIDirectInput8IDirectInputDevice8の
二つのCOMインターフェースにより構成されています。
※インターフェースがわからない方はインターフェースの部分をクラスに置き換えて下さい。
 DirectXを使用するだけならその認識で問題ないと思います。

directx_0034

IDirectInput8

IDirectInput8はDirectInputの大元となるインターフェースです。
IDirectInputDevice8を作成するための関数やDirectXを使用するPCが
どこまで機能を使いこなせるかをチェックする関数を持ちます。
今回のゲームパッド実装では接続されているゲームパッドを検索するためにも使います。

IDirectInputDevice8

IDirectInputDevice8はデバイス入力を管理するインターフェースです。
このインターフェースに入力デバイスの取得関数があるので、
ゲーム中の入力処理は主にこのインターフェースを使用することになります。

DirectInputの初期化

DirectInputを初期化するための流れは以下の通りです。

初期化手順

  1. IDirectInput8インターフェイスの作成
  2. ゲームパッドデバイスの列挙
  3. IDirectInputDevice8の作成
  4. デバイスのフォーマットの設定
  5. プロパティ設定
  6. 協調モードの設定
  7. デバイス制御開始
  8. ポーリング開始

①.IDirectInput8インターフェイスの作成

まずは「DirectInput8Create関数」を使用してIDirectInput8を作成します。

directx_0035
// 使用例
HRESULT hr = DirectInput8Create(instance_handle, 
				DIRECTINPUT_VERSION,
				IID_IDirectInput8,
				(void**)&g_pInputInterface,
				NULL);

if (FAILED(hr))
{
	// 失敗
}

作成が成功すると第四引き数に作成されたIDirectInput8が保存されて、
失敗するとNULLが保存されます。

第一引数にはWinMainの第一引数であるインスタンスハンドルを指定して、
残りの第二、第三、第五引数は、ほぼ固定値なのでサンプルコードの内容を
そのまま使用しても問題ありません。
無事に作成が完了したら②に進みます。

②.ゲームパッドデバイスの列挙

ゲームパッドはキーボードのように必ずPCに付属しているわけでなく、
数も一つとも限らないので接続されているゲームパッドを列挙する必要があります。
列挙はIDirectInput8の「EnumDevices関数」を使用します。
※列挙が分かりづらい人は「接続されているゲームパッドを調べてる」と考えてください。

directx_0114
// デバイスの列挙
if (FAILED(g_InputInterface->EnumDevices(
        DI8DEVTYPE_JOYSTICK,
        DeviceFindCallBack,
        &parameter,
        DIEDFL_ATTACHEDONLY
)))
{
	return false;
}

各引数の説明ですが、説明が簡単な第一、第四引数から説明します。
第一引数は、列挙したいデバイスの指定です。
今回はゲームパッドを列挙したいので「DI8DEVTYPE_GAMEPAD」を指定します。
※DI8DEVTYPE_JOYSTICKの設定もありますが、こちらを設定すると
 Xbox360などの一部のコントローラを認識しないことがあります。

第四引数の取得するデバイスの限定フラグは現状で接続状態にあるデバイスだけを
列挙したいので「DIEDFL_ATTACHEDONLY」を指定します。

第三、第四引き数はデバイスが発見された際に呼ばれるコールバック関数と
その際に渡される引数の指定をしています。
コールバック関数の書式は以下のようになっており、
関数名と引数名以外は変更することができません。

directx_0116
// デバイス発見時に実行される
BOOL CALLBACK DeviceFindCallBack(LPCDIDEVICEINSTANCE ipddi, LPVOID pvRef)
{
	return DIENUM_CONTINUE;
}

デバイスの列挙を終了する場合は戻り値で「DIENUM_STOP」
列挙を継続するなら「DIENUM_CONTINUE」を返します。

引数の第一引数はLPCDIDEVICEINSTANCEにデイバスの情報、
第二引数はLPVOIDにはEnumDevicesで渡した値が代入されています。

directx_0115
様々なメンバ変数がありますが、デバイスを作成するだけなら
「guidInstance」だけで問題ありません。
このページはDirectInput入門編の位置づけなので、デバイスの識別子である
「guidInstance」だけを使用してゲームパッドの入力取得を行います。

③.IDirectInputDevice8の作成

デバイスが無事列挙されたら、列挙されたデバイス情報を使用して
IDirectInput8の「CreateDevice関数」でIDirectInputDevice8を作成します。
IDirectInputDevice8はインプット関連のデバイス(キーボード、ゲームパッドなど)の
入力情報取得の関数や入力制御関数、デバイス情報の取得関数など、
そのデバイスを制御するための様々な関数を所持しています。

directx_0036
// CreateDevice使用例
LPDIRECTINPUTDEVICE device;

hr = g_pInputInterface->CreateDevice(
		lpddi->guidInstance,
		&device,
		NULL
);

if (FAILED(hr))
{
	// 失敗
}

第一引数のデバイスの指定は列挙されたデバイスを指定するので、
DIDEVICEINSTANCE構造体の「guidInstance」を使用してください。
第二引数は作成されたIDirectInputDevice8が代入されます。

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

IDirectInputDevice8を作成した後はIDirectInputDevice8の
「SetDataFormat関数」でフォーマットの設定を行います。
このフォーマットを指定することで、入力情報の取得で
自分達が欲しい情報を取得できるようになります。

directx_0037
// SetDataFormat使用例
// デバイスのフォーマットの設定
HRESULT hr = parameter->GamePadDevice->SetDataFormat(&c_dfDIJoystick);

if (FAILED(hr))
{
	return DIENUM_STOP;
}

今回はゲームパッドの入力設定なので、フォーマットは「c_dfDIJoystick」を指定します。
※c_dfDIJoystickよりも、更に詳細な入力情報を取得できるc_dfDIJoystick2もあります。

⑤.プロパティ設定

プロパティ設定ではアナログスティックの取得データの設定などを行い、
ゲームパッドの入力データを取得した際に開発側に都合にデータにします。
今回設定する項目は「軸モード」と「軸の値の範囲」の設定を行います。

軸モード設定

アナログスティックの軸モードの設定をします。
軸モードには「絶対値モード」「相対値モード」があります。

モード 定数 説明
絶対値モード DIPROPAXISMODE_ABS 軸の値はデバイス上の値を使用する
相対値モード DIPROPAXISMODE_REL 軸の値は前回デバイスの状態を取得した時からの相対値になる
今回は絶対値モードの設定方法の説明をします。 設定は「DIPROPDWORD構造体」に変更する情報を設定して IDirectInputDevice8の「SetProperty関数」で設定を変更します。 directx_0117 directx_0118 // 軸モードを絶対値モードとして設定 DIPROPDWORD diprop; ZeroMemory(&diprop, sizeof(diprop)); diprop.diph.dwSize = sizeof(diprop); diprop.diph.dwHeaderSize = sizeof(diprop.diph); diprop.diph.dwHow = DIPH_DEVICE; diprop.diph.dwObj = 0; diprop.dwData = DIPROPAXISMODE_ABS; // 絶対値モードの指定(DIPROPAXISMODE_RELにしたら相対値) DIPROPHEADERは「DIPROP~構造体」に必ず存在するヘッダ構造体です。 この構造体にプロパティ情報の設定方法の指定を行います。 dwHowが設定方法、dwObjが設定方法の詳細です。 今回のようにdwHowがDIPH_DEVICEの場合、dwObjは基本的に0に設定するなど、 決まった値を指定することもありますが、次の軸の値の範囲設定で これらの変数がきちんと使用されています。 DWORDのdwDataには絶対値モードの値であるDIPROPAXISMODE_ABSを代入します。 構造体の設定も完了したら、SetProperty関数を実行します。 directx_0119 // 軸モードを変更 if (FAILED(device->SetProperty(DIPROP_AXISMODE, &diprop.diph))) { return false; } 第一引数のプロパティの種類は絶対モード値の指定をするので「DIPROPAXISMODE_ABS」を 第二引数はDIPROPHEADERを指定します。 無事成功したら軸モードの設定は終了です。

軸の値の範囲定

次は軸の値の範囲設定を行います。
軸の値とはアナログスティックを傾けた時の値で、この値の最小と最大を決めます。
値を決めることで、全てのデバイスでスティックがどの程度倒されたかを統一できます。
構造体は「DIPROP_RANGE」を使い、関数は「SetProperty関数」を使用します。
軸の値の設定方法はいくつかありますが、今回は単純な方法を採用しています。

directx_0120
// X軸の値の範囲設定
DIPROPRANGE diprg;
ZeroMemory(&diprg, sizeof(diprg));
diprg.diph.dwSize = sizeof(diprg);
diprg.diph.dwHeaderSize = sizeof(diprg.diph);
diprg.diph.dwHow = DIPH_BYOFFSET;
diprg.diph.dwObj = DIJOFS_X;
diprg.lMin = -1000;
diprg.lMax = 1000;
if (FAILED(device->SetProperty(DIPROP_RANGE, &diprg.diph)))
{
	return false;
}

// Y軸の値の範囲設定
diprg.diph.dwObj = DIJOFS_Y;
if (FAILED(device->SetProperty(DIPROP_RANGE, &diprg.diph)))
{
	return false;
}

DIPROPRANGE構造体で「lMin」と「lMax」で最小値と最大値の設定します。
DIPROPHEADER構造体はdwHowに「DIPH_BYOFFSET」をdwObjに
「DIJOFS_X or DIJOFS_Y」を指定します。

directx_0121
上の図はあくまでイメージですが、DIPH_BYOFFSETを指定すると
プロパティにオフセット位置を指定してアクセスします。
そしてアクセスする位置を指定しているのがdwObjです。
DIJOFS_XやDIJOFS_YはX軸やY軸の値の範囲などの情報までの
オフセット値になっており、SetPropertyを使用することで
その部分の変更を行うことができます。

⑥.協調モードの設定

次は協調モードの設定を行います。
協調モードとは入力デバイスとシステム間の関係を設定することです。
ここでは「バックグラウンドモード or フォアグラウンドモード」と
「排他モード or 非排他モード」のいずれかを選択します。
設定はIDirectInputDevice8の「SetCooperativeLevel関数」を使用しますが
その前に、フォアグラウンドとバックグラウンド、排他と非排他の説明します。
フォアグラウンドとバックグラウンドの違いは以下の通り、
ウィンドウがアクティブの状態でデバイス取得ができるか、否かです。

フォアグラウンドとバックグラウンドの特性
モード 定数 特性
フォアグラウンド DISCL_FOREGROUND ウィンドウがバックグラウンドに移動したら
デバイスの取得ができない
バックグラウンド DISCL_BACKGROUND ウィンドウが非アクティブ状態でも
デバイスが取得できる
次に排他的と非排他的の違いは以下の通り、ゲーム起動中に他のアプリケーションでも キーボードなどの入力のデバイスの取得を許可するかどうかを決めます。
排他的と非排他的の特性
モード 定数 特性
排他的 DISCL_EXCLUSIVE 他のアプリケーションは
その入力デバイスを取得できない
非排他的 DISCL_NONEXCLUSIVE 他のアプリケーションでも
そのまま入力デバイスの取得ができる
最後にSetCooperativeLevel関数の詳細は以下の通りです。 directx_0038 // 協調モードの設定 if (FAILED(device->SetCooperativeLevel( FindWindow(WINDOW_CLASS_NAME, nullptr), DISCL_EXCLUSIVE | DISCL_FOREGROUND) )) { // 失敗 } 今回の設定ではフォアグラウンドの排他設定をしているので、 ウィンドウがアクティブ中のみ情報の取得ができ、 別のアプリケーションでは使用しているゲームパッドの入力は取得できません。 これでDirectInputの初期化は完了したので、最後の⑦に進みます。

⑦.デバイス制御開始

ここまでの処理ですべての必要な設定が完了したので最後にデバイスの入力制御を開始します。
この処理を実行することで、デバイスで入力された内容の取得ができるようになります。
制御開始はIDirectInputDevice8の「Acquire関数」を使用します。

directx_0039
// 制御開始
HRESULT hr = g_pKeyDevice->Acquire();

if (FAILED(hr))
{
	// 制御開始失敗
}

制御開始も無事成功したら、最後にポーリングを行います。

ポーリング開始

最後にデバイスのポーリングをIDirectInputDevice8の「Poll関数」を使用して行います。
ポーリングとは競合の回避や処理を同期する際に使用される用語で、
今回は一定間隔でデバイスの情報を同期させて取得できるようにするために使用します。

directx_0124
// ポーリング開始
HRESULT hr = g_pKeyDevice->Poll();
if (FAILED(hr))
{
	// 失敗
}

Acquire実行前に行っているサンプルが多かったのですが、
いくつかのデバイスが失敗してしまったので、Acquire実行後に行っています。
これで、初期化処理は全て終了です。

DirectInputの終了

DirectInputの終了も他のDirectXAPIと同様にプログラム終了時には
インターフェース等を解放する必要があります。
解放は以下の順番で行っていきます。

解放手順

  1. デバイス制御の停止
  2. IDirectInputDevice8の解放
  3. IDirectInput8の解放

①.デバイス制御の停止

終了時にまず行うことは入力デバイスの制御を停止させることです。
これは複数の入力デバイスを用意していた場合は全て停止させます。
停止にはIDirectInputDevice8の「Unacquire関数」を使用します。

directx_0041
// Unacquire関数使用例
g_pKeyDevice->Unacquire();

これで入力デバイスが停止したので安全にIDirectInputDevice8の解放ができます。

②.IDirectInputDevice8の解放

入力デバイスの制御が完了したら、IDirectInputDevice8の「Release関数」を使用して、
IDirectInputDevice8の解放を行います。
この解放もデバイス制御の停止と同様に複数の入力デバイスを用意していた場合は
全てのデバイスを解放してください。

directx_0042
// IDirectInputDevice8の解放
g_pKeyDevice->Release();

COMインターフェースは参照カウンタというインターフェースを
保持しているオブジェクトの数をカウントする値を持っています。
これはCOMインターフェース作成時やAddRefなどにより加算されて、
Releaseなどを実行することで減算されます。
解放はこの参照カウンタが0になったタイミングで行われます。
これでIDirectInputDevice8の解放が終わったので、③に進みます。

③.IDirectInput8の解放

最後はDirectInputの大元であるIDirectInput8を
IDirectInput8の「Release関数」を使用して解放します。

directx_0042
// Release使用例
g_pInputInterface->Release();

これでDirectInputの解放が無事完了しました。
初期化と終了処理の実装、説明は終わりましたので、
次は「入力情報の取得と扱い方」に進んでください。