BLOGブログ

2026.11.04UE5UE/ C++

[UE5]簡単な非同期処理を使って1フレームよりも細かい時間粒度で入力を判定する

こんにちは。エンジニアの松沢です。

先日、趣味でリズムゲームのシミュレーターを作ってみようと思い立ちました。

とりあえず入力とノーツのタイミングが合っているかを判定するシステムを作らなければいけません。

EnhancedInputを活用するのもいいのですが、今回はまだまだテスト段階です。EventGraphに入力のノードを置いてまずは入力された時間の表示を…

さて、ここでふと何か違和感を感じました。「これってちゃんと入力したタイミングを取れているの?」と不安がよぎったのです。

言うまでもなくUE5のメイン処理を司るのはゲームスレッドです。この処理もそこで行われます。

ゲームスレッドから次のゲームスレッドまでは60fpsで16.6…7msほどの時間があります。この間にされた入力というのは実際はどのタイミングで処理されるのでしょうか?

答えは次のゲームスレッドです。入力がなされたタイミングで入力情報は一時的に保存され、次のゲームスレッドが来たタイミングで処理されます。

試しに入力されたタイミングを出力する仕組みを複数作ってみましょう。

t.MaxFPSなどでFPSを落として同時押しすると、返ってくる値が全く同じ出力をする場合があることが確認できると思います。ゲームスレッドで処理を完結するために時間がフレーム単位で量子化されているためこういったことが起こります。

つまり、入力の判定は最大1フレーム分遅延する可能性があります。ほとんどのゲームにおいて1フレームという単位は充分に細かく、フレーム単位の粒度で判定するこのシステムは問題なく機能するでしょう。しかし、一部のゲームではちょっとだけ問題があったりします。

例えば最近のリズムゲームなどでは1ms単位などで判定を調整できるオプションが実装されていたりします。そんな中でそもそも最大1フレーム(16.6…7ms)入力の判定がズレる可能性がある、というのはちょっと許容しにくいのではないでしょうか。

入力がなされた本当に正確な時間--とまではいきませんが、せめてゲームスレッドで処理する際にもう少し細かい粒度で入力があった時間を検知できないものでしょうか。入力に合わせてその入力がなされた正確な時間が返ってくるような仕組みを作れば、処理そのものがゲームスレッドで行われていても判定の計算はできるはずです。そう、例えばこんな形です。

 

肝心の正確な時間情報はどこから手に入れられるでしょうか?ここではゲームとは別の非同期スレッドで高速なループを回し、その中でOS側から入力情報を入手することにします。

入力がいつされるかマシン側は分かりません。なので、入力を感知するには1つのスレッドを常に占有し続けるようなポーリング処理をする必要があります。Unreal Engineにはいくつか非同期のシステムがありますが、こういった役割にはFRunnableを活用するのが最適です。今回は1000Hzの頻度でループを回し続けます。概念図としては以下のようになります。

早速作ってみましょう。入力情報の取り出し口となるクラスはGameInstanceSubsystemを継承して作成します。

まずはC++のゲームスレッドとBP間の入力情報を受け取る仕組みを作ります。

これでこのようにバインドによってSubsystemからBPへ情報が渡せるようになりました。

ただし、このデリゲートが発火するのはゲームスレッドにおけるTick()のタイミングです。SubsystemはデフォルトではTick()を利用できないので、FTickableGameObjectを継承させます。

一旦、Tick()の中身は書かず、FRunnableを継承したポーリングワーカーを作成します。今回は以下のようなワーカーを作成しています。

見ての通り、コンストラクタで2つのTQueueのポインタを受け取り、ポーリングで検知した押し・離し入力とOSから入手したタイミング情報を合わせてTQueue内に格納します。1ms単位でループしているので、これで1000Hzのポーリングが可能です。

さて、Subsystem側に必要なプロパティを作成していきます。各プロパティは以下の通りです。

  • FInputPollingWorkerのポインタ
  • FInputPollingWorkerを起動させるためのFRunnableThreadのポインタ
  • 押し・離し入力を保存するための2つのEventQueue

今度はTick()内で行われる処理を書いていきましょう。ここで行うことは主に3つです。

  • TQueueに記録された情報を展開して、キーコードをUnrealのFKey情報に変換する
  • 同時に記録された時間からゲーム内時間を計算する
  • それらの情報をBroadcast()してBP側に伝える

最後にSubsystemのInitializeでワーカーの作成とFRunnableThreadの起動を行い、Deinitializeで安全に終了できるようにしたら作業完了です。

最終的にはこのようなコードとなりました。

[code]

では、検証してみましょう。

先ほどと同じようにt.MaxFPSでFPSを落とし、同時押し入力をしてみます。

[pic]

きちんと機能しているようですね。

今回ご紹介した方法はEventTickよりもはるかに高頻度でスレッドをループさせるものです。1000Hz程度のポーリング処理で問題になる場合は少ないですが、活用する際にはCPUに相応の負荷がかかるのでパフォーマンスにはご注意ください。

また、この方法はOSからキー入力を直接取る関係上、プラットフォーム依存の方法となっております。Windows以外の環境で動作させるにはまた別途実装が必要になりますのでそちらもご了承ください。

それでは良きUEライフを。