GameProgrammar's Night

ゲームプログラム系の覚え書き

UE4の入力取得の仕組みとプラグインの作り方

Unreal Engine 4 (UE4) 其の弐 Advent Calendar 2015」 3日目の記事になります。

qiita.com

UE4エディタ・BPなどが一切でません。C++のみのプログラマ向けの記事になります。

ここで示すソースコードは「4.10.0-release」タグのものになります。

UE4の入力取得

 確認しておきますと、ここでいう入力とは、ゲームパッド・キーボード・マウスといった入力デバイスのことを指しています。

 UE4でこの入力状態をチェックしイベントを発生させているのは2ヶ所あります。

 まず、ゲームパッド及び独自の入力デバイスの処理が以下の場所。

LaunchEngineLoop.cpp 2414~2415行目

2414: FSlateApplication& SlateApp = FSlateApplication::Get();
2415: SlateApp.PollGameDeviceState();

 2415行目のPollGameDeviceStateメソッドが、ゲームパッドと入力プラグインなどで指しこんだ独自入力デバイスのチェックを行いイベントを発生します。全ActorのTick前に一括して処理しています。
 FSlateApplicationクラスはプラットフォームを抽象化したクラスです。プラットフォームごとのApplicationクラスを内部でもっており、プラットフォームAPI処理を行っています。*1

 2つ目は、キーボードやマウスなどウインドウメッセージによる入力処理です。メッセージ系はチェックとイベント発生の2段階にわかれて処理されています。
 1段目、メッセージの確認が以下の場所。

LaunchEngineLoop.cpp 2391行目

2391: FPlatformMisc::PumpMessages(true);

 メッセージキューにたまっているメッセージを確認して、一端、メッセージバッファに情報を格納しています。
 その後、全ActorのTickが終わった後に、メッセージイベントを発生します。もちろん、すぐに処理すべきウインドウメッセージは、メッセージチェックの段階でも処理されていますが、入力系は遅延処理されます。
 その遅延処理しているのが、以下の場所。

LaunchEngineLoop.cpp 2460行目

2460: FSlateApplication::Get().Tick();

FSlateApplicationのTickメソッドの中で、ProcessDeferredEventsメソッドが呼ばれてメッセージによるイベントを発生させます。

 ざっくりですが、UE4の入力処理は上のようになっています。

 ちなみにLaunchEngineLoopは、いわゆるメインループの実装です。メインスレッドが何をやってるのかを見たい時は、LaunchEngineLoopをのぞくと良いと思います。

独自の入力デバイプラグインを作る

 以上を踏まえて、独自の入力デバイスを使うためのプラグインの作り方を紹介します。独自といってもオリジナルハードという意味ではなく、UE4が公式対応していない程度の意味です。Oculusタッチなどが該当します。*2
 プラグイン全般の作り方は公式ドキュメントなどを参考にしていただくとして、ここでは入力プラグインに特化した内容を紹介します。

 入力プラグインは、2つのクラスIInputDeviceModuleとIInputDeviceを実装して作ります。
 実装例としては、EpicWikiの「Custom Input Devices - Epic Wiki」や、Oculusタッチプラグインの実装がUE4のソースに含まれています。

IInputDeviceModuleクラス

 通常のプラグイン開発との一番の違いは、入力デバイプラグインを作る専用のモジュールインターフェイスが用意されていることです。IInputDeviceModuleクラスです。

 IInputDeviceModuleには、UE4に独自入力デバイスを扱うオブジェクトを渡すためのインターフェイスが追加されています。

TSharedPtr<class IInputDevice> CreateInputDevice(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler);

 PollGameDeviceStateメソッドがはじめて呼ばれる時に、IInputDeviceModuleのプラグインがあるかをチェックし、存在した場合にCreateInputDeviceメソッドを呼び、ポーリングするデバイスとしてリストに追加されます。
 以降、毎フレームポーリングされます。

CreateInputDeviceメソッド

 UE4起動時(エディタでもゲームでも)に1度だけ呼ばれます。
 IInputDeviceを継承した独自入力デバイスのスマートポインタを返してあげます。詳しくは後述しますが、引数のInMessageHandlerはキーイベントを発生させるのに必要ですので、忘れずに保存しておきます。

StartupModuleを実装する際の注意事項

 StartupModuleメソッドをオーバーライドした場合は、IInputDeviceModule::StartupModuleメソッドを忘れずに呼ぶようにしてください。その中でUE4に入力デバイスモジュールとして登録を行っています。忘れると独自入力デバイスが動作しません。

IInputDeviceクラス

 実処理を行うクラスは、IInputDeviceクラスを継承します。
 PollGameDeviceメソッドの中で以下のようにメソッドが、毎フレーム呼ばれます。

WindowsApplicaion.cpp 1788~1793行目

1788: // Poll externally-implemented devices
1789: for( auto DeviceIt = ExternalInputDevices.CreateIterator(); DeviceIt; ++DeviceIt )
1790: {
1791:   (*DeviceIt)->Tick( TimeDelta );
1792:   (*DeviceIt)->SendControllerEvents();
1793: } 

 SendControllerEventsの中で入力チェックとイベント発生を行います。

インターフェイスメソッド

 IInpuDeviceメソッドが要求するインターフェイスは次の通り。引数は省略します。

メソッド 実装する内容
SendControllerEvents 入力状態をチェックし、イベントを発生させる
SetMessageHandler SendControllerEvents実行時にイベントを発生する対象のメッセージハンドラを設定する
SetChannelValue(s) フォースフィードバックを反映する。普通のゲームパッドだと振動とか
Tick バイスの有効・無効をチェックする。またDeletaTimeが渡されるので必要があれば保存
Exec UE4のコンソールコマンドの実行。どういう情報が渡ってくるかは詳しく調査してませんが、独自のコンソールコマンドを実装できるようです

 SendControllerEventsとSetMessageHandlerは実装必須。他は必要に応じて実装すれば良いと思います。

初期化

 UE4側からIInputDeviceの初期化メソッドを呼ぶような仕組みは用意されていませんので、CreateInputDeviceメソッドの実装にIInputDevice初期化を実装します。

 FCustomInputDeviceを生成するとして

IInputDeviceModule::CreateInputDeviceメソッド実装例

TSharedPtr<class IInputDevice> CreateInputDevice(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler)
{
  auto Device = MakeSharable<FCustomInputDevice>(new FCustomInputDevice());
  if(Device->Init(InMessageHandler))
    return Device;
  else
    return nullptr;
}

 EpicWikiの例だとInputDeviceのコンストラクタで初期化コードを書いていますが、エラー処理ができないのでおすすめしません。

 IInputDeviceのオブジェクトは1つしか生成されませんので、複数のデバイスを扱う時は、IInputDeviceの中で複数扱えるように実装しておきます。

 細かい話ですが、CreateInpputDeviceに渡されるメッセージハンドラはTSharedRefクラスとなっています。TSharedRefの引数だからといって、TSharedRefで保存しようと思うとコンパイルエラーとなります。
 TSharedRefクラスは名前の通りTSharedPtrの参照版のようなクラスでして、参照としての性質を保持しています。参照は代入ができませんので、代わりにTSharedPtrで受けて保存します。

 ここまでは、独自入力プラグイン実装のフレームワークの話でした。
 次からは、UE4に独自のキー情報を追加する方法とキーイベントを発生させる方法を紹介します。

独自のキー情報を追加する

 キー情報は、FKeyクラスのオブジェクトをグローバルに作り、EKeysというキー情報をとりまとめてるクラスに登録します。

FKeyの宣言と実装

 UE4が用意しているFKeyの実装見ると、structのstatic constメンバ変数として作るみたいなので、それに習います。

CustomInputState.h

struct ECustomInputKeys
{
  static const FKey CustomAxis;
  static const FKey CustomKey1;
  static const FKey CustomKey2;
};

CustomInputState.cpp

const FKey ECustomInputKeys::CustomAxis("CustomAxis");
const FKey ECustomInputKeys::CustomKey1("CustomKey1");
const FKey ECustomInputKeys::CustomKey2("CustomKey2");

 FKeyのコンストラクタにはKeyの文字列表現を指定します。FNameとして保存され、C++上で特定のFKeyを示すのに使用します。

FKeyの登録

 独自のFKeyをEKeysに登録します。登録するタイミングは、StartupModuleが良いかと思います。

#define LOCTEXT_NAMESPACE "CustomInputDevicePlugin"

void FCustomInputDevicePlugin::StartupModule()
{
  IInputDeviceModule::StartupModule(); // 親のStartupを忘れずに呼ぶ

  EKeys::AddKey(ECutomInputKeys::CustomeAxis, LOCTEXT("CustomeAxis","Custome Axis"),  FKeyDetails::GamepadKey|FKeyDetials::FloatAxis);  
  EKeys::AddKey(ECutomInputKeys::CustomeKey1, LOCTEXT("CustomeKey1","Custome Key 1"), FKeyDetails::GamepadKey);
  EKeys::AddKey(ECutomInputKeys::CustomeKey2, LOCTEXT("CustomeKey2","Custome Key 2"), FKeyDetails::GamepadKey);
}

 EKeys::AddKey関数を使って登録します。引数は順番に、登録するFKey,FKeyのFNameとエディタ上での表示名,Keyの属性、です。
 LOCTEXTマクロの仕様はちゃんと調べてないので、よくわかってませんがたぶんローカライズテキスト対応なんだと思います。
 Keyの属性は、ゲームパッド・キーボード・マウス、ボタンなのか軸なのか、そういう指定をします。属性に合わせて、エディタ上でのカテゴリ分類やイベントの引数が変化します。|で複数指定することができます。

 キーの登録が成功していれば、ActionMappingのドロップダウンリストや、BPグラフ上で入力イベントを置くことができるようになります。

キーイベントの発生

 キーイベントは、CreateInputDevice時などに受け取るメッセージハンドラにイベントを発生させるメソッドがありますので、それを呼ぶだけです。イベントメソッドは、すべてOnではじまる名前になっています。
 ゲームパッド系のイベントの例を挙げます。

// パッド軸
MessageHandler->OnControllerAnalog(ECustomeKeys::CustomAxis.GetName(), 0, AxisValue);

// パッドボタン
MessageHandler->OnControllerButtonPressed(ECustomeKeys::CustomKey1.GetName(), 0, false);
MessageHandler->OnControllerButtonReleased(ECustomeKeys::CustomKey2.GetName(), 0, false);

 ゲームパッドのイベントメソッドの第一引数は対応するFKeyのFName、第二引数はPlayerIndexを渡します。第三引数はパッドは軸の値(-1.0~1.0)、ボタンはキーリピートによるイベントかどうかのbool値を渡します。
 また、軸は毎フレームイベントメソッドを呼び、ボタンは状態に変化(Pressed/Released)があった時だけ呼びます。

 C++からは自由にキーイベントを送ることができますので、独自のFKeyだけなく、UE4が実装済みのFKeyを使うことで、UE4のキーイベントとしてイベントを発生させることも可能です。その場合は、両方のキーでイベントを発生させると良いと思います。

MessageHandler->OnControllerButtonPressed(ECustomeKeys::CustomKey1.GetName(), 0, false);
// UE4が定義済みのゲームパッドボタンイベントとしても発生する
MessageHandler->OnControllerButtonPressed(FGamepadKeyNames::FaceButtonBottom, 0, false);

プラグイン専用のActor/Componentを作ったり、BPに操作関数などを公開する

 プラグインソースコード追加は、UE4エディタのソース追加のウィザードが使えませんので、直接ファイルを作ります。
 その場合でも、".generated.h"をincludeしておいて、UObjectやAActorを継承し、UCLASSなどのマクロを書いておけば、ビルドツールが自動で公開用のソースとして認識してくれますので、いつも通り書けば大丈夫です。

エディタ上でスタンドローン実行した時の対応

 プラグインのロードと実行のタイミングは、基本的にはupluginファイルに書いたLoadPhaseに従いますが、エディタ上でスタンドアローン実行した時(以下EdSA実行)はズレることがあります。
 EdSA実行の時は、現在編集中のLevelロードと実行が先に行われてから、プラグインのロードと実行が行われるよう(LoadPhaseにPreDefaultを指定していても)です。そのため、LevelのBeginPlayで初期化済みだと思ってプラグインを使うと上手く動作せず対処をちゃんとしていないとクラッシュします。
 プラグイン側で、初期化チェックなどエラーチェックをちゃんと行いクラッシュしないように実装しておく、Actor側でも正常値チェックをして、対処する必要があります。

DirectInputPadPlugin

 以上のことを全部踏まえて実装したのが、DirectInputPadPluginです。実装例としてもご覧ください。
github.com

おまけ

ウインドウハンドルの取得

 Windowsプラグインを作ると、何かとメインウインドウのウインドウハンドルが必要になります。取得方法は以下のようになります。

// MainWindowのウインドウハンドルを取得する
HWND GetMainWindowHandler()
{
  TSharedPtr<SWindow> MainWindow;
#if WITH_EDITOR
  if(GIsEditor)
  {
    auto& MainFrameModule = IMainFrameModule::Get();
    MainWindow = MainFrameModule.GetParentWindow();
  }
  else
#endif
  {
    MainWindow = GEngine->GameViewport->GetWindow();
  }
  if(MainWindow.IsValid() && MainWindow->GetNativeWindow().IsValid())
  {
    return static_cast<HWND>(MainWindow->GetNativeWindow()->GetOSWindowHandle());
  }
  return NULL;
}

 エディタ起動・ゲーム起動両対応してます。

依存モジュール

モジュール名 内容
Core ないと始まらない。おまじないレベル
CoreUObject リフレクションシステム。BPなどにクラスや関数を公開する時に使う
Engine GEngineが宣言されてる。ウインドウハンドル取得に使う
InputCore UE4が使うFKeyの定義
InputDevice IInputDeviceの宣言
Slate/SlateCore イベントを発生させるのに使う
MainFrame エディタ起動時のメインウインドウハンドル取得に使う

 MainFrameは、エディタ起動時にのみ依存モジュールとして追加するようにします。

if(UEBuildConfiguration.bBuildEditor)
{
  PrivateDependencyModuleNames.AddRange(new string[]{ "MainFrame", });
}

 こうしないと、パッケージ化でリンクエラーとなり、パッケージ化ができません。

明日のAdventCalendar

明日は、UE4の中の人シモダジュンヤさんのラーニングサンプルの美味しいつまみ方です!

*1:プラットフォームごとのApplictionクラスの名前は、F[プラットフォーム名]Appicationという名前になっています。Windowsだと、FWindowsApplicationクラスになります。

*2:まぁ、公式でプラグインが開発中なので公式対応と言っても良い気もしなくもないですが