DirectInputとWinAPIによるマウス実装

概要

最終更新日:2020/02/16

DirectInputとWinAPIを使用したマウスの実装方法の説明を書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
  • DirectInputの基本を知りたい
  • マウスのクリック情報を取得する方法を知りたい
  • マウスの座標が知りたい
  • マウスの移動量が知りたい

サンプル

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

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9
説明 マウスの位置に画像が表示されます。
Input.cppに入力処理は書いています。


DirectInputとは

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

環境設定方法

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. IDirectInputDevice8の作成
  3. デバイスのフォーマットの設定
  4. 協調モードの設定
  5. デバイス制御開始
  6. ポーリング開始

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

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

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

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

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

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

②.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 = g_MouseDevice->SetDataFormat(&c_dfDIMouse);

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

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

④.協調モードの設定

次は協調モードの設定を行います。
協調モードとは入力デバイスとシステム間の関係を設定することです。
ここでは「バックグラウンドモード 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_NONEXCLUSIVE | 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の解放が無事終了です。

取得方法

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

directx_0040

この関数は引数に取得するデバイスの入力情報保存用の変数と
その変数のサイズの指定をしますが、どのデバイスで取得するかの指定はしません。
何故かというと、取得するデバイスの指定は初期化時の「SetDataFormat関数」で
完了しているからです。
あの関数の引数でGetDeviceStateを使用時に取得できる情報が決まっています。
以下は定数と取得時に必要とする変数の型についての表です。

デバイス毎の入力データ
定数 対応デバイス
c_dfDIKeyborad キーボード バイト型の配列(要素数256)
c_dfDIMouse マウス DIMOUSESTATE構造体
c_dfDIJoystick ゲームパッド DIJOYSTATE構造体
c_dfDIJoystick2 ゲームパッド DIJOYSTATE2構造体
今回のサンプルや説明ではc_dfDIMouseを使用していますので、 入力情報の取得にはDIMOUSESTATE構造体が必要です。 directx_0125 // マウスの状態を取得 HRESULT hr = g_MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE), &g_CurrentMouseState); if (FAILED(hr)) { // 取得失敗 } これで第二引数のDIMOUSESTATE構造体に入力情報が保存されるので、 この情報を使用して入力判断が可能になります。

クリック取得

クリック情報を取得するにはGetDeviceStateで取得したDIMOUSESTATEを使用します。
この構造体の「rgbButtonsメンバ」にマウスの各ボタンのクリック状況が保存されています。
rgbButtonsメンバの値が「0ならクリックされておらず
0x80にビットが立っていたらクリックされています」。

// 左クリックされているか判定
if (g_CurrentMouseState.rgbButtons[0] & (0x80))
{
	// 左クリック中
}

クリックした瞬間とやめた瞬間の判定

クリックした瞬間とクリックをやめた瞬間の判定の判定方法は
いくつかありますが、今回は以下の手順による方法で実装します。
  1. DIMOUSESTATEを二つ用意する
  2. マウス情報を更新する(新旧を含む)
  3. 判定関数を用意する

DIMOUSESTATEを二つ用意する

まず、DIMOUSESTATEの変数を二つ用意します。
この時の変数はローカル変数以外で用意してください。
二つの変数の役割は「マウスの最新と一フレーム前の情報の保存」です。

static DIMOUSESTATE g_CurrentMouseState;		//!< マウスの現在の入力情報
static DIMOUSESTATE g_PrevMouseState;			//!< マウスの一フレーム前の入力情報

マウス情報を更新する(新旧を含む)

GetDeviceStateで最新のマウス情報を更新する前に
最新のマウス情報を一フレーム前のマウス情報に代入します。

// 更新前に最新マウス情報を保存する
g_PrevMouseState = g_CurrentMouseState;

// 最新のマウスの状態を更新
HRESULT	hr = g_MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE), &g_CurrentMouseState);
if (FAILED(hr))
{
	g_MouseDevice->Acquire();
	hr = g_MouseDevice->GetDeviceState(sizeof(DIMOUSESTATE), &g_CurrentMouseState);
}

判定関数を用意する(クリックした瞬間)

クリックした瞬間の判定関数を用意します。
判定は以下の二つの条件がどちらも満たされていた場合に
クリックした瞬間として判定します。
  • 一フレーム前のマウスのボタンがクリックされていない
  • 最新のマウスのボタンがクリックされている
// クリックした瞬間の判定
bool OnMouseDown(MouseButton button_type)
{
	if (!(g_PrevMouseState.rgbButtons[button_type] & MOUSE_ON_VALUE) &&
		g_CurrentMouseState.rgbButtons[button_type] & MOUSE_ON_VALUE)
	{
		return true;
	}

	return false;
}

判定関数を用意する(クリックをやめた瞬間)

クリックをやめた瞬間の判定関数を用意します。
判定は以下の二つの条件がどちらも満たされていた場合に
クリックをやめた瞬間として判定します。
  • 一フレーム前のマウスのボタンがクリックされている
  • 最新のマウスのボタンがクリックされていない
// クリックをやめた瞬間の判定
bool OnMouseUp(MouseButton button_type)
{
	if (g_PrevMouseState.rgbButtons[button_type] & MOUSE_ON_VALUE &&
		!(g_CurrentMouseState.rgbButtons[button_type] & MOUSE_ON_VALUE))
	{
		return true;
	}

	return false;
}

座標取得

マウスの座標取得はDirectInputではできません。
座標を取得できるAPIが用意されているのはWinAPIです。
座標取得の手順は以下の通りです。
  1. マウス座標取得
  2. クライアント領域座標変換
まず「GetCursorPos関数」を使用してマウスの座標を取得します。

directx_0126
// マウス座標を取得する
POINT p;
GetCursorPos(&p);

取得した座標はスクリーン座標(デスクトップ上の座標)です。
その為、ウィンドウ上の座標に変換する必要があります。
この変換には「ScreenToClient関数」を使用します。

directx_0127
// スクリーン座標をクライアント座標に変換する
ScreenToClient(FindWindowA(WINDOW_CLASS_NAME, nullptr), &p);

変換された座標をグローバル座標などに代入しておけば
いつでもマウス座標を取得できます。

移動量取得

マウスが一フレームにどれほど移動したかどうかを知りたい時があります。
その時はDIMOUSESTATEのメンバ変数「lX」「lY」を使用します。

// 移動量の取得
Vec2 GetMouseVelocity()
{
	return Vec2((float)g_CurrentMouseState.lX, (float)g_CurrentMouseState.lY);
}
以上でマウスのクリック判定、座標、移動量の説明は終了です。
お疲れ様でした。