Input イベント
Input System はイベント駆動型です。すべての入力はイベントとして配信され、イベントを挿入することでカスタム入力を生成できます。システムを流れるイベントをリッスンしてソース入力をすべて確認することもできます。
ノート: イベントは Input System の高度な機能であり、大部分は内部で使用されます。イベントシステムの知識が主に役立つのは、カスタム Device をサポートする必要がある場合や、既存の Device の動作を変更する場合です。
Input イベントは低レベルのメカニズムです。通常、アプリケーションのための入力を受けとるだけで十分な場合は、イベントを扱う必要はありません。イベントはアンマネージメモリバッファに格納され、C# ヒープオブジェクトには変換されません。Input System にはラッパー API が用意されていますが、より複雑なイベント操作にはアンセーフコードが必要になります。
ルーティングメカニズムは実装されていません。イベントは、ランタイムによって Input System に直接配信された後、Input System によって Device の状態に直接取り込まれます。
Input イベントは InputEvent
構造体で表されます。各イベントには、以下の一連の共通プロパティがあります。
プロパティ | 説明 |
---|---|
type |
イベントのタイプを示す FourCC コード。 |
eventId |
イベントの一意の数値 ID。 |
time |
イベントが生成された時点のタイムスタンプ。Time.realtimeSinceStartup と同じタイムラインで表されます。 |
deviceId |
イベントのターゲットである Device の ID。 |
sizeInBytes |
イベントの合計サイズ (バイト数)。 |
Input Debugger を使用して、特定の入力デバイスに対して受け取ったイベントを確認することができます。
イベントのタイプ
State イベント
State (状態) イベントには、Device の入力状態が含まれています。Input System は、これらのイベントを使用して新しい入力を Device に送ります。
状態イベントには、以下の 2 つのタイプがあります。
StateEvent
('STAT'
)DeltaStateEvent
('DLTA'
)
StateEvent
には、Device の状態全体の完全なスナップショットが、その Device に固有の形式で含まれています。stateFormat
フィールドにより、イベント内のデータのタイプが識別されます。生のデータには、state
ポインターと stateSizeInBytes
を使用してアクセスすることができます。
DeltaStateEvent
は StateEvent
と似ていますが、Device の状態の部分的なスナップショットだけが含まれています。Input System では通常、大きいサイズの状態レコードを必要とする Device の場合に、これを送信します。これで、一部の Control にのみ状態の変更が発生した場合に更新する必要のあるメモリの量が削減されます。raw データには、deltaState
ポインターと deltaStateSizeInBytes
を使用してアクセスすることができます。このデータは、stateOffset
で定義されるオフセットに位置する Device の状態に適用されます。
Device イベント
Device イベントは、Device 全体に関連する変更を示します。これらのイベントを利用する必要がある場合は、通常、InputEvent
を手動で処理するよりも、より高レベルの InputSystem.onDeviceChange
イベントにサブスクライブする方が便利です。
Device イベントには、以下の 3 つのタイプがあります。
DeviceRemoveEvent
('DREM'
)DeviceConfigurationEvent
('DCFG'
)DeviceResetEvent
('DRST'
)
DeviceRemovedEvent
は、Device が削除されたか、その接続が切断されたことを示します。削除されたデバイスを照会するには、共通フィールドの deviceId
を使用できます。このイベントには追加データはありません。
DeviceConfigurationEvent
は、Device の設定が変更されたことを示します。この意味は Device ごとに異なります。例えば、キーボードで使用されるレイアウトが変更されたことを示す場合や、コンソールで、ゲームパッドの割り当て先のプレイヤー ID が変更されたことを示す場合があります。変更のあったデバイスを照会するには、共通フィールドの deviceId
フィールドを使用できます。このイベントには追加データはありません。
DeviceResetEvent
は、デバイスがリセットされることを示します。これにより、Device で InputSystem.ResetDevice
の呼び出しがトリガーされます。
Text イベント
これらのイベントは、テキスト入力を処理するために キーボード デバイスによって送信されます。これらのイベントの使用を考えている場合は、通常、InputEvent
を手動で処理するよりも、より高レベルの Keyboard クラスのコールバック にサブスクライブする方が便利です。
テキストイベントには、以下の 2 つのタイプがあります。
TextEvent
('TEXT'
)IMECompositionEvent
('IMES'
)
イベントの操作
イベントのリッスン
着信イベントの監視や処理を手動で行う場合は、InputSystem.onEvent
コールバックにサブスクライブします。
InputSystem.onEvent +=
(eventPtr, device) =>
{
Debug.Log($"Received event for {device}");
};
イベントを処理をより便利にする IObservable
インターフェースも提供されています。
//ゲームパッドの最初のボタン押下を待機します。
InputSystem.onEvent
.ForDevice<Gamepad>()
.Where(e => e.HasButtonPress())
.CallOnce(ctrl => Debug.Log($"Button {ctrl} pressed"));
イベントで、値が変更されたコントロールを列挙するには、InputControlExtensions.EnumerateChangedControls
を使用できます。
InputSystem.onEvent
.Call(eventPtr =>
{
foreach (var control in eventPtr.EnumerateChangedControls())
Debug.Log($"Control {control} changed value to {control.ReadValueFromEventAsObject(eventPtr)}");
};
これは、手動で InputDevice.allControls
を反復処理してイベントから各コントロールの値を読み出すよりもはるかに効率的です。
状態イベントの読み取り
状態イベントには、Device の未加工のメモリスナップショットが含まれます。このため、イベント内のデータを解釈するには、特定の Device の個々の状態が格納されている場所と形式を知っている必要があります。
状態イベントに格納されている状態にアクセスする最も簡単な方法は、その状態の対象である Device を利用することです。任意の Control に対して、それ自体の内部に格納されている状態からではなく、特定のイベントから値を読み取るようにリクエストすることができます。
例えば、以下のコードは、Gamepad
をターゲットとした状態イベントから Gamepad.leftStick
の値を読み取る方法を示しています。
InputSystem.onEvent +=
(eventPtr, device) =>
{
//状態イベント以外は無視します。
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
return;
var gamepad = device as Gamepad;
if (gamepad == null)
{
//ゲームパッドのイベントではないか、デバイス ID が有効ではなくなりました。
return;
}
var leftStickValue = gamepad.leftStick.ReadValueFromEvent(eventPtr);
};
イベントの作成
誰でも、任意の既存の Device に対して、新しい Input (入力) イベントを作成し、キューに入れることができます。Input イベントをキューに入れる操作はスレッドセーフです。つまり、イベントの生成はバックグラウンドスレッドでも実行できます。
ノート: Unity では、バックグラウンドスレッドから生じるイベントに割り当てられるメモリに制限があります。バックグラウンドスレッドで生成されたイベントが多すぎる場合、スレッドからキューへのイベントの追加は、メインスレッドでバックグラウンドのイベントキューがフラッシュされるまでブロックされます。
キューに追加されたイベントは、すぐに消費されるわけではありません。イベントの処理は次回の更新時に行われます (更新は、InputSettings.updateMode
に応じて、InputSystem.Update
を介して手動で、または Player Loop の一部として自動的にトリガーされます)。
状態イベントの送信
Device の状態を記述する "状態構造体" が用意されている Device の場合、入力をデバイスに送信するには、それらの構造体のインスタンスをキューに追加することが最も簡単な方法です。
//マウス。
InputSystem.QueueStateEvent(Mouse.current, new MouseState { position = new Vector2(123, 234) });
//キーボード。
InputSystem.QueueStateEvent(Keyboard.current, new KeyboardState(Key.LeftCtrl, Key.A));
Touchscreen
は少し特殊で、TouchState
形式の入力が想定されています。
//タッチを開始します。
InputSystem.QueueStateEvent(Touchscreen.current,
new TouchState { touchId = 1, phase = TouchPhase.Began, position = new Vector2(123, 234) });
//タッチを移動します。
InputSystem.QueueStateEvent(Touchscreen.current,
new TouchState { touchId = 1, phase = TouchPhase.Moved, position = new Vector2(234, 345) });
//タッチを終了します。
InputSystem.QueueStateEvent(Touchscreen.current,
new TouchState { touchId = 1, phase = TouchPhase.Ended, position = new Vector2(123, 234) });
重要: タッチ ID を 0 にすることはできません。有効なタッチのタッチ ID は、0 以外である必要があります。同時タッチには、それぞれに一意の ID が必要です。タッチの終了後は、その ID を再び使用できますが、これは推奨されていません。
特定の Device によって使用される状態の形式が正確にわからない場合、その Device に入力を送信する最も簡単な方法は、Device 自体から StateEvent
を作成することです。
//`StateEvent.From` は、特定のデバイスに十分な大きさの状態イベントを保持する
//一時バッファをアンマネージメモリに作成し、デバイスの現在の状態の
//メモリコピーを格納します。
InputEventPtr eventPtr;
using (StateEvent.From(myDevice, out eventPtr))
{
((AxisControl) myDevice["myControl"]).WriteValueIntoEvent(0.5f, eventPtr);
InputSystem.QueueEvent(eventPtr);
}
または、個々の Control にイベントを送信することもできます。
//ゲームパッドの leftStick を更新するイベントを送信します。
InputSystem.QueueDeltaStateEvent(Gamepad.current.leftStick,
new Vector2(0.123f, 0.234f);
差分を表す状態イベントは、Control がメモリ内でバイト単位で揃えられていて、サイズが 8 ビットの倍数である場合にのみ機能します。例えば、単一ビットとして格納されるボタン Control には、差分の状態イベントを送信できません。
イベントのキャプチャ
ノート: 任意のデバイスから入力をキャプチャして再生できる、
InputRecorder
という再利用可能な MonoBehaviour を含むサンプルプロジェクトを入手するには、Package Manager を開き、Input System Package を選択し、"Input Recorder" サンプルプロジェクトを選択してダウンロードします。
InputEventTrace
クラスを使用すると、Input イベントを記録して後で処理することができます。
var trace = new InputEventTrace(); //デバイス ID を指定して、特定のデバイスに対する
//イベントだけを追跡することもできます。
trace.Enable();
//... 処理を実行
var current = new InputEventPtr();
while (trace.GetNextEvent(ref current))
{
Debug.Log("Got some event:" + current);
}
//IEnumerable もサポートされます。
foreach (var eventPtr in trace)
Debug.Log("Got some event:" + eventPtr);
//トレースではアンマネージリソースが使用されます。必ず破棄してください。
trace.Dispose();
アンマネージ (C++) メモリヒープでメモリリークが発生するのを防ぐために、イベントトレースは使用後に破棄してください。
イベントトレースをファイルやストリームに書き出し、それを再びロードして、記録したストリームを再生することもできます。
//必要に応じてサイズが拡張されるようにトレースを設定します。
var trace = new InputEventTrace(growBuffer:true);
trace.Enable();
//... 入力をキャプチャ ...
//トレースをファイルに書き込みます。
trace.WriteTo("mytrace.inputtrace.");
//同じファイルからトレースをロードします。
var loadedTrace = InputEventTrace.LoadFrom("mytrace.inputtrace");
キャプチャしたトレースは、InputEventTrace
インスタンスから Replay
メソッドを使用して直接再生できます。
//Replay メソッドは、再生の設定と制御に使用できる
//ReplayController を返します。
var controller = trace.Replay();
//例えば、イベントをそのまま再生するのではなく、新しいデバイスを作成し、それらに
//イベントを送信するには、WithAllDevicesMappedToNewInstances を呼び出します。
controller.WithAllDevicessMappedToNewInstances();
//すべてのフレームを 1 つずつ再生します。
controller.PlayAllFramesOnyByOne();
//元のイベントのタイミングのシミュレーションが行われるようにイベントを再生します。
controller.PlayAllEventsAccordingToTimestamps();
イベントの処理
イベント は Unity ランタイムによってキューに収集されます。このキューは定期的にフラッシュされ、その中に入っているイベントが処理されます。イベントを手動でキューに追加するには、InputSystem.QueueEvent
を呼び出します。
入力が処理されるたびに Unity ランタイムによって暗黙で InputSystem.Update
が呼び出されます。
更新が発生する間隔は、"Update Mode" (更新モード) の設定によって決まります。デフォルトでは、各フレームで
通常、入力が処理されるときには、キューにある未処理の Input イベントが すべて 消費されます。ただし、これには 2 つの例外があります。
UpdateMode.ProcessEventsInFixedUpdate
の使用時には、対応する
もう 1 つの例外は、BeforeRender
の更新です。これは、固定更新または動的更新の後で、レンダリングの前に実行される更新で、VR ヘッドセットなど、最新の追跡データを必要とするデバイスを更新するためだけに使用されます。このような更新から他の入力が消費されることはありません。この更新は、該当するデバイスが実際に存在する場合にのみ有効になります。BeforeRender
の更新は、入力に関する限り、別のフレームとは見なされません。
ノート:
InputTestFixture
を使用するテストの内部と、システムを明示的に 手動更新モード に設定する場合以外では、手動でのInputSystem.Update
の呼び出しを行わないことを強く推奨します。
InputAction.WasPerformedThisFrame
や InputAction.WasPerformedThisFrame
などのメソッドは、前述の [InputSystem.Update
] の流れに基づいて暗黙で動作します。つまり、これらは、前回 発生した固定/動的/手動更新に従って状態を参照します。
現在/前回の更新タイプ と 更新回数 は、InputState
から照会できます。
イベントのマージ
Input System では、処理する必要のあるイベントの量を減らすために、イベントのマージが使用されます。 これにより、8000 Hz のマウスやタッチスクリーンなど、リフレッシュレートの高いデバイスを操作するときのパフォーマンスが大幅に向上します。
例えば、1 回の更新に 7 個のマウスイベントのストリームが含まれているとします。
マウス マウス マウス マウス マウス マウス マウス
イベント no1 イベント no2 イベント no3 イベント no4 イベント no5 イベント no6 イベント no7
時刻 1 時刻 2 時刻 3 時刻 4 時刻 5 時刻 6 時刻 7
Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28)
Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1)
BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1)
ワークロードを軽減するために、ボタンの状態変更を含んでいないイベントはスキップできます。
マウス マウス マウス
時刻 3 時刻 4 時刻 7
イベント no3 イベント no4 イベント no7
Pos(13,23) Pos(14,24) Pos(18,28)
Delta(3,3) Delta(1,1) Delta(4,4)
BtnLeft(0) BtnLeft(1) BtnLeft(1)
この場合、no1、no2、no3 を no3 に結合し、差分を累算します。 次の no4 は残します。これは、ボタンが押されていない状態から、押された状態への遷移を含んでいるためです。 このような遷移では、正確なタイムスタンプを保持することが重要です。 その後、no5、no6、no7 を、更新の最後のイベントである no7 に結合します。
現在、このアプローチは以下の Device で実装されています。
FastMouse
では、MouseState
のbuttons
またはclickCount
に違いがない限り、イベントが結合されます。Touchscreen
では、TouchState
のtouchId
、phaseId
、またはflags
に違いがない限り、イベントが結合されます。
以下のコードで、イベントのマージを無効にすることができます。
InputSystem.settings.disableRedundantEventsMerging = true;