This document is about: QUANTUM 3
SWITCH TO

イベントとコールバック

はじめに

シミュレーション (Quantum) とビュー (Unity) の分割は、ゲームの状態とビジュアルの開発において高いモジュール性を提供します。しかし、ビューは自分自身を更新するためにゲームの状態から情報を必要とします。Quantum では、以下の2つの方法があります。

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

どちらも有効なアプローチですが、その使用ケースは若干異なります。一般的に、Unity から Quantum 情報をポーリングすることは継続的なビジュアルに対して好ましく、イベントはゲームシミュレーションがビューに反応を引き起こす際の一時的な発生に使用されます。このドキュメントでは フレームイベントコールバック に焦点を当てます。

フレームイベント

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

  • イベントはクライアント間で何も同期せず、各クライアントの独自のシミュレーションによって発火します。
  • 同じフレームは複数回シミュレーションを行うことができるため(予測、ロールバック)、イベントが何度もトリガーされる可能性があります。予期しない重複イベントを避けるために、Quantum はイベントデータメンバー、イベントID、およびティックに基づくハッシュコード関数を使用して重複を特定します。詳細については nothashed キーワードを参照してください。
  • 通常の非 synced イベントは、発火したフレームの予測が確認されるとキャンセルまたは確認されます。詳細については キャンセルおよび確認イベント を参照してください。
  • イベントはすべてのフレームがシミュレーションされた後、OnUpdateView コールバックの直後にディスパッチされます。イベントは、トリガーされた順序で呼び出されますが、重複していると識別された非 synced イベントはスキップされる可能性があります。このタイミングのため、対象となる QuantumEntityView はすでに破棄されている場合があります。

最も単純なイベントとその使用法は次のようになります。

  • Quantum DSL を使用してイベントを定義します

    C#

    event MyEvent {
      int Foo;
    }
    
  • シミュレーションからイベントをトリガーします

    C#

    f.Events.MyEvent(2023);
    
  • Unity でイベントを購読し、消費します。イベントのためのクラスを生成し、接頭辞 Event を付けます

    C#

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

DSL 構造

イベントとそのデータは、qtn-file 内の Quantum DSL を使用して定義されます。プロジェクトをコンパイルすると、これらはシミュレーション内の Frame.Events API を通じて利用可能になります。

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生成構造体を再利用します。

C#

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

event FooEvent {
  FooEventData EventData;
}

キーワード

synced

ロールバックによって引き起こされる誤検出イベントを回避するために、イベントに synced キーワードを付けることができます。これにより、フレームの入力がサーバーによって確認されるまで、イベントが Unity に送信されないことが保証されます。

Synced イベントは、シミュレーション内で発行された時点(予測フレーム中)と、そのイベントがビューに現れるまでに遅延が生じるため、プレイヤーに通知するのに利用できます。

C#

synced event MyEvent {}
  • Synced イベントは、誤検出(false positives)や誤否定(false negatives)を引き起こしません。
  • synced イベントは、Unity 上で二度呼び出されることはありません。

nothashed

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

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

nothashed キーワードは、イベントデータの一部を無視することで、イベントの一意性をテストするために使用されるキー候補データを制御することができます。

C#

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

local, remote

イベントに player_ref メンバーが含まれている場合は、特別なキーワード remotelocal が利用可能です。

イベントが Unity のクライアントに送信される前に、これらのキーワードは player_ref がそれぞれ local または 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;
}

クライアントが複数のプレイヤーを制御する場合(例:分割画面)、すべての player_ref はローカルとして扱われます。

client, server

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

イベントは client および server キーワードを使用して、どこで実行されるかのスコープを制限できます。デフォルトですべてのイベントはクライアントとサーバーの両方に送信されます。

C#

server synced event MyServerEvent {}

C#

client event MyClientEvent {}

イベントの使用

イベントのトリガー

イベントの種類とシグネチャは Frame.FrameEvents 構造体にコード生成され、Frame.Events を介してアクセス可能です。

C#

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

イベントデータの選択

理想的には、イベントデータは自己完結しており、サブスクライバーがビューで処理するために必要なすべての情報を持っているべきです。

イベントがシミュレーションで発生したフレームは、実際にビューでイベントが呼ばれたときにはもう存在しない可能性があります。つまり、イベントを処理するためにフレームから取得される情報が失われてしまう可能性があるのです。

イベント上の QCollection または QList は、実際にはフレームヒープのメモリへの Ptr のみが渡されます。このポインタの解決は、バッファがもはや利用できないために失敗する可能性があります。EntityRefs に関しても同様で、イベントが送信される時点で最新のフレームからコンポーネントにアクセスすると、データはもともとイベントが発生した際のものとは異なる可能性があります。

イベントデータを array または List で充実させる方法:

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

    C#

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

Unity におけるイベントの購読

Quantum は、QuantumEvent を介して Unity に柔軟なイベント購読 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#

// 一度だけ呼び出され、その後削除される
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, once: true);

// リスナーがアクティブではない場合や有効でない場合は呼び出されない
// (Behaviour.isActiveAndEnabled または GameObject.activeInHierarchy がチェックされる)
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, onlyIfActiveAndEnabled: true);

// 指定された ID を持つランナーに対してのみ呼ばれる
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runnerId: "SomeRunnerId");

// 特定のランナーに対してのみ呼ばれる
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runner: runnerReference);

// カスタムフィルタ、プレイヤー4がローカルである場合のみ呼び出される
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, filter: (QuantumGame game) => game.PlayerIsLocal(4));

// リプレイのためのみ
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay);

// リプレイを除くすべてのタイプのため
QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay, exclude: true);
// => gameMode パラメータは DeterministicGameMode の配列を受け入れます

イベントの購読解除

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

より厳密な制御が必要な場合には、手動で購読解除することも可能です。

C#

var subscription = QuantumEvent.Subscribe();

// この特定の購読をキャンセルする
QuantumEvent.Unsubscribe(subscription);

// このリスナーに対するすべての購読をキャンセルする
QuantumEvent.UnsubscribeListener(this);

// このリスナーに対する EventPlayerHit のすべてのリスナーをキャンセルする
QuantumEvent.UnsubscribeListener<EventPlayerHit>(this);

CSharp におけるイベントの購読

MonoBehaviour の外部でイベントに購読する場合は、手動で購読を管理する必要があります。

C#

var disposable = QuantumEvent.SubscribeManual((EventPlayerHit e) => {}); // イベントに購読する
// ...
disposable.Dispose(); // イベント購読を破棄する

キャンセルおよび確認イベント

synced イベントは、検証されたフレームがシミュレーションされると、キャンセルまたは確認されます。Quantum では、これに反応するためのコールバック CallbackEventCanceledCallbackEventConfirmed を提供しています。

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}"));

イベントインスタンスは EventKey 構造体によって識別されます。以前に受信したイベントは、たとえば EventKey を作成することで辞書に追加できます。

C#

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

イベント実装の拡張

イベントは QList を使用することをサポートしていますが、リストを解決するときに対応するフレームがもはや利用できない場合があります。部分クラス声明を使用することで、追加のデータ型を追加することができます。

C#

event ListEvent {
  Int32 Foo;
}

C#

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

カスタマイズされたイベントを 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 によって内部的にトリガーされる特別なタイプのイベントです。ユーザーに提供されるコールバックは次のとおりです:

コールバック 説明
CallbackPollInput シミュレーションがローカル入力をクエリする際に呼ばれます。
CallbackInputConfirmed ローカル入力が確認されたときに呼ばれます。
CallbackGameStarted ゲームが開始されたときに呼ばれます。
CallbackGameResynced スナップショットからゲームが再同期されたときに呼ばれます。
CallbackGameDestroyed ゲームが破棄されたときに呼ばれます。
CallbackUpdateView 毎フレーム描画時に呼ばれることが保証されています。
CallbackSimulateFinished フレームシミュレーションが完了したときに呼ばれます。
CallbackEventCanceled 予測フレームで発生したイベントがロールバックまたはミスした予測によって確認されたフレームでキャンセルされたときに呼ばれます。同期イベントは確認されたフレームでのみ発生するため、キャンセルされることはありません。これは、ビュー内で非同期イベントを優雅に破棄するのに役立ちます。
CallbackEventConfirmed イベントが確認されたときに呼ばれます。
CallbackChecksumError チェックサムエラーが発生したときに呼ばれます。
CallbackChecksumErrorFrameDump チェックサムエラーによりフレームがダンプされたときに呼ばれます。
CallbackChecksumComputed チェックサムが計算されたときに呼ばれます。
CallbackPluginDisconnect プラグインがエラーによりクライアントを切断したときに呼ばれます。理由パラメータにはエラーの説明(例:"Error #15: Snapshot request timed out")が設定されます。この後、クライアント状態は回復不可能であり、再接続およびシミュレーションの再起動が必要です。現在の QuantumRunner はすぐにシャットダウンされるべきです。

Unity 側のコールバック

SimulationConfig アセット内の Auto Load Scene From Map の値を調整することで、ゲームシーンが自動的にロードされるかどうかを決定でき、また、プレビューシーンのアンロードがゲームシーンのロードの前または後に行われるかどうかを決定できます。


シーンがロードおよびアンロードされる際に呼ばれる 4 つのコールバックがあります:CallbackUnitySceneLoadBegin, CallbackUnitySceneLoadDone, CallbackUnitySceneUnloadBegin, CallbackUnitySceneUnloadDone

MonoBehaviour

コールバックは、先に説明したフレームイベントと同様に QuantumCallback を介して購読および購読解除されます。

C#

var subscription = QuantumCallback.Subscribe(...);
QuantumCallback.Unsubscribe(subscription); // この特定の購読をキャンセルします
QuantumCallback.UnsubscribeListener(this); // このリスナーに対するすべての購読をキャンセルします
QuantumCallback.UnsubscribeListener<CallbackPollInput>(this); // このリスナーに対する CallbackPollInput のすべてのリスナーをキャンセルします

Unity はオブジェクトのライフタイムを管理します。したがって、Quantum はリスナーが生存しているかどうかを検出できます。「死んでいる」リスナーは、各 LateUpdate と特定のイベントタイプのイベント呼び出しごとに削除されます。

たとえば、PollInput メソッドに購読し、プレイヤー入力をセットアップするには、次の手順が必要です:

C#

public class LocalInput : MonoBehaviour {
  private DispatcherSubscription _pollInputDispatcher;
  private void OnEnable() {
    _pollInputDispatcher = QuantumCallback.Subscribe(this, (CallbackPollInput callback) => PollInput(callback));
  }

  public void PollInput(CallbackPollInput callback) {
    Quantum.Input i = new Quantum.Input();
    callback.SetInput(i, DeterministicInputFlags.Repeatable);
  }

  private void OnDisable() {
    QuantumCallback.Unsubscribe(_pollInputDispatcher);
  }
}

ピュア CSharp

MonoBehaviour の外部でコールバックに購読する場合は、手動で購読を管理する必要があります。

C#

var disposable = QuantumCallback.SubscribeManual((CallbackPollInput pollInput) => {}); // コールバックに購読する
// ...
disposable.Dispose(); // コールバック購読を破棄する

エンティティのインスタンス生成順序

Frame.Create() を使用してエンティティを作成し、フレームシミュレーションが完了したときに、次のコールバックが順番に実行されます:

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

イベントおよびコールバックの購読は、Monobehaviour.OnEnabled または QuantumEntityView.OnEntityInstantiated で行うことができます。

  • MonoBehaviour.OnEnabled では、ここでコード内でイベントに購読することが可能ですが、QuantumEntityView の EntityRef および Asset GUID はまだ設定されていません。
  • QuantumEntityView.OnEntityInstantiated は、QuantumEntityView コンポーネントのユニティイベントです。エディタメニューを介して購読できます。OnEntityInstantiated が呼び出されると、QuantumEntityView の EntityRef および Asset GUID が設定されていることが保証されます。イベントの購読やカスタムロジックでこれらのパラメータが必要な場合、ここで実行すべきです。
OnEntityInstantiated subscription menu in Editor
エディタ内の OnEntityInstantiated 購読メニュー。

イベントやコールバックから購読解除するには、単に対応する関数を使用します:

  • OnEnabled で行った購読は、OnDisabled でキャンセルします。
  • OnEntityInstantiated で行った購読は、OnEntityDestroyed でキャンセルします。
Back to top