[UE4]RawInputを使ってDirectInputのゲームパッドを動かす

# 概要
UE4は基本的にXInputでのみ動作するようになっている。
しかし昨今のPCゲームでは手持ちのPS4コントローラや従来品のDirectInputコントローラを使用することも多く、DirectInput対応を入れる開発者も多いだろう。

UE4公式のRawInputプラグインを用いれば簡単にDirectInputのゲームパッドを使えるようにできる！わけもなく。
当記事はいろいろ試して動くようにした忘備録である。

# 開発環境
UE4.27
VisualStadio2019
PS4ワイヤレスコントローラ
LogicoolF310コントローラ

# 実装

### プラグイン設定
RawInputのプラグインを有効にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/fa7357f1-1aaa-208a-c785-dd067efee1e7.png)

プロジェクト設定からRawInputで認識するデバイスを登録する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/7254be77-3cda-fbb4-05b6-780f1601d6c7.png)
この設定ではLogicoolF310とワイヤレスコントローラの2つのデバイスを登録している。
VendorIDとProductIDは、デバイスマネージャーから取得した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/84945a62-64d5-aad4-3c98-7429762b86b2.png)
この設定を行えばゲーム側で最低限DirectInputの入力イベントをとることができるようになる。

### 入力値の確認

細かい設定を行う前に、RawInputが提供しているコマンド `showdebug  rawinput` を使って入力がどう取得されるのか確認してみる。
logicoolコントローラを接続した状態で何も押していない状態（＝ニュートラル）ではこんな入力値になっているようだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/a7699e3f-e8a9-06c9-77e1-6d1a241feba4.png)
Button1~Button20がFaceButtonやLB,RBなどのデジタル入力、Axis1~Axis8がスティックなどのアナログ入力となっている。
しかしこの表示をみるだけでは、Buttonの何番にパッドのどのキーが割り振られているかがわからない。
実際にパッドを押してみてどこが反応するか見るしかないようだ。

また、アナログ入力(Axis1~4)のニュートラル値が0.5となっている。
これはXInputが(-1.0)~(1.0)の入力が取れるのに対して、DirectInputでは(0.0)~(1.0)が取れているからだ。
このままでは使いづらいため、RawInputの設定で調整を行う。

### 入力値の設定

使用したいAxis入力に対して設定を行う。
DeviceConfigurations>[任意コントローラ]>AxisProperties内のIndexは `showdebug  rawinput` で表示されるテキストのGenericUSBController_Axis1~Axis8に該当する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/2b89c30b-429d-0c06-0adc-58367aece85d.png)
・Enable：このキーを有効にするか
・Key：対応するキー
・Inverted：入力値を逆にするか
・GamePadStick：入力値を(-1.0)~(1.0)にマッピングするか
・Offset：入力値のオフセット

これらの設定を各イベントに対して行う。
GenericUSBController_Button1~20はEnableの設定しかないため、ほぼ触らなくていいはずだ。

設定を入れた上で入力の対応表を以下にまとめる。
| RawInputButton名 | ワイヤレスコントローラの入力 | LogicoolF310の入力 |
|:-:|:-:|:-:|
| Button1 | □ | X |
| Button2 | × | A |
| Button3 | ○ | B |
| Button4 | △ | Y |
| Button5 | L1 | LB |
| Button6 | R1 | RB |
| Button7 | L2 | LT |
| Button8 | R2 | RT |
| Button9 | SHARE | BACK |
| Button10 | OPTION | START |
| Button11 | LeftStick押し込み | LeftStick押し込み |
| Button12 | RightStick押し込み | RightStick押し込み |
| Button13 | PlayStationボタン | - |
| Button14 | TouchPad | - |
| Button15 | - | - |
| Button16 | - | - |
| Button17 | - | - |
| Button18 | - | - |
| Button19 | - | - |
| Button20 | - | - |

| RawInputAxis名 | ワイヤレスコントローラの入力 | LogicoolF310の入力 | 設定備考 |
|:-:|:-:|:-:|:--|
| Axis1 | RightStick <br>↑(1.0) ・(0.004) ↓(-1.0) | RightStick <br>↑(1.0) ・(0.004) ↓(-1.0) | Inverted=true<br>GamePadStick=true<br>Offset=0.0 |
| Axis2 | RightStick <br>→(1.0) ・(0.004) ←(-1.0) | RightStick <br>→(1.0) ・(0.004) ←(-1.0) | Inverted=false<br>GamePadStick=true<br>Offset=0.0 |
| Axis3 | LeftStick <br>↑(1.0) ・(0.004) ↓(-1.0) | LeftStick <br>↑(1.0) ・(0.004) ↓(-1.0) | Inverted=true<br>GamePadStick=true<br>Offset=0.0 |
| Axis4 | LeftStick <br>→(1.0) ・(0.004) ←(-1.0) | LeftStick <br>→(1.0) ・(0.004) ←(-1.0) | Inverted=false<br>GamePadStick=true<br>Offset=0.0 |
| Axis5 | 方向キー<br>※詳細は後述 | 方向キー<br>※詳細は後述 | Inverted=false<br>GamePadStick=false<br>Offset=0.0 |
| Axis6 | - | - | - |
| Axis7 | R2 | - | - |
| Axis8 | L2 | - | - |

表中の括弧内はその方向入力時のAxisValue、「・」はニュートラル状態を示す。
方向キーの入力はAxis5に割り振られているが、全方向が一つのイベントとして取れるためスティックの入力値と同じように扱うことはできない点に注意が必要である。

#### 方向キーの変換

方向キーの入力は一つのAxisとして取得が可能で、どの方向の入力なのかは計算で算出することができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/eb5d33cb-43f0-086d-bd6d-b113394f9259.png)

Axis5の`GamePadStick`をfalseにしているのであれば入力値は(0.0)~(1.0)になるため、方向キーの各方向（8方向分）に割り振ってあげればいい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/975a5fd6-f5f7-9164-de50-5aa30930bf58.png)
ただしコレには少し落とし穴がある。
基本的には機械の世界では0は入力なし、1は入力ありとして扱いたいものだ。
しかしこの方向キーは↑の入力が0.0としてとれてしまう。

そのため下記のようにAxisPropertiesのOffsetに1.0を設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/4f3cdcd4-64bc-1fad-d185-26af3bb71cbd.png)

こうすることで、入力がある場合を1.0~2.0へ収めることができ、0.0は入力なしとして扱うことができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/72240a6d-c7f9-5796-aefa-c11931903b8f.png)

### 入力イベントの登録

さて、ここでようやくRawInputで取れる入力キーをActionMappingに紐づける。
なんてことはない簡単な作業だ。
`GenericUSBController`というカテゴリ内にキーがまとまっているため、先程確認しておいたキー番号を適当に設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548800/cb3089e5-1c17-f27f-fac3-18d0abfda533.png)
既存のActionMappings/AxisMappingsに組み込んでしまえば、ここまでの処理でDirectInputのパッドでもXInput同様に動かすことが可能である。

# まとめ

RawInputを使うにあたって検索して出てきたページが「うまく動かないから使うのやーめた」となっており（しかも一つじゃない）少々不安ではあったがなんとか動かすところまでいけてよかった。

実は当初はPS5のコントローラも試してみたが、入力を取る時点でかなり面白おかしいことになってしまったため、当記事では省略している。
是非実際に試してみていただきたい。

# 参考
https://docs.unrealengine.com/4.27/ja/InteractiveExperiences/Input/RawInput/

https://katze.hatenablog.jp/entry/2017/01/18/155906

# 【おまけ】開発上の注意点
開発に当たってぶち当たって困ったことについて記載する。

### 入力が取れない
複数パッドの入力を試すため、パッドを代わる代わるPCに挿して試そうとしたところ、はじめに刺していたパッドの入力は取れるのに2つ目に挿したパッドの入力が正常に取れなかった。
→どうやらパッドの入力はエディタが立ち上がる際にささっていたものが正常に取れるようだった。
そのため上手く入力が取れなかったパッドもエディタ起動前に挿しておけば正常に取れることを確認した。

### logicoolF310の入力値がおかしい
logicoolのパッドしか挿していないのにデバイスが2個あって、どちらの入力値もおかしい状態になった。
→エディタが立ち上がった状態でパッドの設定をXInput⇔DirectInput切り替えしてしまったため、パッドは1つなのに入力デバイスが2つになったようだ。
そのため2つの入力値が合算された入力値が取得されていたらしい。
これはXInput⇔DirectInputが切り替えられるパッドでのみ起こる問題だと思われる。

### パッドはニュートラル状態なのにスティックの入力が入る
この問題は本当に多かった。
様々な要因が考えられるため、自分が引っかかったポイントをまとめておく。

・RawInput設定でAxis系の項に`GamePadStick`がfalseになっているものはないか？
→`showdebug rawinput`でみるとAxis系のニュートラルの入力が0.5になっているものがあったらそいつが怪しい。

・エディタ起動時にXInputしか刺さっていない
→なぜかRawInputがXInputを自分のイベント（GeneralUSB系）にアサインしてしまうようだ。やめてほしい。
この問題を解決するため、RawInputのソースコードを調査した。
RawInputの実際のイベント処理などはRawInputWindowsというクラスが行っているようだった。

どうもRawInputWindowsの処理を追っていくと、コンストラクタ内で`DeviceID=4`がDirectInput、`DeviceID=5`がXInputになっており、DirectInputのパッドを見つけられなかった場合、XInputをRawInputのイベントに登録する処理が走ってしまうため、XInputの入力が重複してしまうようだ。
```c++:RawInputWindows.cpp
FRawInputWindows::FRawInputWindows(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler)
	: IRawInput(InMessageHandler)
{
	DefaultDeviceHandle = INDEX_NONE;
	DLLPointers.InitFuncPointers();

	FWindowsApplication* WindowsApplication = (FWindowsApplication*)FSlateApplication::Get().GetPlatformApplication().Get();
	check(WindowsApplication);

	WindowsApplication->AddMessageHandler(*this);
	QueryConnectedDevices();

	// Register a default device if desired
	if (GetDefault<URawInputSettings>()->bRegisterDefaultDevice)
	{
		const uint32 Flags = 0;
		const int32 PageID = 0x01;
		int32 DeviceID = 0x04;
		DefaultDeviceHandle = RegisterInputDevice(RIM_TYPEHID, Flags, DeviceID, PageID);
        // ここから-----------------------
        // ↓↓DirectInputのパッドを見つけられなかった場合、刺さっているXInputのパッドを入力デバイスとして登録する処理をしている↓↓
		if (DefaultDeviceHandle == INDEX_NONE)
		{
			DeviceID = 0x05;
			DefaultDeviceHandle = RegisterInputDevice(RIM_TYPEHID, Flags, DeviceID, PageID);
		}
        // ここまで------------------------
	}

	AHUD::OnShowDebugInfo.AddRaw(this, &FRawInputWindows::ShowDebugInfo);
}	
```
安直にここの処理を消してみたが、そうしたら今度はDirectInputのパッド入力が取れなくなってしまったため、下記のようにソースに手を入れた。

```c++:RawInputWindows.cpp
void FRawInputWindows::SetupBindings(const int32 DeviceHandle, const bool bApplyDefaults)
{
	FRawWindowsDeviceEntry& DeviceEntry = RegisteredDeviceList[DeviceHandle];
	const URawInputSettings* RawInputSettings = GetDefault<URawInputSettings>();
	bool bDefaultsSetup = false;

	//@third party code BEGIN-----
	// 登録されているデバイスか.
	bool bRegisteredDevice = false;
	//@third party code END-------
	for (const FRawInputDeviceConfiguration& DeviceConfig : RawInputSettings->DeviceConfigurations)
	{
		const int32 VendorID = FCString::Strtoi(*DeviceConfig.VendorID, nullptr, 16);
		const int32 ProductID = FCString::Strtoi(*DeviceConfig.ProductID, nullptr, 16);

		// If VendorId or ProductId are 0, apply to everything
		if ((VendorID == 0 || VendorID == DeviceEntry.DeviceData.VendorID) && (ProductID == 0 || ProductID == DeviceEntry.DeviceData.ProductID))
		{
			//@third party code BEGIN-----
			bRegisteredDevice = true;
			//@third party code END-------
			const int32 NumButtons = FMath::Min(DeviceConfig.ButtonProperties.Num(), MAX_NUM_CONTROLLER_BUTTONS);
			DeviceEntry.ButtonData.SetNum(NumButtons);
			for (int32 Index = 0; Index < NumButtons; ++Index)
			{
				const FRawInputDeviceButtonProperties& ButtonProps = DeviceConfig.ButtonProperties[Index];
				DeviceEntry.ButtonData[Index].ButtonName = (ButtonProps.bEnabled ? ButtonProps.Key.GetFName() : NAME_None);
			}

			const int32 NumAnalogAxes = FMath::Min(DeviceConfig.AxisProperties.Num(), MAX_NUM_CONTROLLER_ANALOG);
			DeviceEntry.AnalogData.SetNum(NumAnalogAxes);
			for (int32 Index = 0; Index < NumAnalogAxes; ++Index)
			{
				const FRawInputDeviceAxisProperties& AxisProps = DeviceConfig.AxisProperties[Index];
				FAnalogData& AnalogData = DeviceEntry.AnalogData[Index];
				if (AxisProps.bEnabled)
				{
					AnalogData.KeyName = AxisProps.Key.GetFName();
					AnalogData.Offset = AxisProps.Offset;
					AnalogData.bInverted = AxisProps.bInverted;
					AnalogData.bGamepadStick = AxisProps.bGamepadStick;
				}
				else
				{
					AnalogData.KeyName = NAME_None;
				}
			}

			bDefaultsSetup = true;
			break;
		}
	}

	//@third party code BEGIN-----
	// Before..
	// if (!bDefaultsSetup && bApplyDefaults)
	// After..
	// デバイス登録されたものしかBindしない.
	if (!bDefaultsSetup && bApplyDefaults && bRegisteredDevice)
	//@third party code END-------
	{
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis1, 0);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis2, 1);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis3, 2);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis4, 3);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis5, 4);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis6, 5);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis7, 6);
		BindAnalogForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Axis8, 7);

		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button1, 0);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button2, 1);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button3, 2);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button4, 3);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button5, 4);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button6, 5);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button7, 6);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button8, 7);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button9, 8);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button10, 9);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button11, 10);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button12, 11);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button13, 12);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button14, 13);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button15, 14);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button16, 15);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button17, 16);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button18, 17);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button19, 18);
		BindButtonForDevice(DeviceHandle, FRawInputKeyNames::GenericUSBController_Button20, 19);
	}
}
```
やっていることはデバイスにGenericUSBControllerのイベントをバインドする条件に、RawInput設定に記載したデバイスかどうかのチェックを追加しただけである。
これによってXInputしか刺さっていない状態でも、おかしな動きをすることはなくなった。

-----------------------------------

# おまけのおまけ
RawInputの設定で`VendorID = ProductID = 0`にしておくとどのデバイスであろうとイベントをバインドしてくれるらしい。
XInputはここの処理までこない（と思う）ため、もしデバイスごとに設定を変えなくていいのであればRawInputのID設定は0にしておいていいかもしれない。