This document is about: QUANTUM 2
SWITCH TO

イベントとコールバック

イントロ

シミュレーション(Quantum)とビュー(Unity)を分けることで、ゲームステートとビジュアルの開発時にモジュール性を高めることができます。ただし、ビューは自身を更新するためにゲーム状態からの情報を必要とします。Quantumはそのために2つの方法を提供します。

  • ゲームステートのポーリング
  • イベント/コールバック

どちらも有効なアプローチではありますが、ユースケースは若干異なります。一般的に、UnityからのQuantum情報をポーリングすることは、継続的なビジュアルに適しています。一方、イベントは、ゲームシミュレーションがビューに反応をトリガーするような時間が重要な場合に使用されます。このドキュメントでは、フレームイベントコールバックに焦点を当てます。

フレームイベント

イベントは、シミュレーションからビューに情報を転送するためのファイア・アンド・フォゲットメカニズムです。ゲームステートの変更や更新に使ってはいけません(その用途にはSignalsを使用します)。イベントには予測やロールバックの際にイベントを管理するのに役立つ、理解すべきいくつかの重要な側面があります。

  • イベントはクライアント間で同期を取らず、各クライアント自身のシミュレーションによって発生します。
  • 同じFrameを複数回シミュレーションすることができるため(予測、ロールバック)、イベントが複数回トリガーされる可能性があります。重複するイベントを避けるために、Quantum はイベントデータ、イベント ID、tick に対してハッシュコード関数を使用して重複を識別します。詳しくは nothashed キーワードを参照してください。
  • 通常のイベント(非同期イベント)は、イベントが発生した予測フレームが検証されると、キャンセルされるかまたは確認されます。詳しくは Canceled And Confirmed Events を参照してください。
  • イベントは、すべてのFrameがシミュレートされた後、OnUpdateViewコールバックの直後にディスパッチされます。イベントは、起動された順番に呼び出されます。ただし、syncedされていないイベントは例外で、重複していると判断されるとスキップされます。このタイミングにより、対象となるEntityViewはすでに破壊されている可能性があります。

もっとも単純なEventとその使用は、以下のとおりです:

  • Quantum DSLを使用してEventを定義

    C#

    event MyEvent {
      int Foo;
    }
    
  • シミュレーションからEventをトリガー

    C#

    f.Events.MyEvent(2022);
    
  • そしてUnityでイベントを購読して消費し、イベント用のクラスを生成します。接頭辞はEventとなります。

    C#

    QuantumEvent.Subscribe(listener: this, handler: (EventMyEvent e) => Debug.Log($"MyEvent {e.Foo}"));
    

DSL構造

イベントとそのデータはqtnファイル内のQuantum DSLを使用して定義されます。プロジェクトをコンパイルし、シミュレーション内のFrame.EventsAPIによって、イベントとそのデータを使用できるようにします。

C#

event MyEvent {
  FPVector3 Position;
  FPVector3 Direction;
  FP Length
}

クラス継承は、ベースイベントのクラスとメンバーの共有を許可します。

C#

event MyBaseEvent {}
event SpecializedEventFoo : MyBaseEvent {}
event SpecializedEventBar : MyBaseEvent {}
syncedキーワードは継承できません。

抽象クラスを使用し、ベースイベントが直接トリガーされるのを防止します。

C#

abstract event MyBaseEvent {}
event MyConcreteEvent : MyBaseEvent {}

DSLが生成した構造をEvent内で再利用します。

C#

struct FooEventData {
  FP Bar;
  FP Par;
  FP Rap;
}

event FooEvent {
  FooEventData EventData;
}

キーワード

synced

ロールバックによる誤検出を避けるために、イベントにはsyncedキーワードを付けることができます。これは、Frameの入力がサーバーによって確認されたときにのみ、イベントが(Unityに)ディスパッチされることを保証します。

Syncedイベントは、シミュレーションで発行された時(予測されたFrameの間)からビューに表示されるまでの間に遅延を追加し、プレイヤーに通知するために使用することができます。

C#

synced event MyEvent {}
  • Syncedイベントは、偽陽性や偽陰性を作成しません。
    Syncedされないイベントは、Unity上で2回呼び出されることはありません。

nothashed

以前の予測されたフレームでビューによってすでに消費されたイベントが再びディスパッチされるのを防ぐために、ハッシュコードが各Eventインスタンスのために計算されます。Eventをディスパッチする前に、イベントが重複しているかどうかを確認するためにハッシュコードが使用されます。

これにより、以下のような状況が発生する可能性があります。最小のロールバックによって引き起こされた1つのイベントの位置変更が、2つの異なるイベントとして誤って解釈されます。

nothashedキーワードを使用すると、Eventデータの一部を無視して、Eventの一意性をテストするためにどのキー候補データを使用するかを管理できます。

C#

abstract event MyEvent {
  nothashed FPVector2 Position;
  Int32 Foo;
}

ローカル、リモート

イベントにplayer_refメンバーがある場合、特別なキーワードを使用することができます:remotelocalです。

このキーワードは、Unity でイベントがディスパッチされる前に、player_reflocalまたはremoteのプレイヤーに割り当てられているかどうかをチェックします。すべての条件が一致した場合、このクライアントでイベントがディスパッチされます。

C#

event LocalPlayerOnly {
  local player_ref player;
}

C#

event RemotePlayerOnly {
  remote player_ref player;
}

まとめると、シミュレーション自体はremotelocalという概念にとらわれません。キーワードは、特定のイベントが各クライアントのビューで発生した場合にのみ変化します。

イベントに複数のplayer_refパラメータがある場合、localremoteを組み合わせることができます。このイベントはLocalPlayerをコントロールするクライアントと、RemotePlayerが別のプレイヤーにアサインされたときのみ発生します。

C#

event MyEvent {
  local player_ref LocalPlayer;
  remote player_ref RemotePlayer;
  player_ref AnyPlayer;
}

クライアントが複数のプレイヤーを管理している場合(例:split-screen)、プレイヤーのplayer_refはすべてローカルとみなされます。

クライアント、サーバー

Quantum 2.1以降

これは、サーバーサイドシミュレーションをカスタムのQuantumプラグインで実行している場合にのみ該当します。

イベントは、clientserverキーワードを使用して、実行場所を指定することができます。デフォルトでは、すべてのイベントはクライアントとサーバーでディスパッチされます。

C#

server synced event MyServerEvent {}

C#

client event MyClientEvent {}

イベントの使用

イベントをトリガー

イベントのタイプとシグネチャはFrame.FrameEvents構造にコード生成され、Frame.Eventsからアクセスできます。

C#

public override void Update(Frame f) {
  f.Events.MyEvent(2022);
}

イベントデータの選択

理想的には、Eventデータは自己完結し、サブスクライバがビュー上で処理するために必要な情報を運ぶべきです。

シミュレーションでEventが発生したFrameは、Eventが実際にビュー上で呼び出されたときに、すでに利用できない可能性があります。つまり、Eventの処理に必要なFrameから取得する情報が失われる可能性があります。

EventのQCollectionQListは、実際にはFrameのヒープ上のメモリにPtrとして渡されるだけです。バッファがもう利用できないため、ポインタの解決が失敗する可能性があります。同じことが EntityRefs にも当てはまります。Eventがディスパッチされた時点で最新のFrameからComponent にアクセスする場合には、Eventが最初に呼び出された時点とデータが同一ではない可能性があります。

イベントデータをarrayListでリッチ化する方法は以下のとおりです:

  • コレクションデータのペイロードが既知で、かつ妥当な最大サイズである場合、fixed arrayを構造の中にラップしてEventに追加することができます。QCollectionsとは異なり、配列はデータをFrameヒープに保存せず、値自体に担わせます。

C#

struct FooEventData {
  array<FP>[4] ArrayOfValues;
}
event FooEvent {
  FooEventData EventData;
}
  • 現時点では、DSLでは通常のC#の List<T>型を含むEventを宣言することはできません。しかし、Eventは部分クラスを使って拡張することができます。詳しくはイベント実装の拡張 セクションを参照してください。

Unityでのイベントサブスクリプション

Quantumは、QuantumEventによるUnityでの柔軟なEventサブスクリプションAPIをサポートしています。

C#

QuantumEvent.Subscribe(listener: this, handler: (EventPlayerHit e) => Debug.Log($"Player hit in Frame {e.Tick}"));

上記の例では、リスナーは単純に現在のMonoBehaviourで、ハンドラーは無名関数です。別の方法として、デリゲート関数を渡すこともできます。

C#

QuantumEvent.Subscribe<EventPlayerHit>(listener: this, handler: OnEventPlayerHit);

private void OnEventPlayerHit(EventPlayerHit e){
  Debug.Log($"Player hit in Frame {e.Tick}");
}

QuantumEvent.Subscribeは複数の任意のQoL引数を提供し、様々な方法でサブスクリプションを適格化しています。

C#

// only invoked once, then removed
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, once: true);

// not invoked if the listener is not active
// and enabled (Behaviour.isActiveAndEnabled or GameObject.activeInHierarchy is checked)
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, onlyIfActiveAndEnabled: true);

// only called for runner with specified id
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runnerId: "SomeRunnerId");

// only called for a specific
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runner: runnerReference);

// custom filter, invoked only if player 4 is local
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, filter: (QuantumGame game) => game.PlayerIsLocal(4));

// only for replays
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay);

// not for replays (Quantum SDK v2.0)
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, excludeGameMode: DeterministicGameMode.Replay);

// for all types except replays (Quantum SDK 2.1+)
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay, exclude: true);
//=> The gameMode parameter accepts and array of DeterministicGameMode

イベントからサブスクライブを解除

UnityはMonoBehavioursのライフタイムを管理し、リスナーは自動的にクリーンアップされるため登録解除する必要はありません。

より強力な管理が必要な場合、サブスクライブ解除は手動で処理できます。

C#

var subscription = QuantumEvent.Subscribe();

// cancels this specific subscription
QuantumEvent.Unsubscribe(subscription);

// cancels all subscriptions for this listener
QuantumEvent.UnsubscribeListener(this);

// cancels all listeners to EventPlayerHit for this listener
QuantumEvent.UnsubscribeListener<EventPlayerHit>(this);

CSharpでのイベントサブスクリプション

EventがMonoBehaviourの外部でサブスクライブされた場合には、サブスクリプションを手動で処理する必要があります。

C#

var disposable = QuantumEvent.SubscribeManual((EventPlayerHit e) => {}); // subscribes to the event
// ...
disposable.Dispose(); // disposes the event subscription

キャンセルされ、確認されたイベント

syncedされていないイベントは、検証されたフレームがシミュレーションされるとキャンセルされるか確認されます。Quantum はそれらに対応するため、CallbackEventCanceledおよびCallbackEventConfirmedというコールバックを提供します。

C#

QuantumCallback.Subscribe(this, (Quantum.CallbackEventCanceled c) => Debug.Log($"Cancelled event {c.EventKey}"));
QuantumCallback.Subscribe(this, (Quantum.CallbackEventConfirmed c) => Debug.Log($"Confirmed event {c.EventKey}"));

EventインスタンスはEventKey構造によって識別されます。たとえば、このようにEventKeyを作成することで、以前受信したEventをディクショナリに追加できます。

C#

public void OnEvent(MyEvent e) {
  EventKey eventKey = (EventKey)e;
  // ...
}

イベント実装を拡張

EventはQListの使用をサポートしていますが、リストを解決する際、対応するFrameはもう利用できないかもしれません。追加のデータ型は、部分的なクラス宣言を使用して追加することができます。

C#

event ListEvent {
  Int32 Foo;
}

C#

public partial class EventListEvent {
  public List<Int32> ListOfFoo;
}

カスタマイズされたEventをFrame.Event APIで発生できるようにするには、FrameEvents構造を拡張します。

C#

 f.Events.ListEvent(f, 1, new List<FP>() {2, 3, 4});. 

C#

namespace Quantum {
  public partial class Frame {
    public partial struct FrameEvents {
      public EventListEvent ListEvent(Frame f, Int32 foo, List<Int32> listOfFoo) {
        var ev = f.Events.ListEvent(foo);
        ev.ListOfFoo = listOfFoo;
        return ev;
      }
    }
  }
}

コールバック

コールバックはQuantum Coreによって内部的にトリガーされる、特別な種類のイベントです。ユーザーが利用できるコールバックは以下です:

Callback Description
CallbackPollInput Is called when the simulation queries local input.
CallbackInputConfirmed Is called when local input was confirmed.
CallbackGameStarted Is called when the game has been started.
CallbackGameResynced Is called when the game has been re-synchronized from a snapshot.
CallbackGameDestroyed Is called when the game was destroyed.
CallbackUpdateView Is guaranteed to be called every rendered frame.
CallbackSimulateFinished Is called when frame simulation has completed.
CallbackEventCanceled Is called when an event raised in a predicted frame was cancelled in a verified frame due to a roll-back / missed prediction. Synchronized events are only raised on verified frames and thus will never be cancelled; this is useful to graciously discard non-synced events in the view.
CallbackEventConfirmed Is called when an event was confirmed by a verified frame.
CallbackChecksumError Is called on a checksum error.
CallbackChecksumErrorFrameDump Is called when due to a checksum error a frame is dumped.
CallbackChecksumComputed Is called when a checksum has been computed.

MonoBehaviour

コールバックは、前述のFrame Eventと同様にサブスクライブ/サブスクライブ解除されます。ただし、QuantumEventではなくQuantumCallbackでおこないます。

C#

var subscription = QuantumCallback.Subscribe(...);
QuantumCallback.Unsubscribe(subscription); // cancels this specific subscription
QuantumCallback.UnsubscribeListener(this); // cancels all subscriptions for this listener
QuantumCallback.UnsubscribeListener<CallbackPollInput>(this); // cancels all listeners to CallbackPollInput for this listener

Unityはオブジェクトのライフタイムを管理します。このため、Quantumはリスナーの生死を判定できます。「死んだ」リスナーは、LateUpdateや特定のイベントタイプのイベントを呼び出すたびに削除されます。

たとえば、PollInputをサブスクライブしプレイヤーの入力をセットアップするには、以下の手順が必要です:

C#

public class LocalInput : MonoBehaviour
{
    private void Start() {
        QuantumCallback.Subscribe<CallbackPollInput>(this, PollInput);
    }

    private void PollInput(CallbackPollInput pollInput) {
    Input i = new Input();
    // add input values
        pollInput.SetInput(i, DeterministicInputFlags.Repeatable);
    }
}

純粋なCSharp

コールバックがMonoBehaviourの外部でサブスクライブされた場合、サブスクリプションを手動で処理する必要があります。

C#

var disposable = QuantumCallback.SubscribeManual((CallbackPollInput pollInput) => {}); // subscribes to the callback
// ...
disposable.Dispose(); // disposes the callback subscription

エンティティをインスタンス化する順序

Frame.Create()を使用してエンティティを作成し、またFrameシミュレーションが完了している場合には、以下のコールバックが順番に実行されます:

  1. OnUpdateView、新たに作成されたエンティティのビューがインスタンス化されます。
  2. Monobehaviour.Awake
  3. Monobehaviour.OnEnabled
  4. EntityView.OnEntityInstantiated
  5. Frame.Eventsが呼び出されます。

EventおよびCallbackサブスクリプションは、Monobehaviour.OnEnabledまたはEntityView.OnEntityInstantiatedでおこなわれます。

  • MonoBehaviour.OnEnabled、ここでのコードでイベントにサブスクライブすることができます。ただし、EntityViewのEntityRefとAsset GUIDはまだ設定されていません。
  • EntityView.OnEntityInstantiatedは、EntityViewコンポーネントのUnityEvent部分です。これは、エディター内のメニューでサブスライブできます。OnEntityInstantiatedが呼び出されると、EntityRefとEntityViewのAsset GUIDは設定されることが保証されます。イベントサブスクリプションまたはカスタムロジックがこれらのパラメータのいずれかを必要とすると、この時点で実行される必要があります。
OnEntityInstantiated subscription menu in Editor
エディターに表示されたOnEntityInstantiatedサブスクリプションメニュー

イベントまたはコールバックからサブスクライブ解除するには、補足機能を使用します:

  • OnEnabledで作成されたすべてのサブスクリプションについて、OnDisabled でサブスクライブ解除します。
  • OnEntityInstantiatedで作成されたすべてのサブスクリプションについて、OnEntityDestroyedでサブスクライブ解除します。
Back to top