이벤트 & 콜백
소개
시뮬레이션(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
멤버 특수 키워드가 있는 경우 remote
및 local
을 사용할 수 있습니다
클라이언트의 유니티에서 이벤트가 디스패치되기 전에 키워드를 지정하면 각각 remote
또는 local
플레이어에 할당된 경우 player_ref
가 선택됩니다. 모든 조건이 일치하면 이 클라이언트에서 이벤트가 발송됩니다.
C#
event LocalPlayerOnly {
local player_ref player;
}
C#
event RemotePlayerOnly {
remote player_ref player;
}
요약: 시뮬레이션 자체는 remote
과 local
의 개념에 구애받지 않습니다. 키워드는 특정 이벤트가 개별 클라이언트 뷰에서 발생한 경우에만 변경됩니다.
이벤트에 여러 개의 player_ref
파라미터가 있는 경우 local
과 remote
를 결합할 수 있습니다. 이 이벤트는 LocalPlayer
를 제어하는 클라이언트와 RemotePlayer
가 다른 플레이어에 할당된 경우에만 트리거됩니다.
C#
event MyEvent {
local player_ref LocalPlayer;
remote player_ref RemotePlayer;
player_ref AnyPlayer;
}
클라이언트가 여러 플레이어(예: 분할 화면)를 제어하는 경우 모든 player_ref
는 로컬로 간주됩니다.
클라이언트, 서버
Quantum 2.1 부터
이벤트를 실행할 범위를 지정하려면 client
및 server
키워드를 사용하여 이벤트를 확인할 수 있습니다. 기본적으로 모든 이벤트는 클라이언트와 서버에서 발송됩니다.
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()
을 사용하여 엔티티를 생성할 때 프레임 시뮬레이션이 완료되면 다음 콜백이 순서대로 실행됩니다:
OnUpdateView
, 새로 생성된 엔티티에 대한 뷰가 인스턴스화됩니다.Monobehaviour.Awake
Monobehaviour.OnEnabled
EntityView.OnEntityInstantiated
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. WhenOnEntityInstantiated
이 호출됩니다. EntityView의 EntityRef 및 에셋 GUID는 설정이 보장됩니다. 이벤트 구독 또는 사용자 지정 로직에 이러한 파라미터 중 하나가 필요한 경우 이 파라미터를 실행해야 합니다.
이벤트 또는 콜백에서 구독을 취소하려면 다음과 같은 보완 기능을 사용하면 됩니다:
OnEnabled
에서 구독된 것은OnDisabled
에서 구독을 해지합니다..OnEntityInstantiated
에서 구독된 것은OnEntityDestroyed
에서 구독을 해지합니다.