시뮬레이션의 에셋
데이터 에셋 클래스
Quantum 에셋은 런타임 동안 불변의 데이터 컨테이너 역할을 하는 일반 C# 클래스입니다. Quantum에서 이러한 에셋이 어떻게 설계, 구현 및 사용되어야 하는지를 규정하는 몇 가지 규칙이 있습니다.
다음은 몇 가지 간단한 결정론적 속성을 가진 에셋 클래스(캐릭터 스펙)의 최소 정의입니다.
C#
namespace Quantum {
partial class CharacterSpec {
public FP Speed;
public FP MaxHealth;
}
}
에셋 클래스 정의는 partial이어야 하며 Quantum 네임스페이스에 포함되어야 합니다.
에셋 클래스의 인스턴스를 생성하고 데이터베이스에 로드하는 작업(유니티에서 편집)은 이 장의 뒷부분에서 다룹니다.
에셋 사용하고 링크하기
Quantum에 에셋 클래스(기본 AssetObject 클래스를 상속하도록 하고 인스턴스를 포함하도록 데이터베이스를 준비하여 내부 메타데이터 추가)라고 알려 줍니다.
C#
// this goes into a DSL file
asset CharacterSpec;
에셋 인스턴스는 참조로 가져와야 하는 변경할 수 없는 객체입니다. 일반 C# 객체 참조는 메모리에 정렬된 ECS 구조체에 포함될 수 없으므로 게임 상태(엔티티, 컴포넌트 또는 기타 데이터 구조체로부터) 내의 속성을 선언하려면 DSL 내부에 asset_ref 특수 타입을 사용해야 합니다:
C#
component CharacterData {
// reference to an immutable instance of CharacterSpec (from the Quantum asset database)
asset_ref<CharacterSpec> Spec;
// other component data
}
캐릭터 엔티티를 작성할 때 에셋 참조를 지정하려면 한 가지 옵션은 프레임 에셋 데이터베이스에서 직접 인스턴스를 가져와 속성에 설정하는 것입니다:
C#
// assuming cd is a pointer to the CharacterData component
// using the SLOW string path option (fast data driven asset refs will be explained next)
cd->Spec = frame.FindAsset<CharacterSpec>("path-to-spec");
에셋의 기본 용도는 런타임에 데이터를 읽고 시스템 내부의 모든 계산에 적용하는 것입니다. 다음 예에서는 할당된 CharacterSpec의 Speed 값을 사용하여 해당 캐릭터 속도(물리적 엔진)를 계산합니다:
C#
// consider cd a CharacterData*, and body a PhysicsBody2D* (from a component filter, for example)
var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
body->Velocity = FPVector2.Right * spec.Speed;
결정론에 대한 참고사항
위의 코드는 런타임 동안 캐릭터에 대해 원하는 속도를 계산하기 위해 Speed 속성만 읽지만 값(속도)은 변경되지 않습니다.
업데이트 내에서 런타임에 게임 상태 에셋 참조를 전환하는 것은 완전히 안전하고 유효합니다(asset_ref는 롤백 가능한 유형이기 때문에 게임 상태의 일부가 될 수 있음).
그러나 데이터 에셋의 속성의 values를 변경하는 것은 결정론적인 것이 아닙니다(에셋에 대한 내부 데이터는 게임 상태의 일부로 간주되지 않으므로 롤백 되지 않습니다).
다음 코드는 런타임 중에 안전한 것(ref 전환)과 안전하지 않은 것(내부 데이터 변경)의 예를 보여줍니다.
C#
// cd is a CharacterData*
// this is VALID and SAFE, as the CharacterSpec asset ref is part of the game state
cd->Spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// this is NOR valid NEITHER deterministic, as the internal data from an asset is NOT part of the transient game state:
var spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// (DO NOT do this) changing a value directly in the asset object instance
spec.Speed = 10;
AssetObjectConfig 속성
AssetObjectConfig
속성으로 에셋 링크 스크립트 생성을 조정할 수 있습니다.
C#
[AssetObjectConfig(GenerateLinkingScripts = false)]
partial class CharacterSpec {
// ...
}
- GenerateLinkingScripts (기본값=true) - 유니티에서 에셋을 편집 가능하게 만드는 스크립트가 생성되지 않도록 합니다.
- GenerateAssetCreateMenu (기본값=true) - 이 에셋에 대한 유니티
CreateAssetMenu
속성을 생성하지 못하도록 합니다. - GenerateAssetResetMethod (기본값=true) - 유니티 스크립트 가능 객체
Reset()
메소드(여기서 자산이 생성될 때 GUID가 자동으로 생성되는 곳)의 생성을 방지합니다. - CustomCreateAssetMenuName (기본값=null) -
CreateAssetMenu
이름을 덮어씁니다.null
로 설정하면 상속 그래프를 사용하여 메뉴 경로가 자동으로 생성됩니다. - CustomCreateAssetMenuOrder (기본값=-1) -
CreateAssetMenu
순서를 덮어씁니다. -1로 설정하면 알파벳 순서가 사용됩니다.
에셋 스크립트 위치를 덮어쓰고 AOT 파일 생성을 실행 중지합니다.
tools\codegen_unity\quantum.codegen.unity.dll.config
파일을 생성합니다. 주의 사항: 업그레이드할 때 파일이 손실될 수 있으므로 주의하십시오.
XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="RelativeLinkDrawerFilePath" value="Quantum/Editor/PropertyDrawers/Generated/AssetLinkDrawers.cs"/>
<add key="RelativeAssetScriptFolderPath" value="Quantum/AssetTypes/Generated"/>
<add key="RelativeGeneratedFolderPath" value="Quantum/Generated"/>
<add key="GenerateAOTFile" value="true"/>
<add key="EndOfLineCharacters" value=" "/>
</appSettings>
</configuration>
에셋 상속
데이터 에셋에 상속을 사용할 수 있으므로 개발자에게 훨씬 더 많은 유연성을 제공할 수 있습니다(특히 다형성 방법과 함께 사용할 경우).
상속의 기본 단계는 추상 기본 에셋 클래스를 만드는 것입니다(CharacterSpec 예제로 계속 진행합니다):
C#
namespace Quantum {
partial abstract class CharacterSpec {
public FP Speed;
public FP MaxHealth;
}
}
CharacterSpec의 구체적인 하위 클래스는 자체적인 사용자 지정 데이터 속성을 추가할 수 있으며 Serializable 타입으로 표시되어야 합니다:
C#
namespace Quantum {
[Serializable]
public partial class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
}
[Serializable]
public partial class WarriorSpec : CharacterSpec {
public FP Armour;
}
}
DSL 내
DSL에 에셋을 선언했으면 DSL에서 asset_ref<T>
을 입력하여 기본 클래스와 해당 하위 클래스에 대한 참조를 보관할 수 있습니다.
C#
component CharacterData {
// Accepts asset references to CharacterSpec base class and its sub-classes(MageSpec and WarriorSpec).
asset_ref<CharacterSpec> ClassDefinition;
FP CooldownTimer;
}
하위 클래스에 대한 참조를 유지하려면 먼저 asset import
를 사용하여 DSL에서 파생 에셋을 선언해야 합니다:
C#
asset CharacterSpec;
asset import MageSpec;
파생 자산이 DSL에 이미 선언된 경우 기본 클래스로 asset_ref<T>
를 사용하십시오. 예를 들어 DSL에서 CharacterSpec
대신 MageSpec
을 직접 사용하려면 다음을 작성해야 합니다:
C#
component MageData {
// Only accepts asset references to MageSpec class.
asset_ref<MageSpec> ClassDefinition;
FP CooldownTimer;
}
데이터-기반 다형성
구체적인 CharacterSpec 클래스를 직접 평가하는 게임 플레이 로직을 가지고 있으면 설계가 매우 잘못될 수 있으므로 에셋 상속은 다형 메소드와 결합할 때 더 효과적입니다.
데이터 에셋에 로직을 추가하는 것은 quantum.state 프로젝트에서 로직을 구현하는 것을 의미하며 이 로직은 여전히 다음과 같은 제한을 고려해야 합니다.
일시적인 게임 상태 데이터에 대해 작업합니다. 즉, 데이터 에셋의 로직 메소드가 임시 데이터를 파라미터로 수신해야 합니다(엔티티 포인터 또는 프레임 객체 자체).
에셋 자체에 대한 데이터는 읽기만 하고 수정해서는 안 됩니다. 에셋은 여전히 수정 불가 읽기 전용 인스턴스로 취급해야 합니다.
다음 예에서는 기본 클래스에 가상 메서드를 추가하고 하위 클래스 중 하나에 사용자 지정 구현을 추가합니다(이 문서의 맨 위에 Character 엔티티에 대해 정의된 Health 필드를 사용합니다):
C#
namespace Quantum {
partial unsafe abstract class CharacterSpec {
public FP Speed;
public FP MaxHealth;
public virtual void Update(Frame f, EntityRef e, CharacterData* cd) {
if (cd->Health < 0)
f.Destroy(e);
}
}
[Serializable]
public partial unsafe class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
// reads data from own instance and uses it to update transient health of Character pointer passed as param
public override void Update(Frame f, EntityRef e, CharacterData* cd) {
cd->Health += HealthRegenerationFactor * f.DeltaTime;
base.Update(f, e, cd);
}
}
}
각 CharacterData에 할당된 구체적인 에셋과는 독립적으로 이 유연한 메소드 구현을 사용하기 위해, 이것은 모든 시스템에서 실행할 수 있습니다:
C#
// Assuming cd is the pointer to a specific entity's CharacterData component, and entity is the corresponding EntityRef:
var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
// Updating Health using data-driven polymorphism (behavior depends on the data asset type and instance assigned to character
spec.Update(frame, entity, cd);
에셋에서 DSL 생성 구조체 사용하기
DSL에 정의된 Structs
는 에셋에도 사용할 수 있습니다. DSL 구조에는 [Serializable]
속성이 설명되어야 합니다. 그렇지 않으면 유니티에서 데이터를 검사할 수 없습니다.
[Serializable]
struct Foo {
int Bar;
}
asset FooUser;
Quantum 에셋에서 DSL struct
사용.
C#
namespace Quantum {
public partial class FooUser {
public Foo F;
}
}
구조체가 [Serializable]
이지 않은 경우(예: 유니온이거나 Quantum 컬렉션을 포함하기 때문에) 프로토타입을 대신 사용할 수 있습니다.
C#
using Quantum.Prototypes;
namespace Quantum {
public partial class FooUser {
public Foo_Prototype F;
}
}
프로토타입은 필요할 때 시뮬레이션 구조체로 구체화할 수 있습니다:
C#
Foo f = new Foo();
fooUser.F.Materialize(frame, ref f, default);
동적 에셋
에셋은 시뮬레이션을 통해 런타임에 생성할 수 있습니다. 이 기능을 DynamicAssetDB라고 합니다.
C#
var assetGuid = frame.AddAsset(new MageSpec() {
Speed = 1,
MaxHealth = 100,
HealthRegenerationFactor = 1
});
이러한 에셋은 다른 에셋과 마찬가지로 로드 및 처분될 수 있습니다.
C#
MageSpec asset = frame.FindAsset<MageSpec>(assetGuid);
frame.DisposeAsset(assetGuid);
동적 에셋은 피어 간에 동기화되지 않습니다. 대신, 새로운 에셋을 생성하는 코드는 결정론적이어야 하며 각 피어가 동일한 값을 사용하여 에셋을 보유하도록 보장해야 합니다.
위의 규칙에 대한 유일한 예외는 늦은 참가가 있는 경우입니다. 새 클라이언트는 최근의 프레임 데이터와 같이 DynamicAssetDB의 스냅샷을 수신합니다. 프레임의 직렬화와 달리 동적 에셋의 직렬화 및 역 직렬화는 시뮬레이션 외부에서 IAssetSerializer
인터페이스에 위임됩니다. 유니티에서 실행할 때 QuantumUnityJsonSerializer
가 기본적으로 사용됩니다. 모든 유니티-serializable 타입을 직렬화/직렬 해제할 수 있습니다.
DynamicAssetDB 초기화
시뮬레이션은 기존 동적 에셋으로 초기화할 수 있습니다. 시뮬레이션 중에 에셋을 추가하는 것과 마찬가지로, 이러한 자산은 클라이언트 전체에 걸쳐 결정적이어야 합니다.
첫째, DynamicAssetDB
의 인스턴스로 생성하고 에셋으로 채워야 합니다:
C#
var initialAssets = new DynamicAssetDB();
initialAssets.AddAsset(new MageSpec() {
HealthRegenerationFactor = 10
});
initialAssets.AddAsset(new WarriorSpec() {
Armour = 100
});
...
둘째, 인스턴스를 새 시뮬레이션으로 전달하려면 QuantumGame.StartParameters.InitialDynamicAssets
를 사용해야 합니다. 유니티에서는 QuantumGame
, QuantumRunner.StartParamters.InitialDynamicAssets
를 관리하는 것이 QuantumRunner
행동이기 때문이므로 대신 사용합니다.