시스템 (게임 로직)
소개
시스템은 Quantum의 모든 게임플레이 로직의 시작점입니다.
시스템이 예측/롤백 모델을 준수하도록 몇 가지 제한이 적용되지만 이러한 클래스는 일반 C# 클래스로 구현됩니다.
- 상태 비저장 상태여야 합니다(게임플레이 데이터(프레임 클래스의 인스턴스)는 Quantum 시뮬레이터에 의해 파라미터로 모든 시스템 업데이트에 전달됩니다).
- 결정론적 라이브러리와 알고리즘만 구현 및/또는 사용합니다(부동 소수점 수학, 벡터 수학, 물리학, 난수 생성, 경로 찾기 등을 위한 라이브러리를 제공합니다).
- Quantum 네임스페이스에 있습니다.
상속할 수 있는 세 가지 기본 시스템 클래스가 있습니다:
SystemMainThread
: 간단한 게임플레이 구현(콜백 시작 및 업데이트 + 시그널)을 지원SystemSignalsOnly
: 업데이트 없는 시스템을 사용하여 시그널을 구현(작업 스케줄을 설정하지 않음으로써 오버헤드를 줄일 수 있습니다)SystemBase
: 작업 그래프에 병렬 작업을 예약하는 데에만 사용됩니다(이 기본 매뉴얼에서는 다루지 않음).
코어 시스템
기본적으로 Quantum SDK는 SystemSetup
에 모든 Core 시스템을 포함합니다.
Core.CullingSystem2D()
: 예측 프레임에서Transform2D
컴포넌트를 사용하여 엔티티를 컬링합니다.Core.CullingSystem3D()
: 예측된 프레임의Transform3D
로 엔티티를 컬링합니다.Core.PhysicsSystem2D()
:Transform2D
및PhysicsCollider2D
를 사용하여 모든 엔티티에 대해 물리학을 실행합니다.Core.PhysicsSystem3D()
:Transform3D
및PhysicsCollider3D
를 사용하여 모든 엔티티에 대해 물리학을 실행합니다.Core.NavigationSystem()
: 모든 NavMesh 관련 컴포넌트에 사용됩니다.Core.EntityPrototypeSystem()
:EntityPrototypes
을 생성, 구체화 및 초기화합니다.Core.PlayerConnectedSystem()
:ISignalOnPlayerConnected
및ISignalOnPlayerDisconnected
시그널을 트리거하는 데 사용됩니다.Core.DebugCommand.CreateSystem()
: 상태 인스펙터가 엔티티를 즉시 인스턴스화/제거/수정하기 위해 데이터를 전송하는 데 사용합니다(에디터에서만 사용 가능!).
사용자의 편의를 위해 모든 시스템이 기본적으로 포함됩니다. 게임의 필수 기능에 따라 코어 시스템을 선택적으로 추가/제거할 수 있습니다. 예를 들어 게임이 2D 또는 3D 인지에 따라 PhysicsSystem2D
또는 PhysicsSystem3D
만 보관합니다.
기본 시스템
Quantum의 가장 기본적인 시스템은 SystemMainThread
에서 상속되는 C# 클래스입니다.
기본을 구현하려면 적어도 업데이트 콜백을 정의해야 합니다.
C#
namespace Quantum
{
public unsafe class MySystem : SystemMainThread
{
public override void Update(Frame f)
{
}
}
}
시스템 클래스에서 재정의할 수 있는 콜백은 다음과 같습니다.:
OnInit(Frame f)
: 게임플레이가 초기화 중일 때 한 번만 호출됩니다(게임 제어 데이터를 설정하기에 좋은 장소 등).Update(Frame f)
: 게임 상태(게임 루프 진입점)를 진전시키는 데 사용됩니다.OnDisabled(Frame f)
및OnEnabled(Frame f)
: 다른 시스템에 의해 시스템이 비활성화/활성화되면 호출됩니다.
사용 가능한 모든 콜백에는 동일한 매개변수(프레임 인스턴스)가 포함됩니다. 프레임 클래스는 엔티티, 물리, 탐색 및 불변 자산 객체(별도 장에서 다루게 될)와 같은 기타 항목을 포함한 모든 일시적 및 정적 게임 상태 데이터를 위한 컨테이너입니다.
그 이유는 Quantum의 예측/롤백 모델을 준수하려면 시스템이 상태 없음이어야 하기 때문입니다. Quantum은 모든 (상호 가능한) 게임 상태 데이터가 프레임 인스턴스에 완전히 포함된 경우에만 결정론을 보장합니다.
필요한 모든 데이터를 파라미터로 수신해야 하는 읽기 전용 상수 또는 전용 메소드를 만드는 것이 유효합니다.
다음 코드는 시스템에서 유효한 경우와 유효하지 않은 경우의 몇 가지 기본 예를 보여 줍니다(상태 비저장 요구 사항 위반):
C#
namespace Quantum
{
public unsafe class MySystem : SystemMainThread
{
// 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;
}
}
}
시스템 설정
게임을 초기화하는 동안 Quantum의 시뮬레이터에 콘크리트 시스템 클래스를 주입해야 합니다.
이 작업은 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.PhysicsSystem(),
new Core.NavMeshAgentSystem(),
new Core.EntityPrototypeSystem(),
// user systems go here
new MySystem(),
};
}
}
}
Quantum에는 몇 가지 사전 구축된 시스템(물리 엔진 업데이트, Navmesh 및 엔티티 프로토타입 인스턴스화를 위한 진입점)이 포함되어 있습니다.
결정론을 보장하기 위해 시스템이 삽입되는 순서는 시뮬레이터가 모든 클라이언트에서 모든 콜백을 실행하는 순서입니다. 따라서 업데이트 발생 순서를 제어하려면 원하는 순서대로 사용자 지정 시스템을 삽입하면 됩니다.
시스템 활성화 및 비활성화하기
주입된 모든 시스템은 기본적으로 활성화되지만 시뮬레이션의 모든 위치에서 다음 일반 함수를 호출하여 런타임에 상태를 제어할 수 있습니다(프레임 객체에서 사용 가능).
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>();
}
어떤 시스템이라도 다른 시스템을 비활성화(다시 활성화)할 수 있으므로 일반적인 패턴은 간단한 상태 머신을 사용하여 보다 전문화된 시스템의 활성/비활성 라이프 사이클을 관리하는 메인 컨트롤러 시스템을 갖는 것입니다(한 가지 예는 게임 내 로비, 게임 플레이 카운트다운, 이후 정상적인 게임 플레이, 그리고 마지막으로 점수 상태).
특별한 시스템 타입
대부분의 시스템에 기본 SystemMainThread
타입을 사용할 수 있지만 Quantum은 특수 시스템을 위한 몇 가지 대체 옵션을 제공합니다.
System | 설명 |
---|---|
SystemMainThread | 가장 일반적인 시스템 타입입니다. 일반적인 기능을 모두 갖춘 일반 Update()를 구현합니다. |
SystemSignalsOnly | Update() 함수가 없습니다. 다른 시스템으로부터 시그널을 수신하고 구현하는 데만 집중하는 시스템을 위한 것입니다. Update 루프를 피함으로써 일부 오버헤드를 줄일 수 있습니다. |
SystemMainThreadFilter | 이 타입의 시스템은 유형 T의 FilterStruct를 사용하여 엔티티 집합을 필터링하고 해당 엔티티 집합을 순환한 다음 메서드를 호출합니다. 주의: 파라미터가 없는 경우에는 SystemMainThread에서 상속받은 후 필터를 통해 직접 반복하는 것이 좋습니다(FilterStructs 및 Filters에 대한 자세한 내용은 구성 요소 페이지 참조). |
시스템 그룹
시스템을 설정하고 그룹으로 처리할 수 있습니다.
첫 번째 단계에서는 SystemMainThreadGroup
에서 상속하는 클래스를 만듭니다.
C#
namespace Quantum
{
public class MySystemGroup : SystemMainThreadGroup
{
public MySystemGroup(string update, params SystemMainThread[] children) : base(update, children)
{
}
}
}
MySystemGroup
시스템은 이제 SystemSetup.cs
에서 시스템을 그룹화하는 데 사용할 수 있습니다. 시스템 그룹은 일반 시스템과 혼합하여 사용할 수 있습니다.
C#
namespace Quantum {
public static class SystemSetup {
public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) {
return new SystemBase[] {
new MyRegularSystem(),
new MySystemGroup("Gameplay Systems", new MyMovementSystem(), new MyOrbitScanSystem()),
};
}
}
}
이를 통해 코드 한 줄로 시스템 집합을 활성화/비활성화할 수 있습니다. 시스템 그룹을 사용/사용 안 함으로 설정하면 시스템 그룹에 속한 모든 시스템이 사용/사용 안 함으로 설정됩니다. 주의: Frame.SystemEnable<T>()
그리고 Frame.SystemDisable<T>()
메소드는 타입별로 시스템을 식별하므로, 여러 시스템 그룹이 있는 경우 여러 시스템 그룹을 독립적으로 활성화/비활성화할 수 있는 자체 구현이 필요합니다.
엔티티 라이프사이클 API
이 섹션에서는 엔티티 생성 및 구성에 직접 API 메소드를 사용합니다. 데이터 중심 접근 방식은 엔티티 프로토타입 장을 참조하십시오.
새 엔티티 인스턴스를 작성하려면 다음을 사용합니다(메소드는 EntityRef를 반환합니다):
C#
var e = frame.Create();
엔티티에는 더 이상 미리 정의된 컴포넌트가 없습니다. 이 엔티티에 Transform3D 및 PhysicsCollider3D를 추가하려면 다음을 입력하십시오.
C#
var t = Transform3D.Create();
frame.Set(e, t);
var c = PhysicsCollider3D.Create(f, Shape3D.CreateSphere(1));
frame.Set(e, c);
이 두개의 메소드는 또한 유용합니다:
C#
// destroys the entity, including any component that was added to it.
frame.Destroy(e);
// checks if an EntityRef is still valid (good for when you store it as a reference inside other components):
if (frame.Exists(e)) {
// safe to do stuff, Get/Set components, etc
}
또한 엔티티에 특정 컴포넌트 타입이 포함되어 있는지 확인하고 프레임에서 컴포넌트 데이터에 대한 포인터를 직접 가져올 수 있습니다.
C#
if (frame.Has<Transform3D>(e)) {
var t = frame.Unsafe.GetPointer<Transform3D>(e);
}
ComponentSet을 사용하면 엔티티에 여러 개의 컴포넌트가 있는지 한 번 확인할 수 있습니다:
C#
var components = ComponentSet.Create<CharacterController3D, PhysicsBody3D>();
if (frame.Has(e, components)) {
// do something
}
컴포넌트를 동적으로 제거하는 것은 다음과 같이 쉽습니다:
C#
frame.Remove<Transform3D>(e);
EntityRef 타입
Quantum의 롤백 모델은 가변 크기의 프레임 버퍼를 유지합니다. 즉, DSL에서 정의된 게임 상태 데이터의 여러 복사본은 별도의 위치의 메모리 블록에 보관됩니다. 즉, 엔티티, 컴포넌트 또는 구조체에 대한 포인터는 단일 프레임 객체(업데이트 등) 내에서만 유효합니다.
엔티티 참조는 해당 엔티티가 아직 존재하는 한 프레임에서 작동하는 엔티티(일시적으로 포인터 교체)에 대한 안전하게 보관되는 참조입니다. 엔티티 참조에는 다음과 같은 데이터가 내부적으로 포함됩니다.
- 엔티티 인덱스: 엔티티 슬롯, 특정 유형에 대해 DSL이 정의한 최대 숫자입니다.
- 엔티티 버전 번호: 엔티티 인스턴스가 제거되고 슬롯을 새 항목에 재사용할 수 있는 경우 이전 엔티티 참조를 더 이상 렌더링 하는 데 사용됩니다.
필터
Quantum v2에 엔티티 타입이 없습니다. 스파스 집합 ECS 메모리 모델에서 엔티티는 컴포넌트 모음의 인덱스이며 EntityRef 타입은 버전 관리와 같은 일부 추가 정보를 포함합니다. 이러한 컬렉션은 동적으로 할당된 스파스 세트에 보관됩니다.
따라서 엔티티 집합에서 반복하는 대신 필터를 사용하여 시스템이 작동할 컴포넌트 집합을 작성합니다.
C#
public unsafe class MySystem : SystemMainThread
{
public override void Update(Frame f)
{
var filtered = frame.Filter<Transform3D, PhysicsBody3D>();
while (filtered.Next(out var e, out var t, out var b)) {
t.Position += FPVector3.Forward * frame.DeltaTime;
frame.Set(e, t);
}
}
}
필터 사용 방법에 대한 포괄적인 뷰는 컴포넌트 페이지를 참조하십시오.
데이터 기반 에셋
데이터 중심 접근 방식은 일반적으로 게임을 구현할 때 매우 강력할 수 있으므로 Quantum에는 읽기 전용 데이터(초기화를 위한 데이터 또는 런타임 사용을 위한 매개 변수화된 데이터)를 시뮬레이션에 주입하는 매우 유연한 에셋 연결 시스템이 포함되어 있습니다.
데이터 기반 에셋에 대해서는 다음 장에서 더 자세히 다루지만, 데이터 기반 에셋을 사용하기 위한 진입점은 시스템이기 때문에 여기에 몇 가지 예가 포함되어 있습니다.
사전 구축 에셋과 구성 클래스
Quantum에는 항상 프레임 개체를 통해 Systems로 전달되는 몇 가지 사전 구축된 데이터 에셋이 포함되어 있습니다.
다음은 Quantum의 에셋 DB에서 가장 중요한 사전 빌드 된 에셋 개체입니다.
Map
과NavMesh
: 재생 가능 영역, 정적 물리 콜라이더, 내비게이션 메시 등에 대한 데이터입니다. 사용자 정의 플레이어 데이터는 데이터 에셋 슬롯에서 추가할 수 있습니다(데이터 에셋 챕터에서 다룰 것).SimulationConfig
: 물리 엔진, 내비게이션 시스템 등에 대한 일반 구성 데이터- 기본
PhysicsMaterial
및agent configs
(KCC, navmesh, 등):
다음 코드는 프레임 개체에서 현재 Map 및 NavMesh 인스턴스에 접근하는 방법을 보여줍니다.
C#
// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];
에셋 데이터베이스
모든 Quantum 데이터 에셋은 동적 에셋 데이터베이스 API를 통해 시스템 내에서 사용할 수 있습니다. 다음 코드(DSL 이후 시스템의 C# 코드)에서는 데이터베이스에서 데이터 에셋을 가져와 asset_ref 슬롯에 캐릭터를 할당하는 방법을 보여 줍니다. 먼저 qtn 파일로 에셋을 선언하고 이를 보관할 수 있는 컴포넌트를 생성합니다.
C#
asset CharacterSpec;
component CharacterData
{
asset_ref<CharacterSpec> Spec;
// other data
}
참조를 보유하고 있는 에셋 및 컴포넌트가 선언되면 다음과 같은 시스템에서 참조를 설정할 수 있습니다.
C#
// C# code from inside a System
// grabing the data asset from the database, using the unique GUID (long) or path (string)
var spec = frame.FindAsset<CharacterData>("path-to-spec");
// assigning the asset reference assuming you have a pointer to CharacterData component
data->Spec = spec;
데이터 에셋은 각각의 장에서 자세히 설명합니다(유니티 스크립트 가능 객체 - 기본; 사용자 지정 직렬화 또는 절차적으로 생성된 콘텐츠를 통해 채우는 방법에 대한 옵션 포함).
시그널
이전 장에서 설명한 것처럼 신호는 시스템 간 통신을 위한 게시자/가입자 API를 생성하는 데 사용되는 함수 서명입니다.
DSL 파일의 다음 예는 이전 장의 예입니다:
C#
signal OnDamage(FP damage, entity_ref entity);
"퍼블리셔" 시스템에서 호출할 수 있는 프레임 클래스(f 변수)에서 이 트리거 신호가 생성됩니다.
C#
// any System can trigger the generated signal, not leading to coupling with a specific implementation
f.Signals.OnDamage(10, entity)
"가입자" 시스템은 생성된 "ISignalOnDamage" 인터페이스를 구현하며 코드는 다음과 같습니다:
C#
namespace Quantum
{
class CallbacksSystem : SystemSignalsOnly, ISignalOnDamage
{
public void OnDamage(Frame f, FP damage, EntityRef entity)
{
// this will be called everytime any other system calls the OnDamage signal
}
}
}
알림 신호는 일반적으로 게임 상태에 유용한 모든 작업을 수행하는 데 필요하므로 항상 프레임 개체를 첫 번째 파라미터로 포함합니다.
새성 및 사전 구축된 시그널
DSL에 직접 정의된 명시적 시그널 외에도 Quantum에는 미리 빌드된("원시" 물리 충돌 콜백) 및 엔티티 정의에 따라 생성된 콜백(엔티 유형별 생성/파괴 콜백)도 포함되어 있습니다.
충돌 콜백 신호는 물리 엔진에 대한 특정 챕터에서 다루므로, 다음과 같이 사전 구축된 다른 시그널에 대한 간략한 설명이 제공됩니다.
ISignalOnPlayerDataSet
: 게임 클라이언트가 RuntimePlayer의 런타임 인스턴스를 서버로 보낼 때 호출됩니다. (그리고 데이터는 하나의 틱에 확인/연결됩니다.)ISignalOnAdd<T>
,ISignalOnRemove<T>
: 엔티티에 컴포넌트 타입 T가 추가/제거되면 호출됩니다.
이벤트 트리거하기
시그널에 발생하는 것과 마찬가지로 이벤트를 트리거하는 진입점은 프레임 객체이며, 각(구체) 이벤트는 이벤트 데이터를 파라미터로 하여 생성된 특정 함수를 생성합니다.
C#
// taking this DSL event definition as a basis
event TriggerSound
{
FPVector2 Position;
FP Volume;
}
시스템에서 호출하여 이 이벤트의 인스턴스를 트리거할 수 있습니다(유니티에서 처리하는 것은 부트스트랩 프로젝트에 대한 장에서 다룹니다).
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);
중요한 점은 (유니티 측의 콜백은 결정적이지 않기 때문에) 게임 자체를 구현하기 위해 이벤트를 사용해서는 안 된다는 것입니다. 이벤트는 자세한 게임 상태 업데이트의 렌더링 엔진을 전달하기 위한 단방향 미세 API로, 비주얼, 사운드 및 UI 관련 개체를 유니티에서 업데이트할 수 있습니다.
추가 프레임 API 아이템
프레임 클래스에는 임시 데이터로 처리해야 하는 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