DirectInputによるキーボード入力情報取得の実装


概要

DirectInputはDirectXの入力関連を扱うAPIです。
DirectInputで使用できる入力デバイスはキーボード、マウス、ゲームパッドなど、
様々なデバイスを扱うことができます。
今回はこのDirectInputを使用してキーボードの入力情報の取得を行います。
※DirectInputのバージョンは8で止まっており、9以降では更新されておらず、
 現在はXInputという後継のAPIがでています。

サンプル

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

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9
備考 x86で起動してください

環境設定方法

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インターフェースにより構成されています。
IDirectInput8はDirectInputの大元で、IDirectInputDevice8は入力情報を
取得する関数などを持っています。
※インターフェースがわからない方はインターフェースの部分をクラスに置き換えて下さい。
 DirectXを使用するだけならその認識で問題ないと思います。

directx_0034

IDirectInput8

IDirectInput8はDirectInputの大元となるインターフェースです。
IDirectInputDevice8を作成するための関数やDirectXを使用するPCが
どこまで機能を使いこなせるかをチェックする関数を持ちます。

IDirectInputDevice8

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

DirectInputの初期化

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

初期化手順
IDirectInput8インターフェイスの作成
IDirectInputDevice8の作成
デバイスのフォーマットの設定
協調モードの設定
デバイス制御開始

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

DirectInputを使用する場合、最初にIDirectInput8を作成します。
IDirectInput8はDirectX9におけるDirectInputの大元になりますが
この後に作成するIDirectInputDevice8を作成した後はほとんど使用しません。
作成にはIDirectInput8を生成する「DirectInput8Create関数」を使います。

directx_0035

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

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

作成が成功すると第四引き数に作成されたIDirectInput8が保存されて、
失敗するとnullptrが保存されます。
無事に作成が完了したら②に進みます。

②.IDirectInputDevice8の作成

IDirectInput8を作成したあとはIDirectInputDevice8の作成をします。
IDirectInputDevice8はインプット関連のデバイス(キーボード、ゲームパッドなど)の
デバイスを管理しており、それらのデバイスの入力情報取得もこのデバイスから行います。
作成はIDirectInput8の持つ「CreateDevice関数」を使います。

directx_0036
// CreateDevice使用例
LPDIRECTINPUTDEVICE device;

hr = g_pInputInterface->CreateDevice(
			GUID_SysKeyboard,
			&device,
			NULL);
if (FAILED(hr))
{
	// 失敗
}

作成が成功すると第二引数に作成されたIDirectInputDevice8が保存されて、
失敗するとnullptrが保存されます。
無事に作成が成功したら③に進みます。

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

入力デバイスを指定してIDirectInputDevice8を作成した後は設定を行います。
CreateDeviceによってIDirectInputDevice8で使用するデバイスの指定は終わっていますが
フォーマットの設定は完了しておらず、今のままでは正常な入力情報の取得ができません。
フォーマットの設定関数はIDirectInputDevice8の「SetDataFormat関数」を使用します。

directx_0037
// SetDataFormat使用例
// デバイスのフォーマットの設定
HRESULT hr = g_pKeyDevice->SetDataFormat(&c_dfDIKeyboard);
if (FAILED(hr))
{
	// 失敗
}

これでフォーマットの指定が完了しました。
次は④に進み協調モードの設定を行います。

④.協調モードの設定

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

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

デバイス制御開始

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

directx_0039
// Acquire使用例
HRESULT hr = g_pKeyDevice->Acquire();

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

制御開始も無事終了したら、次はキーボードのキー情報を取得する方法の説明をします。

入力情報の取得

入力デバイスの入力情報は基本的に毎フレーム取得します。
そして、その情報を使用して「入力された瞬間」「入力中」「入力終了」を判断します。
ただし、入力情報は入力デバイスのボタンを押されたか押されていないかしかわかりませんので、
「入力された瞬間」や「入力終了」は開発側で判定するようにしなければいけません。
※1.ゲームパッドではアナログスティックをどの程度倒しているかという情報の取得ができます。
※2.この記事はあくまでDirectInputの基本となっており、
   入力制御についてはこちらで書いてます。

キーボードの入力データの取得方法の前に知っておいた方がいい情報の
「入力情報の保存方法」と「キーボード定数」の説明を先にします。

入力情報の保存方法

以下のようにデバイスによって保存する構成が構造体であったり、
配列であったりと異なります。

デバイス毎の入力データ
デバイス 入力データ
キーボード c_dfDIKeyboradを指定した場合は
バイト型の配列(要素数256)を用意
マウス c_dfDIMouseを指定した場合は
DIMOUSESTATE構造体を用意
ゲームパッド c_dfDIJoystickを指定した場合は
DIJOYSTATe構造体を用意
今回はキーボードを使用するので入力データの取得が無事完了した場合は バイト型の配列に入力情報が保存されます。

キーボード定数

入力デバイスがキーボードの場合、どのキーが押されたかどうかのための値を
DirectInput側が用意してくれているのでそちらを使用して判定します。

定数の一例
キー 定数
右矢印キー DIK_RIGHT
左矢印キー DIK_LEFET
Enterキー DIK_RETURN
Aキー DIK_A
この定数を使用して取得した入力データからキーが入力されたかを判断します。

取得方法

キーボードの入力情報のIDirectInputDevice8の「GetDeviceState関数」を使用します。

directx_0040
// GetDeviceState関数使用例
// キー情報取格納用
BYTE KeyState[256];
HRESULT hr;

// キーボードデバイスのゲッター
hr = g_pKeyDevice->GetDeviceState(256, KeyState);
if (SUCCEEDED(hr))
{
	// 1フレーム前のキー情報の確保
	DWORD old = g_InputState.now;	

	// キー情報クリア
	g_InputState.now = CLEAR_KEY;	

	// 上キー
	if (KeyState[DIK_UP] & 0x80)
	{
		g_InputState.now |= UP_KEY; 
	}

	// 下キー
	if (KeyState[DIK_DOWN] & 0x80)
	{
		g_InputState.now |= DOWN_KEY; 
	}

	// 左キー
	if (KeyState[DIK_LEFT] & 0x80)
	{
		g_InputState.now |= LEFT_KEY; 
	}

	// 右キー
	if (KeyState[DIK_RIGHT] & 0x80)
	{
		g_InputState.now |= RIGHT_KEY; 
	}

	// リターンキー
	if (KeyState[DIK_RETURN] & 0x80)
	{
		g_InputState.now |= RETURN_KEY; 
	}

	// トリガー情報取得
	g_InputState.trg = (g_InputState.now & (~old));
	// 逆トリガー情報取得
	g_InputState.ntrg = (~g_InputState.now) & old;

// デバイスロスト時は再度制御開始を呼ぶ
} else if (hr == DIERR_INPUTLOST) {
	g_pKeyDevice->Acquire();
}

上のコードで各ボタンとの入力判定で0x80が使われているのは
取得したデバイスでボタンが押されている場合は
1バイトの最上位ビットが1となっているからです。

DirectInputの終了

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

解放手順
デバイス制御の停止
IDirectInputDevice8の解放
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の解放が無事完了しました。