This document is about: QUANTUM 2
SWITCH TO

이벤트 & 콜백

소개

시뮬레이션(Quantum)과 뷰(유니티) 사이의 분할은 게임 상태와 비주얼의 개발 동안 큰 모듈화를 가능하게 합니다. 그러나 뷰는 자체적으로 업데이트하기 위해 게임 상태의 정보가 필요합니다. Quantum은 다음과 같은 두 가지 방법을 제공합니다.

  • 게임 상태 폴링, 그리고
  • 이벤트/콜백

두 방법 모두 유효한 접근 방식이지만, 사용 사례는 약간 다릅니다. 일반적으로, 유니티의 Quantum 정보를 폴링 하는 것이 진행 중인 시각에 선호되는 반면, 이벤트는 게임 시뮬레이션이 뷰에서 반응을 유발하는 정확한 상황에 사용됩니다. 이 문서에서는 프레임 이벤트 & 콜백에 초점을 맞출 것입니다.

프레임 이벤트

이벤트는 시뮬레이션에서 뷰로 정보를 전송하는 발생 후 알아서 해주는 메커니즘입니다. 게임 상태의 일부를 수정하거나 업데이트하는 데 사용해서는 안 됩니다(Signals는 여기에 사용됩니다). 이벤트에는 예측 및 롤백 중에 이벤트를 관리하는 데 도움이 되는 몇 가지 중요한 측면이 있습니다.

  • 이벤트는 클라이언트 간에 동기화되지 않으며 각 클라이언트의 시뮬레이션에 의해 실행됩니다.
  • 동일한 프레임을 두 번 이상(예측, 롤백) 시뮬레이션할 수 있으므로 이벤트를 여러 번 트리거할 수 있습니다. 원하지 않는 중복 이벤트를 방지하기 위해 Quantum은 이벤트 데이터 멤버, 이벤트 ID 및 틱 표시를 통해 해시 코드 함수를 사용하여 중복을 식별합니다. 자세한 내용은 nothashed 키워드를 참조하십시오.
  • 정기적이고 동기화 되지않은 이벤트는 이벤트가 발생한 예측 프레임이 확인되면 취소되거나 확인됩니다. 자세한 내용은 취소 및 확인된 이벤트를 참조하십시오.
  • OnUpdateView 콜백 직후 모든 프레임이 시뮬레이션된 후 이벤트가 전송됩니다. 이벤트는 호출된 것과 같은 순서로 호출되지만 중복된 것으로 식별될 때 건너뛸 수 있는 동기화 되지 않은 이벤트는 예외입니다. 이 타이밍으로 인해 대상이 되는 EntityView가 이미 파괴되었을 수 있습니다.

가장 단순한 이벤트 및 이벤트의 용도는 다음과 같습니다:

  • Quantum DSL을 사용하여 이벤트를 정의합니다.

    C#

    event MyEvent {
      int Foo;
    }
    
  • 시뮬레이션에서 이벤트를 트리거합니다.

    C#

    f.Events.MyEvent(2022);
    
  • 그리고 이벤트에 대한 클래스를 Event라는 접두사로 생성하는 이벤트를 유니티에서 구독 및 사용합니다

    C#

    QuantumEvent.Subscribe(listener: this, handler: (EventMyEvent 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 {}
synced 키워드는 상속될 수 없습니다.

추상 클래스를 사용하여 기본 이벤트가 직접 트리거되지 않도록 합니다.

C#

abstract event MyBaseEvent {}
event MyConcreteEvent : MyBaseEvent {}

이벤트 내부에서 DSL 생성 구조를 재사용합니다.

C#

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

event FooEvent {
  FooEventData EventData;
}

키워드

synced

롤백으로 인한 잘못된 긍정 이벤트를 방지하기 위해 synced 키워드로 표시할 수 있습니다. 이렇게 하면 프레임에 대한 입력이 서버에 의해 확인된 경우에만 이벤트가 유니티로 발송됩니다.

synced 이벤트는 시뮬레이션에서 실행되는 시간(예측된 프레임 동안)과 플레이어에게 정보를 제공하는 데 사용할 수 있는 뷰에서 나타나는 시간 사이의 지연을 추가합니다.

C#

synced event MyEvent {}
  • Synced 이벤트는 잘못된 긍정 또는 잘못된 부정을 생성하지 않습니다.
  • Non-synced 이벤트는 유니티에서 2번 호출되지 않습니다.

nothashed

이전에 예측된 프레임의 뷰에서 이미 사용된 이벤트가 다시 디스패치되는 것을 방지하기 위해 각 이벤트 인스턴스에 대해 해시 코드가 계산됩니다. 이벤트를 발송하기 전에 해시 코드를 사용하여 이벤트가 중복되는지 확인합니다.

이로 인해 다음과 같은 상황이 발생할 수 있습니다: 하나 이벤트의 롤백으로 인한 최소 위치 변경은 2개의 다른 이벤트로 잘못 해석됩니다.

nothashed 키워드는 이벤트 데이터의 일부를 무시하여 이벤트 고유성을 테스트하는 데 사용되는 키 후보 데이터를 제어하는 데 사용할 수 있습니다.

C#

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

로컬, 리모트

이벤트에 player_ref 멤버 특수 키워드가 있는 경우 remotelocal을 사용할 수 있습니다

클라이언트의 유니티에서 이벤트가 디스패치되기 전에 키워드를 지정하면 각각 remote 또는 local 플레이어에 할당된 경우 player_ref가 선택됩니다. 모든 조건이 일치하면 이 클라이언트에서 이벤트가 발송됩니다.

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.Events로 접근할 수 있는 Frame.FrameEvents 구조체로 생성됩니다.

C#

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

이벤트 데이터 선택

이상적으로는 이벤트 데이터가 자체적으로 포함되어야 하며 구독자가 이를 처리하는 데 필요한 모든 정보를 뷰에 전달해야 합니다.

이벤트가 실제로 뷰에서 호출될 때 시뮬레이션에서 이벤트가 발생한 프레임을 더 이상 사용할 수 없습니다. 이벤트를 처리하는 데 필요한 프레임에서 검색할 정보가 손실될 수 있음을 의미합니다.

이벤트의 QCollection 또는 QList는 실제로는 프레임 힙의 메모리에 Ptr로만 전달됩니다. 버퍼를 더 이상 사용할 수 없기 때문에 포인터를 확인하지 못할 수 있습니다. EntityRefs도 마찬가지일 수 있습니다. 이벤트가 발송될 때 가장 최신 프레임에서 구성 요소에 접근할 때 데이터가 이벤트가 처음 호출되었을 때와 같지 않을 수 있습니다.

array 또는 List로 이벤트 데이터를 풍부하게 하는 방법:

  • 수집 데이터 페이로드가 알려져 있고 합리적인 최대 크기인 경우 고정 배열을 구조체 내부에 감싸서 이벤트에 추가할 수 있습니다. QCollections와는 달리 배열은 데이터를 프레임 힙에 저장하지 않고 값 자체에 저장합니다.

    C#

    struct FooEventData {
      array<FP>[4] ArrayOfValues;
    }
    event FooEvent {
      FooEventData EventData;
    }
    
  • DSL은 현재 일반 C# List<T> 타입으로 이벤트를 선언할 수 없습니다. 그러나 partial 클래스를 사용하여 이벤트를 확장할 수 있습니다. 자세한 내용은 이벤트 구현 확장 섹션을 참조하십시오.

유니티에서 이벤트 구독

Quantum은 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

이벤트 구독 해제

유니티는 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

취소 및 확인된 이벤트

바-동기화되된 이벤트들은 확인된 프레임이 시뮬레이션되면 취소되거나 확인됩니다. 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}"));

이벤트 인스턴스는 "EventKey 구조체로 식별됩니다. 이전에 받은 Event는 예를 들어 EventKey를 다음과 같이 만들어 딕셔너리에 추가할 수 있습니다.

C#

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

확장 이벤트 구현

이벤트는 QList 사용을 지원하지만. 목록을 확인할 때 해당 프레임을 더 이상 사용할 수 없을 수 있습니다. partial 클래스 선언을 사용하여 추가 데이터 형식을 추가할 수 있습니다.

C#

event ListEvent {
  Int32 Foo;
}

C#

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

FrameEvents 구조체 확작으로 Frame.Event API를 통해 사용자 정의 이벤트를 발생시킬 수 있습니다.

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 Core에 의해 내부적으로 트리거되는 특수한 유형의 이벤트입니다. 사용자가 사용할 수 있는 항목은 다음과 같습니다.

Callback 설명
CallbackPollInput 시뮬레이션이 로컬 입력을 쿼리하면 호출됩니다.
CallbackInputConfirmed 시뮬레이션이 로컬 입력이 검증되면 호출됩니다.
CallbackGameStarted 게임이 시작되면 호출됩니다.
CallbackGameResynced 게임이 스냅샷으로 부터 재동기화되었을 때 호출됩니다
CallbackGameDestroyed 게임이 없어져쓸 때 호출됩니다.
CallbackUpdateView 모든 렌더링된 프레임마다 호출되는 것이 보장됩니다.
CallbackSimulateFinished 프레임 시뮬레이션이 완료되었을 때 호출됩니다.
CallbackEventCanceled 예측 프레임에서 발생한 이벤트가 롤백/예측 누락으로 인해 확인된 프레임에서 취소되면 호출됩니다. 동기화된 이벤트는 확인된 프레임에서만 발생하므로 취소되지 않습니다. 이 기능은 뷰에서 동기화되지 않은 이벤트를 삭제하는데 유용합니다.
CallbackEventConfirmed 확인된 프레임에 의해 이벤트가 확인되면 호출됩니다.
CallbackChecksumError 체크섬 오류일때 호출됩니다
CallbackChecksumErrorFrameDump 체크섬 오류로 인해 프레임이 덤프되면 호출됩니다.
CallbackChecksumComputed 체크섬이 계산되면 호출됩니다.
CallbackPluginDisconnect 플러그인이 오류로 인해 클라이언트의 연결을 끊었을 때 호출됩니다. reason 파라미터가 오류 설명(예: "Error #15: Snapshot request timed out")으로 채워집니다. 그 후에는 클라이언트 상태를 복구할 수 없으므로 시뮬레이션을 다시 연결하고 다시 시작해야 합니다. 현재 QuantumRunner를 즉시 종료해야 합니다.

유니티측 콜백

SimulationConfig 에셋의 Auto Load Scene From Map 값을 조정하여 게임 씬이 자동으로 로드되는지 여부를 판단할 수 있으며, 미리 보기 장면 언로드가 게임 씬 로드 전후에 수행되는지 여부도 판단할 수 있습니다.

씬을 로드 및 언로드할 때 호출되는 네 가지 콜백이 있습니다:: CallbackUnitySceneLoadBegin, CallbackUnitySceneLoadDone, CallbackUnitySceneUnloadBegin, CallbackUnitySceneUnloadDone.

MonoBehaviour

콜백은 이전에 제시된 프레임 이벤트와 동일한 방식으로 구독하고 구독을 취소합니다. 단, 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

유니티는 객체의 수명을 관리합니다. 따라서 Quantum은 리스너의의 생사를 감지할 수 있습니다. "불량" 리스너는 각 LateUpdate 및 특정 이벤트 유형에 대한 각 이벤트 호출과 함께 제거됩니다.

예를 들어 플레이어 입력을 설정하려면 다음을 수행합니다:

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의 수명만 관리합니다. 따라서 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.OnEnabled 또는 EntityView.OnEntityInstantiated에서 수행됩니다.

  • MonoBehaviour.OnEnabled, 여기서 코드로 이벤트를 구독할 수 있지만, EntityView의 EntityRef 및 에셋 GUID는 아직 설정되지 않았습니다.
  • EntityView.OnEntityInstantiated is a UnityEvent part of the EntityView component. It can be subscribed to via the in-editor menu. When OnEntityInstantiated 이 호출됩니다. EntityView의 EntityRef 및 에셋 GUID는 설정이 보장됩니다. 이벤트 구독 또는 사용자 지정 로직에 이러한 파라미터 중 하나가 필요한 경우 이 파라미터를 실행해야 합니다.
onentityinstantiated subscription menu in editor
OnEntityInstantiated subscription menu as seen in the Editor.

이벤트 또는 콜백에서 구독을 취소하려면 다음과 같은 보완 기능을 사용하면 됩니다:

  • OnEnabled에서 구독된 것은OnDisabled 에서 구독을 해지합니다..
  • OnEntityInstantiated에서 구독된 것은 OnEntityDestroyed 에서 구독을 해지합니다.
Back to top