This document is about: QUANTUM 2
SWITCH TO

Events & Callbacks

概述

模擬(Quantum)及檢視(Unity)之間的分割,在開發遊戲狀態及視覺物件時允許了大的模組化。然而,檢視要求從遊戲狀態而來的資訊,以更新自己。Quantum提供兩種方法:

  • 輪詢遊戲狀態
  • 事件/回調

雖然兩者都是有效的方法,他們的使用案例有些許的不同。一般而言,從Unity輪詢Quantum資訊,針對進行中的視覺物件而言較為合適,而事件則用於準時發生事件,其中遊戲模擬在檢視中觸發一個回應。這份文檔將專注於幀事件回調

幀事件

事件是一個自主導引的機制,以從模擬來轉移資訊到檢視。它們永遠不應該用於修正或更新遊戲狀態的一部分(對此需使用Signals)。事件有幾個重要方面需要理解,這有助於在預測及復原時管理他們。

  • 事件並不同步任何客戶端之間的事情,並且它們由各個客戶端自己的模擬所發出。
  • 因為相同的幀可被模擬超過一次(預測、復原),因此事件可以被觸發多次。為了避免不需要的重複的事件,Quantum在事件資料成員、事件識別碼及刷新上使用一個雜湊碼函式來識別重複事件。請參見nothashed關鍵字以取得更多資訊。
  • 常規、非synced的事件,一旦發出事件的被預測的幀得到驗證,事件將被取消或確認。請參見Canceled And Confirmed Events以取得更多資訊。
  • OnUpdateView回調之後,立即模擬所有幀之後,事件被分派。事件以被叫用的順序被調用,而非synced事件是例外,當其被識別為重複事件時,其可被略過。因為這個時機點,目標EntityView可能已經被消除了。

最簡單的事件及其使用,看起來如下:

  • 使用Quantum DSL來定義一個事件

    C#

    event MyEvent {
      int Foo;
    }
    
  • 從模擬觸發事件

    C#

    f.Events.MyEvent(2022);
    
  • 並且在Unity中訂閱及取用事件

    C#

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

DSL架構

事件及其資料是在一個qtn檔案中使用Quantum DSL被定義。編譯專案以讓他們在模擬中透過Frame.Events API成為可用。

C#

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

類別繼承允許共享基本事件類別及成員。

C#

event MyBaseEvent {}
event SpecializedEventFoo : MyBaseEvent {}
event SpecializedEventBar : MyBaseEvent {}
已同步的關鍵字不能被繼承。

使用抽象類別以防止基本事件被直接觸發。

C#

abstract event MyBaseEvent {}
event MyConcreteEvent : MyBaseEvent {}

在事件中重新使用由DSL生成的架構。

C#

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

event FooEvent {
  FooEventData EventData;
}

關鍵字

已同步的

為了避免復原引發的誤判為真的事件,他們可以以synced關鍵字來被標記。當針對幀的輸入已經被伺服器確認時,這將確保事件將只被分派(到Unity)。

Synced事件將在其在模擬中被發出的時間(在一個已預測的幀時),及其在檢視中的可用於通知玩家的表現形式圖之間,新增一個延遲。

C#

synced event MyEvent {}
  • Synced事件永不建立誤判為真或誤判為偽
  • synced事件在Unity上永不被調用兩次

非雜湊

為了避免在先前的已預測的幀中,已經被檢視所取用的事件被再次分派,因此針對各個事件執行個體計算出一個雜湊碼。在分派一個事件之前,雜湊碼用於檢查事件是否是一個重複。

這可導致下列情況:最小的由復原引發的一個事件的位置更改,被錯誤地解譯成兩個不同的事件。

nothashed關鍵字可用於控制哪個關鍵候選資料將被用於測試事件唯一性,其透過忽略一部分的事件資料來控制。

C#

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

本機、遠端

如果一個事件有一個player_ref成員,則特殊關鍵字為可用:remotelocal

當一個事件在一個客戶端上,在Unity中被分派之前,關鍵字將使得player_ref被檢查是否被分別地指派到一個localremote玩家。如果所有條件符合,事件將在這個客戶端上被分派。

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都將被當作是本機。

客戶端、伺服器

自從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);
}

選擇事件資料

理想上,事件資料應該是自封式,並且帶有訂閱者在檢視上處理它時將需要的所有資訊。

當事件被實際地在檢視上調用時,在模擬中,在事件被引發的幀可能不再可用。這意味著從幀所擷取的,處理事件所需要資訊可能遺失。

在一個事件上的一個QCollectionQList,實際上是只作為一個Ptr被傳送到在幀堆積上的記憶體。解析指標可能會失敗,因為緩衝區不再可用。對於EntityRefs也是如此,當在事件被分派時,從最新的幀存取元件時,資料可能與最初叫用事件時不同。

以一個array或一個List來強化事件資料的方式:

  • 如果集合資料的最大大小已知且合理,則可以將一個fixed array包裝在一個架構中,並且新增到事件。不同於QCollections,陣列並不在幀堆積上儲存資料,但是在自己的值上帶有它。

    C#

    struct FooEventData {
      array<FP>[4] ArrayOfValues;
    }
    event FooEvent {
      FooEventData EventData;
    }
    
  • 目前DSL並不允許以在事件之中的一個常規的C# List<T>類型來宣告一個事件。但是可以使用部分類別來擴展事件。請參見Extend Event Implementation章節以取得更多資訊。

在Unity中的事件訂閱

Quantum在Unity中透過QuantumEvent支援一個彈性的事件訂閱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中的事件訂閱

如果一個事件在MonoBehaviour之外被訂閱,訂閱需要被手動地處理。

C#

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

被取消及被確認的事件

當已驗證的幀已成為模擬時,非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.TestListEvent(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核心內部地觸發。讓使用者可用的回調是:

回調 說明
CallbackPollInput 當模擬查詢本機輸入時被調用。
CallbackInputConfirmed 當本機輸入被確認時被調用。
CallbackGameStarted 當遊戲已經被開始時被調用。
CallbackGameResynced 當遊戲已經從一個快照被重新同步時被調用。
CallbackGameDestroyed 當遊戲被消除時被調用。
CallbackUpdateView 被確保在每個轉譯幀上被調用。
CallbackSimulateFinished 當幀模擬已完成時被調用。
CallbackEventCanceled 當因為一個復原/已遺失預測,在一個已預測幀中被引發的一個事件在一個已驗證幀中被取消時被調用。已同步事件只在已驗證幀上被引發,並且因此永不被取消;這有助於在檢視中優雅地捨棄非同步事件。
CallbackEventConfirmed 當一個事件被一個已驗證幀確認時被調用。
CallbackChecksumError 在一個總和檢查碼錯誤上被調用。
CallbackChecksumErrorFrameDump 當因為一個總和檢查碼錯誤,一個幀被拋棄時被調用。
CallbackChecksumComputed 當一個總和檢查碼已經被計算時被調用。

單行為

以先前幀事件呈現的相同方式來訂閱回調,及從先前幀事件呈現的相同方式來取消訂閱,但是是透過QuantumCallback而非QuantumEvent

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()建立實體,並且幀模擬被完成時,下列回調將按照順序被執行:

  1. OnUpdateView,針對新建立的實體的檢視被具現化。
  2. Monobehaviour.Awake
  3. Monobehaviour.OnEnabled
  4. EntityView.OnEntityInstantiated
  5. Frame.Events被調用。

事件及回調訂閱可在Monobehaviour.OnEnabledEntityView.OnEntityInstantiated中被完成。

  • MonoBehaviour.OnEnabled,可以在這裡訂閱程式碼中的事件;然而,還不會設定EntityView的實體參照及資產GUID。
  • EntityView.OnEntityInstantiated是一個實體檢視元件的Unity事件部分。可透過編輯器內選單來訂閱它。當OnEntityInstantiated被調用,將確保實體檢視的實體參照及資產GUID被設定。如果事件訂閱或自訂邏輯需要這兩個參數的其中之一,這就是它應該被執行的地方。
onentityinstantiated subscription menu in editor
OnEntityInstantiated訂閱選單,如同在編輯器中所見。

為了從一個事件或回調取消訂閱,只需要使用互補函式:

  • 針對任何在OnEnabled中作成的訂閱,在OnDisabled中取消訂閱。
  • 針對任何在OnEntityInstantiated中作成的訂閱,在OnEntityDestroyed中取消訂閱。
Back to top