Quantum Systems (게임 로직)
Systems는 Quantum에서 모든 게임플레이 로직에 대한 진입점입니다.
예측/롤백 모델과의 호환을 위해 System에 몇 개의 제약사항이 적용되었지만 일반 C#으로 구현되었습니다:
- 스테이트리스가 되어야 합니다 (게임 플레이 데이터는 Quantum의 시뮬레이터에 의해 모든 게임 루프 콜백에 매개 변수로 전달됩니다);
- C# 포인터를 사용합니다 (따라서 Systems는 unsafe 키워드를 사용해야만 합니다);
- 결정론적 라이브러리와 알고리즘만을 구현하거나 사용합니다 (우리는 부동 소수점 수학, 벡터 수학, 물리, 랜덤 번호 생성, 경로 찾기 등을 위한 라이브러리를 제공합니다);
- Quantum 네임스페이스 내에 있어야 합니다;
기본 System
Quantum 내의 기본 System은 C# 클래스로 "SystemBase" 로부터 상속됩니다.
구현 골격은 최소한 Update 콜백이 정의되는 것이 필요합니다.
C#
namespace Quantum
{
public unsafe class MySystem : SystemBase
{
public override void Update(Frame f)
{
}
}
}
System 클래스에서 오버라이드 될 수 있는 콜백들이 있습니다:
- OnInit(Frame f): 게임플레이가 초기화 될 때 한 번만 호출됩니다.(초기 엔티티 생성, 게임 데이터 설정등을 하기위한 좋은 곳 입니다);
- Update(Frame f): 게임 상태를 업데이트하기 위해 사용됩니다 (게임 루프 진입점);
- OnDisabled(Frame f)와 OnEnabled(Frame f): 다른 시스템에 의해서 시스템에 사용불가/사용 되어질 때 호출됩니다 (이 부분은 다음 장에서 다룹니다);
모든 사용할 수 있는 콜백들은 동일한 파라미터를 포함합니다 (Frame).
Frame 클래스는 엔티티용 생성된 API, 물리등을 포함한(이에 대해서는 각각 별도로 다룰것입니다) 모든 일시적이고 정적 게임 상태 데이터에 대한 컨테이너입니다.
이 이유는 Quantum의 예측/롤백 모델에 부합하기 위해 시스템은 반드시 stateless이어야 합니다.
읽기 전용 상수 또는 프라이빗 메소드를 생성하는 것이 유효합니다(반드시 모든 필요한 데이터는 파라미터로써 받아야 합니다).
다음의 코드는 System 내에 유효한 것과 유효하지 않는 것(stateless 요건 위반)에 대한 기본 예제입니다
C#
namespace Quantum
{
public unsafe class MySystem : SystemBase
{
// this is ok
private const int _readOnlyData = 10;
// this is NOT ok (this data will not be rolled back, so it would lead to instant drifts between game clients during rollbacks)
private int _transientData = 10;
public override void Update(Frame f)
{
// ok to use a constant to compute something here
var temporaryData = _readOnlyData + 5;
// NOT ok to modify transient data that lives outside of the Frame object:
_transientData = 5;
}
}
}
시스템 설정
게임 플레이 초기화 중에는 콘크리트 System 클래스를 Quantum 시뮬레이터에 넣어주어야 합니다.
이는 quantum.systems 프로젝트내에 포함되어 있는 "SystemSetup.cs" 파일을 통해 수행됩니다.
C#
namespace Quantum
{
public static class SystemSetup
{
public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig)
{
return new SystemBase[]
{
// pre-defined core systems
new Core.PhysicsSystemPre(),
// user systems go here
new MySystem(),
// pre-defined core systems
new Core.AnimatorSystem(),
};
}
}
}
Quantum에는 사전 구축된 시스템이 포함되어 있다는 점에 주목하십시오 (물리 엔진 업데이트와 결정론적 애니메이터 매니저에 대한 진입점)
결정론을 보장하기 위해 시스템이 삽입되는 순서는 모든 클라이언트에서 시뮬레이터에 의해 실행되는 모든 콜백들의 순서입니다.
따라서 업데이트가 발생하는 순서를 제어하려면 원하는 순서대로 커스텀 시스템을 삽입하면 됩니다.
시스템 활성화와 비활성화
주입된 모든 시스템은 기본적으로 활성화되지만 시뮬레이션의 어느 곳에서라도 이러한 일반 기능을 호출하여 런타임에서 상태를 제어하는 것이 가능합니다(Frame 객체에서 사용 가능합니다).
C#
public override void OnInit(Frame f)
{
// deactivates MySystem, so no updates (or signals) are called in it
f.SystemDisable<MySystem>();
// (re)activates MySystem
f.SystemEnable<MySystem>();
// possible to query if a System is currently enabled
var enabled = f.SystemIsEnabled<MySystem>();
}
모든 시스템이 다른 시스템을 비활성화(그리고 재활성화)할 수 있으므로, 하나의 일반적인 패턴은 간단한 상태 머신을 사용하여 더 전문화된 시스템의 활성/비활성 수명 주기를 관리하는 메인 컨트롤러 시스템을 갖는 것 입니다(하나의 예를 들자면 게임내 게임플레이에 쿨다운을 하는 로비를 첫 번째로 가지고나서 일반 게임플레이와 최종의 점수 상태를 갖는 것입니다).
엔티티 생성 API
시스템은 Quantum 게임에서 게임플레이 로직의 진입점이므로, 실제 유용한 것은 엔티티들의 라이플 사이클을 기준으로 움직인다는 것 입니다:생성, 업데이트, 파괴.
엔티티 유형이 DSL 파일에 정의되어 있으면 인스턴스 라이프사이클을 관리하는 데 필요한 모든 API 호출이 자동으로 생성됩니다.
따라서 이전 장(DSL)에서 다루었던 다음과 같은 "Character" 엔티티 기반을 예로 들 수 있습니다.
C#
// this goes in a Quantum DSL file
entity Character[8]
{
use Transform2D;
use DynamicBody;
}
다음의 함수들은 Frame 클래스내에서 사용될 것 입니다:
C#
// finds the next free Character slot (from 8 pre-allocated ones) and returns a pointer to it.
Returns null if there are no free slots;
var c = f.CreateCharacter();
// entity refs (explained in the next section) are rollbackable and safe to store across frame both in the game state and from Unity
var reference = c->EntityRef;
// entity references can be used to retrive back the pointer from the Frame object (returns null if ref is obsolete/invalid)
var ca = f.GetCharacter(reference);
// optional check to avoid the null return:
var exists = f.CharacterExists(reference);
// to destroy an entity (also making all refs to it obsolete), one can use either the pointer or a ref.
// This frees up a Character entity slow as well:
f.DestroyCharacter(c);
f.DestroyCharacter(reference);
EntityRef 유형
Quantum의 롤백 모델은 다양한 크기의 프레임 버퍼를 유지한다는 것을 의미합니다. 즉, 게임 상태 데이터(DSL에서 정의된)의 여러 복사본이 별개의 위치에서 메모리 블록에 보관됩니다.
즉, 엔티티 또는 컴포넌트 또는 구조체에 대한 포인터는 단일 Frame 개체 내에서만 유효합니다(업데이트 등).
해당 엔티티가 여전히 존재하는 한, 엔티티 refs는 프레임 전체에서 작동하는 엔티티(일시적으로 포인터를 대체)에 대한 안전한 참조입니다.
엔티티 refs에는 내부적으로 다음 데이터가 있습니다:
- Entity 유형: DSL-정의된 유형으로 부터;
- Entity 인덱스: 엔티티 슬롯, 특수 유형용 DSL-정의된 최대 숫자로 부터;
- Entity 버전 번호: 엔티티 인스턴스가 제거되고 슬롯을 새 인스턴스로 재사용할 수 있을 때 오래된 엔티티를 더 이상 사용하지 않도록 만드는 데 사용됩니다.
엔티티 데이터 갱신
특정 엔티티 유형의 모든 활성(생성된) 인스턴스에 액세스하려면 생성된 반복 접근자(iterator accessor)를 사용해야 합니다.
C#
// this returns an iterator with pointers to all currently active Character entities
var all = f.GetAllCharacters();
// looping through the iterator
while (all.Next())
{
// retrieving the Character pointer
var c = all.Current;
// Updating data in the entity:
c->Transform2D.Position += FPVector2.Up * f.DeltaTime;
}
직접 컴포넌트 갱신
게임 플레이 업데이트에 대한 흥미로운 접근법은 엔터티 대신 컴포넌트 유형을 통해 게임 상태를 이동하는 기능입니다.
이렇게 하면 컴포넌트 재사용을 하도록 하게 됩니다. 이 경우 시스템이 해당 컴포넌트를 사용하는 엔티티 유형에 대한 지식 없이 데이터를 업데이트할 수 있습니다(이 예제에서는 해당 유형에 관계없이 활성 엔티티의 모든 DynamicBody 컴포넌트에 힘을 가해주게 됩니다).
C#
// acquiring the component buffer based on the component type (generated function).
Buffers are pooled, so the using keyword must be used to safely return them to the pool.
using (var bodies = f.GetAllDynamicBodies())
{
// a component buffer is traversed with a for loop
for (int i = 0; i < bodies.Count; i++)
{
// each entry includes the Component pointer (in this case DynamicBody)
var b = bodies[i];
// applying a force, no need to know which entity type is this
b.DynamicBody->AddForce(FPVector2.Up / 2);
// entity pointer is also available if needed (type information can be queried, etc)
var e = b.Entity;
}
}
데이터-기반 에셋
일반적으로 게임을 구현할 때 데이터 기반 접근법은 매우 강력하므로 Quantum에는 읽기 전용 데이터를 시뮬레이션에 주입하기 위한 매우 유연한 에셋 링킹 시스템이 포함되어 있습니다(런타임 사용을 위한 초기화 또는 파라미터화된 데이터).
데이터 기반 에셋에 대한 자세한 내용은 다음 장에서 다룰 예정이지만 이러한 자산 사용의 진입점은 Systems이며, 여기에 몇 가지 예가 있습니다.
사전-구축된 에셋
Quantum에는 Frame 객체를 통해 항상 시스템에 전달되는 몇 가지 사전 구축된 데이터 에셋이 포함되어 있습니다.
가장 중요한 것들은 다음과 같습니다:
- RuntimeConfig: 일반 게임 환경구성 데이터 (RuntimePlayers의 배열을 포함합니다 - 아래에서 설명)로 "RuntimeConfig.User.cs" 파일내에서 확장될 수 있습니다;
- RuntimePlayer: 플레이어 지정 런타임 데이터를 주입하기 위해 좋은 곳 (예를 들면, 선택한 캐릭터 상세내용).
커스텀 데이터는 "RuntimePlayer.User.cs" 에 추가될 수 있습니다; - Map: 플레이가능 영역에 대한 데이터, 정적 물리 콜라이더.
커스텀 플레이어 데이터는 데이터 에셋 슬롯에 추가될 수 있습니다 (데이터 에셋 장에서 다룰 예정입니다);
다음 코드는 Frame 객체에서 이 데이터를 어떻게 접근하는지 보여주고 있습니다:
C#
RuntimeConfig config = f.RuntimeConfig;
// basic data in runtime config is the players array
for (int i = 0; i < config.Players.Length; i++)
{
RuntimePlayer p = config.Players[i];
}
// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];
에셋 데이터베이스
모든 Quantum 데이터 에셋은 "DB" 정적 클래스를 통해 Systems 내에서 사용할 수 있습니다.
다음 코드(DSL 후 System에서 C# 코드)는 데이터베이스에서 데이터 에셋을 획득하고 이를 asset_ref 슬롯에 할당하는 방식을 나타내주고 있습니다.
C#
// this goes in a Quantum DSL file
asset CharacterSpec;
entity Character[8]
{
use Transform2D;
use DynamicBody;
fields
{
asset_ref<CharacterSpec> Spec;
}
}
C#
// C# code from inside a System
// grabing the data asset from the database, using a unique string ID
var spec = DB.FindAsset<CharacterData>("spec-guid");
// assigning the asset reference to the first Character entity
f.GetAllCharacters().Current->Spec = spec;
데이터 에셋은 데이터 에셋 장에 자세히 설명되어 있습니다 (Unity 스크립트 가능 객체를 통해 데이터를 채우는 방법에 대한 옵션을 포함합니다. 기본값, 커스텀 시리얼라이저 또는 절차적으로 생성된 콘텐츠).
시그널
이전 장에서도 설명했지만, 시그널은 함수 서명으로 내부 시스템 커뮤니케이션을 위해 게시자/가입자 API 생성을 위해 사용됩니다.
다음은 DSL 파일내의 예제입니다(이전 장에서):
C#
signal OnDamage(FP Damage);
Frame 클래스(f 변수)에서 이 트리거 신호가 생성되어 "게시자" 시스템에서 호출할 수 있습니다.
C#
// any System can trigger the generated signal, not leading to coupling with a specific implementation
f.Signals.OnDamage(10)
"가입자" 시스템은 다음과 같이 생성된 "ISignalOnDamage" 인터페이스를 구현합니다.
C#
namespace Quantum.Systems.Example
{
class CallbacksSystem : SystemBase, ISignalOnDamage
{
public void OnDamage(Frame f, Damage dmg)
{
// this will be called everytime any other system calls the OnDamage signal
}
public override void Update(Frame f)
{
}
}
}
알림 시그널은 일반적으로 게임 상태에 유용한 작업을 하는 데 필요 Frame 개체를 항상 첫 번째 파라미터로 포함한다는 것에 주의하십시오.
생성되고 사전 구축된 시그널
DSL에 직접 정의된 명시적 시그널 이외에도, Quantum에는 미리 구성된 일부(예를들면 "원시" 물리 충돌 콜백)와 엔티티 정의(엔티티 유형별 생성/파괴 콜백)를 기반으로 생성된 시그널도 포함되어 있습니다.
충돌 콜백 신호는 물리 엔진에 대한 장에서 다루게 되므로, 여기에서는 생성/파괴 시그널에 대해서는 다음과 같이 간략하게 설명하겠습니다.
- ISignalOnEntityCreated/ISignalOnEntityDestroyed: 모든 엔티티를 생성/파괴할 때마다 호출되는 사전 구축된 시그널(params는 Frame이고 질문의 진입점)
파괴 신호는 프레임 업데이트가 완료될 때까지 Quantum의 엔티티 파괴가 항상 유예되기 때문에 특히 유용합니다(모든 시스템 업데이트가 호출됨). - ISignalOnENTITY_TYPECreated/ISignalOnENTITY_TYPEDestroyed (예 ISignalOnCharacterCreated): 위와 유사하지만, 타입-지정 포인터를 전달합니다;
이벤트 트리거
시그널과 유사하게, 이벤트를 트리거하는 진입점은 Frame 객체이며, 각 (콘크리트) 이벤트는 (이벤트 데이터를 매개 변수로 사용) 생성된 특정 함수를 생성합니다.
C#
// taking this DSL event definition as a basis
event TriggerSound
{
FPVector2 Position;
FP Volume;
}
System에서 호출하여 이 이벤트의 인스턴스를 트리거할 수 있습니다. Unity에서 처리하는 작업은 bootstrap 프로젝트에 대한 장에서 다룹니다.
C#
// any System can trigger the generated events (FP._0_5 means fixed point value for 0.5)
f.Events.TriggerSound(FPVector2.Zero, FP._0_5);
이벤트(Unity 측의 콜백이 결정론적 요소가 아니기 때문에)는 게임플레이 자체를 구현하기 위해 이벤트를 사용해서는 안 됩니다.
이벤트는 상세 게임 상태 업데이트의 렌더링 엔진을 통신하는 단방향으로 세밀하게 조정하는 API이므로, 시각, 소리 및 UI 관련 객체들을 Unity에서 업데이트할 수 있습니다.
추가 Frame API 항목
또한 Frame 클래스에는 임시 데이터로 취급해야 하는 API의 다른 결정론적 부분에 대한 진입점이 포함되어 있습니다(필요한 경우 롤백됩니다).
다음은 중요한 코드입니다.
C#
// RNG is a pointer.
// Next gives a random FP between 0 and 1.
// There are also bound options for both FP and int
f.RNG->Next();
// any property defined in the global {} scope in the DSL files is accessed through the Global pointer
var d = f.Global->DeltaTime;
// input from a player is referenced by its index (i is a pointer to the DSL defined Input struct)
var i = f.GetPlayerInput(0);
Back to top