This document is about: QUANTUM 2
SWITCH TO

컴포넌트

소개

컴포넌트는 엔티티에 붙일 수 있고 부착된 구성요소를 기반으로 활성 엔티티의 하위 집합만 반복하여 필터링하는 데 사용할 수 있는 특수 구조입니다.

Quantum은 사용자 지정 컴포넌트 외에도 다음과 같은 몇 가지 사전 컴포넌트가 제공됩니다.

  • Transform2D/Transform3D: 고정점(FP) 값을 사용하여 위치 및 회전합니다;
  • PhysicsCollider, PhysicsBody, PhysicsCallbacks, PhysicsJoints(2D/3D): Quantum의 상태 비저장 물리 엔진에서 사용됩니다.
  • PathFinderAgent, SteeringAgent, AlizionAgent, AlizionAgent와 같은 주요 구성 요소가 있습니다.OBstacle: 항법 기반 경로 찾기 및 이동입니다.

컴포넌트

DSL에서 기본적인 컴포넌트를 정의하는 기본 예제입니다:

C#

component Action
{
    FP Cooldown;
    FP Power;
}

구조체 대신 컴포넌트로 레이블을 지정(위와 같이) 하면 적절한 코드 구조(마커 인터페이스, id 속성 등)가 생성됩니다. 컴파일이 완료되면 컴포넌트 프로토타입과 함께 사용할 수 있도록 유니티 편집기에서도 사용할 수 있습니다. 편집기에서 사용자 지정 컴포넌트의 이름은 Entity Component ComponentName입니다.

컴포넌트에서 작업할 API는 Frame 클래스를 통해 제공됩니다.
컴포넌트 또는 포인터를 통해 컴포넌트에 대한 복사본 작업을 수행할 수 있습니다. 접근 타입을 구분하기 위해 복사본 작업을 위한 API는 Frame을 통해 직접 접근할 수 있으며 포인터 액세스를 위한 API는 Frame.Unsafe에서 사용할 수 있습니다 - 후자가 메모리를 수정합니다.

컴포넌트를 추가, 가져오기 및 설정하는 데 필요한 가장 기본적인 기능은 같은 이름의 함수입니다.

Add<T>는 엔티티에 컴포넌트를 추가하는 데 사용됩니다. 각 엔티티는 특정 컴포넌트의 복사본을 하나만 가질 수 있습니다. 디버깅에 도움이 되도록 Add<T>AddResult 열거형을 리턴합니다.

C#

public enum AddResult {
    EntityDoesNotExist     = 0, // The EntityRef passed in is invalid.
    ComponentAlreadyExists = 1, // The Entity in question already has this component attached to it.
    ComponentAdded         = 2  // The component was successfully added to the entity.
}

엔티티에 컴포넌트가 있으면 Get<T>를 사용하여 검색할 수 있습니다. 컴포넌트 값의 사본이 리턴됩니다. 복사 작업을 수행 중이므로 Set<T>를 사용하여 수정된 값을 컴포넌트에 저장해야 합니다. Add 메소드와 마찬가지로 작업 결과를 확인하거나 응답하는 데 사용할 수 있는 SetResult 를 리턴합니다.

C#

public enum SetResult {
    EntityDoesNotExist = 0, // The EntityRef passed in is invalid.
    ComponentUpdated   = 1, // The component values were successfully updated.
    ComponentAdded     = 2  // The Entity did not have a component of this type yet, so it was added with the new values.
}

예를 들어 상태 컴포넌트의 시작 값을 설정하려면 다음을 수행합니다:

C#

private void SetHealth(Frame f, EntityRef entity, FP value){    
    var health = f.Get<Health>(entity);
    health.Value = value;
    f.Set(entity, health);
}

다음 표는 이미 제시된 방법과 컴포넌트 및 컴포넌트 값을 조작하기 위해 제공된 다른 메소드를 요약한 것입니다.

메소드 리턴 추가 정보
Add<T>(EntityRef entityRef) AddResult enum, 위 참조. 무효한 엔티티 참조 허용.
Get<T>(EntityRef entityRef) T
현재 값을 가진 T의 복사본.
잘못된 컴포넌트 참조를 허용하지 않습니다.
컴포넌트 T가 엔티티에 없는 경우 예외를 발생시킵니다.
Set<T>(EntityRef entityRef) SetResult enum, 위 참조. 잘못된 엔티티 참조 허용.
Has<T>(EntityRef entityRef) bool
true = 엔티티가 존재하며 컴포넌트가 연결됨
false = 엔티티가 존재하지 않으며 컴포넌트가 연결되지 않음.
잘못된 엔티티 참조 허용,
그리고 존재하지 않는 컴포넌트 허용.
TryGet<T>(EntityRef entityRef, out T value) bool
true = 엔티티가 존재하고 컴포넌트가 연결됨.
false = 엔티티가 존재하지 않거나 컴포넌트가 연결되지 않음.
잘못된 엔티티 참조 허용.
TryGetComponentSet(EntityRef entityRef,
out ComponentSet componentSet)
bool
true = 엔티티가 존재하고 모든 컴포넌트의 컴포넌트들이 연결됨
false = 엔티티가 존재하지 않고, 또는 집합의 하나 이상의 컴포넌트가
연결되지 않음.
잘못된 엔티티 참조 허용.
Remove<T>(EntityRef entityRef) 리턴값 없음.
엔티티가 존재하고 컴포넌트를 가지고 경우 컴포넌트를 제거합니다.
이외는 아무일도 하지 않습니다.
잘못된 엔티티 참조 허용.

컴포넌트에 대한 작업을 직접 용이하게 하고 Get/Set, Frame.Unsafe을 사용할 경우 -작은- 오버헤드 발생하지 않도록 하기 위해 Get 및 TryGet의 안전하지 않은 버전을 제공합니다(아래 표 참조).

메소드 리턴 추가 정보
GetPointer<T>(EntityRef entityRef) T* 잘못된 엔티티 참조 허용하지 않음.
컴포넌트 T가 엔티티에 없는 경우 예외를 발생시킵니다.
TryGetPointer<T>(EntityRef entityRef
out T* value)
bool
true = 엔티티가 존재하고 컴포넌트들이 엔티티에 연결됨
false = 엔티티가 존재하지 않거나, 컴포넌트들이 엔티티에 연결되지 않음
잘못된 엔티티 참조 허용.

싱글톤 컴포넌트

싱글톤 컴포넌트는 특정 시간에 하나만 존재할 수 있는 특수 유형의 컴포넌트입니다. 전체 게임 상태의 임의 엔티티에는 특정 T 싱글톤 컴포넌트의 인스턴스가 하나만 있을 수 있습니다. 이 인스턴스는 ECS 데이터 버퍼의 핵심에 깊이 적용됩니다. 이것은 Quantum에 의해 엄격하게 시행됩니다.

사용자 정의 싱글톤 컴포넌트singleton component를 사용하여 DSL에서 정의할 수 있습니다.

C#

singleton component MySingleton{
    FP Foo;
}

싱글톤은 IComponentSingleton이라는 인터페이스를 상속받으며, 그 자체가 IComponent에서 상속받습니다. 따라서 일반 컴포넌트에서 기대할 수 있는 모든 일반적인 작업을 수행할 수 있습니다.

  • 모든 엔티티에 부착할 수 있습니다.
  • 모든 일반 안전 및 안전하지 않은 방법(예: Get, Set, TryGetPointer 등)을 사용하여 관리할 수 있습니다.
  • 유니티 편집기를 통해 엔티티 프로토타입에 넣거나 엔티티의 코드에서 인스턴스화할 수 있습니다.

일반적인 컴포넌트 관련 방법 외에도 싱글톤 전용의 몇 가지 특별한 방법이 있습니다. 메소드는 일반 컴포넌트와 마찬가지로 값 유형을 반환하는지 포인터를 반환하는지 여부에 따라 SafeUnsafe 로 구분됩니다.

메소드 리턴 추가정보
API - Frame
SetSingleton<T> (T component,
EntityRef optionalAddTarget = default)
void 싱글톤이 존재하지 않는 경우 싱글톤을 설정.
-------
EntityRef (선택), 어떤 엔티티를 추가할 지 지정합니다.
아무것도 주어지지 않았으면, 새로운 엔티티가 생성되고 싱글톤에 추가됩니다.
GetSingleton<T>() T 싱글톤이 존재하지 않으면 예외를 발생시킵니다.
엔티티 참조는 필요하지 않으며, 자동적으로 찾게됩니다
TryGetSingleton<T>(out T component) bool
true = 싱글톤이 존재
false = 싱글톤이 존재하지 않음
싱글톤이 존재하지 않으면 예외를 발생시키지 않습니다.
엔티티 참조가 필요하지 않으며, 자동으로 찾습니다.
GetOrAddSingleton<T>(EntityRef optionalAddTarget = default) T 싱글톤을 얻어 리턴합니다.
싱글톤이 없는 경우 SetSingleton에서와 같이 생성됩니다.
-----
EntityRef (선택), 작성해야 하는 경우 엔티티를 추가할 엔티티를 지정합니다.
엔티티 Ref가 전달되지 않은 경우 싱글톤을 추가할 새 엔티티가 작성됩니다.
GetSingletonEntityRef<T>() EntityRef 현재 싱글톤을 보유하고 있는 엔티티를 반환합니다.
싱글톤이 존재하지 않으면 예외를 발생시킵니다.
TryGetSingletonEntityRef<T>(out EntityRef entityRef) bool
true = 싱글톤이 존재.
false = 싱글톤이 미 존재
현재 싱글톤을 보유하고 있는 엔티티를 가져옵니다. 싱글이 없는 경우 예외를 던지지 않습니다.
API - Frame.Unsafe
Unsafe.GetSingletonPointer<T>() T* 싱글톤 포인터를 얻습니다.
존재하지 않으면 예외를 발생시킵니다
TryGetPointerSingleton<T>(out T* component) bool
true = 싱글톤이 존재.
false = 싱글톤이 미 존재.
싱글톤 포인터를 얻습니다.
GetOrAddSingletonPointer<T>(EntityRef optionalAddTarget = default) T* 싱글톤을 얻거나 추가하여 리턴합니다.
싱글톤이 존재하지 않으면, 생성합니ㄷ.
-----
EntityRef (선택), 작성해야 하는 경우 엔티티를 추가할 엔티티를 지정합니다.
엔티티 Ref가 전달되지 않은 경우 싱글톤을 추가할 새 엔티티가 작성됩니다.

추가 기능

컴포넌트는 특수 구조체이므로 C# 파일에 partial 구조체 정의를 작성하여 사용자 지정 메소드로 확장할 수 있습니다.
예를 들어 Action 컴포넌트를 다음과 같이 확장할 수 있습니다.

C#

namespace Quantum
{
    public partial struct Action
    {
        public void UpdateCooldown(FP deltaTime){
            Cooldown -= deltaTime;
        }
    }
}

대응 콜백

대응 콜백에 두 가지의 컴포넌트가 있습니다:

  • ISignalOnAdd<T>: 컴포넌트 유형 T가 엔티티에 추가되면 호출됩니다.
  • ISignalOnRemove<T>: 엔티티에서 엔티티 유형 T가 제거되면 호출됩니다.

이 기능은 사용자 정의 컴포넌트의 목록을 할당 및 할당하는 등 컴포넌트의 일부를 추가/제거할 때 특히 유용합니다.

이러한 신호를 수신하려면 시스템에서 해당 신호를 구현하기만 하면 됩니다.

컴포넌트 반복

단일 컴포넌트만 필요한 경우 ComponentIterator(안전) 및 ComponentBlockIterator(안전하지 않음)가 가장 적합합니다.

C#

foreach(var transform in frame.GetComponentIterator<Transform3D>()) {
  transform.Component.Position += FPVector3.Forward * frame.DeltaTime;
  
  // EntityRef
  var e = transform.Entity;
  
  // The safe version returns a copy of the component.
  // Remember to set the new values on the entity!
  frame.Set(e, transform);
}

컴포넌트 블록 반복기는 포인터를 통해 가장 빠르게 접근할 수 있도록 합니다. 아주 주의하여 처리.

C#

foreach(var t in frame.Unsafe.GetComponentBlockIterator<Transform3D>()) {
    t->Position += FPVector3.Forward * frame.DeltaTime;
}

필터

필터는 컴포넌트 세트를 기준으로 엔티티를 필터링할 수 있을 뿐만 아니라 시스템에 필요한 컴포넌트만 캡처할 수 있는 편리한 방법입니다. 필터는 안전(Get/Set) 코드와 안전하지 않은(pointer) 코드에 모두 사용할 수 있습니다.

지네릭

필터를 생성하기 위해서는 간단히 프레임에서 제공되는 Filter() API를 사용하면 됩니다.

C#

var filtered = frame.Filter<Transform3D, PhysicsBody3D>();

지네릴 필터는 최대 8개의 컴포넌트를 포함할 수 있습니다.
withoutany ComponentSet 필터를 생성하여 구체화해야 하는 경우입니다.

C#

var without = ComponentSet.Create<CharacterController3D>();
var any = ComponentSet.Create<NavMeshPathFinder, NavMeshSteeringAgent>();
var filtered = frame.Filter<Transform3D, PhysicsBody3D>(without, any);

_ComponentSet_은 최대 8개의 컴포넌트를 포함할 수 있습니다.
without 매개 변수로 전달된 ComponentSet 은 집합에 지정된 구성 요소 중 하나 이상을 포함하는 모든 엔티티를 제외합니다. any 집합은 엔티티에 하나 이상의 지정된 컴포넌트가 있는지 확인합니다. 엔티티에 지정된 구성 요소가 없으면 필터에서 제외됩니다.

필터를 통해 반복하는 것은 filter.Next()와 함께 while loop를 사용하는 것만큼 간단합니다. 이렇게 하면 컴포넌트의 모든 사본과 컴포넌트가 부착된 엔티티의 EntityRef가 채워집니다.

C#

while (filtered.Next(out var e, out var t, out var b)) {
  t.Position += FPVector3.Forward * frame.DeltaTime;
  frame.Set(e, t);
}

주의: 컴포넌트의 copies 를 반복하여 작업하고 있습니다. 따라서 새 데이터를 해당 엔티티에 다시 설정해야 합니다.

지네릭 필터는 컴포넌트 포인터로 작업할 수도 있습니다.

C#

while (filtered.UnsafeNext(out var e, out var t, out var b)) {
  t->Position += FPVector3.Forward * frame.DeltaTime;
}

이 경우 컴포넌트의 데이터를 직접 수정합니다.

FilterStruct

일반 필터 외에도 FilterStruct 접근 방식을 사용할 수 있습니다.
이를 위해 먼저 수신하려는 각 컴포넌트 유형에 대해 public 속성을 가진 구조체를 정의해야 합니다.

C#

struct PlayerFilter
{
    public EntityRef Entity;
    public CharacterController3D* KCC;
    public Health* Health;
    public FP AccumulatedDamage;
}

ComponentSet 과 같이, FilterStruct 는 최대 8개의 컴포넌트 포인터까지 필터링할 수 있습니다.

주의: FilterStruct 로 사용되는 구조체는 EntityRef 필드가 필수 입니다!

FilterStruct컴포넌트 타입 멤머들은 반드시 포인터이어야 합니다. 포인터인 것만 필터에 의해 채워질 것입니다. 컴포넌트 포인터뿐만 아니라 다른 변수도 정의할 수 있지만 이러한 변수는 필터에서 무시되고 사용자가 관리해야 합니다.

C#

var players = f.Unsafe.FilterStruct<PlayerFilter>();
var playerStruct = default(PlayerFilter);

while (players.Next(&playerStruct))
{
    // Do stuff
}

Frame.Unsafe.FilterStruct<T>() 필터를 추가로 지정하기 위해 선택적 ComponentSets anywithout 을 사용하는 오버로드 있습니다.

카운트에 대한 노트

필터는 얼마나 많은 엔티티가 터치되고 반복될지 미리 알 수 없습니다. 이는 Sparse-Set ECS에서 필터가 작동하는 방식 때문입니다.

  1. 필터는 제공된 컴포넌트 중에서 가장 적은 엔티티를 가진 컴포넌트를 찾습니다(교차를 확인하도록 설정된 컴포넌트).
  2. 세트를 통과하여 쿼리 된 다른 컴포넌트를 가지고 있지 않은 컴포넌트는 폐기됩니다.

정확한 숫자를 미리 알고 있으면 (O(n) 작업이기 때문에 필터를 한 번 통과해야 합니다.

컴포넌트 Getter

알려진 엔티티에서 특정 컴포넌트 집합을 가져오려면 필터 구조를 Frame.Unsafe.ComponentGetter와 함께 사용하십시오. 주의: 안전하지 않은 컨텍스트에서만 사용할 수 있습니다!

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    public override void Update(Frame f) {
        MyFilter result = default;
      
        if (f.Unsafe.ComponentGetter<MyFilter>().TryGet(f, f.Global->MyEntity, &result)) {
            // Do Stuff
        }
    }

이 작업을 자주 수행해야 하는 경우 아래와 같이 시스템에서 룩업 구조체를 캐시 할 수 있습니다(100% 안전).

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    ComponentGetter<MyFilter> _myFilterGetter;

    public override void OnInit(Frame f) {
      _myFilterGetter = f.Unsafe.ComponentGetter<MyFilter>();
    }

    public override void Update(Frame f) {
      MyFilter result = default;
      
      if (_myFilterGetter.TryGet(f, f.Global->MyEntity, &result)) {
        // Do Stuff
      }
    }

필터링 전략

종종 컴포넌트가 많지만 그중 일부만 원하는 상황에 처하게 됩니다. 앞에서 Quantum에서 필터링에 사용할 수 있는 컴포넌트와 도구를 소개했습니다. 이 섹션에서는 이러한 컴포넌트를 활용하는 몇 가지 전략을 제시하겠습니다.
주의: 최고 접근 방식은 게임 및 시스템에 따라 달라집니다. 우리는 독특한 상황에 맞는 전략을 만들기 위한 출발점으로 아래의 전략을 택할 것을 추천합니다.

참고: 아래에 사용된 모든 용어는 다른 단어 개념들을 캡슐화하기 위해 내부적으로 만들어졌습니다.

마이크로-컴포넌트

많은 엔터티가 동일한 컴포넌트 유형을 사용하고 있을 수 있지만, 동일한 컴포넌트 구성을 사용하는 엔터티는 거의 없습니다. 이들의 구성을 더욱 전문화하는 한 가지 방법은 마이크로 컴포넌트 를 사용하는 것입니다. 마이크로 컴포넌트 는 특정 시스템이나 동작에 대한 데이터를 가진 고도로 전문화된 컴포넌트입니다. 고유성을 통해 필터를 생성하는 엔티티를 빠르게 식별할 수 있습니다.

플래그-컴포넌트

엔티티를 식별하는 일반적인 방법 중 하나는 플래그-컴포넌트 를 엔티티에 추가하는 것입니다. ECS에서 flags 의 개념은 se 단위로 존재하지 않으며 Quantum은 엔티티 타입을 지원하지 않습니다. 그러면 플래그-컴포넌트 는 정확히 무엇일까요? 데이터를 거의 또는 전혀 보유하지 않는 컴포넌트이며 엔티티를 식별하기 위한 독점적인 목적으로 만들어졌습니다.

예를 들어 팀 기반 게임에서 다음을 수행할 수 있습니다.

  1. TeamA 및 TeamB에 대한 열거가 있는 "Team" 컴포넌트입니다.
  2. "TeamA" 및 "TeamB" 컴포넌트로 구성됩니다.

옵션 1은 View에서 데이터를 폴링 하는 것이 주된 목적인 경우 유용하며, 옵션 2는 관련 시뮬레이션 시스템의 필터링 성능을 활용할 수 있습니다.

참고: 태그 지정 엔티티와 플래그 지정 엔티티가 서로 바꿔 사용되기 때문에 플래그 컴포넌트를 태그 컴포넌트라고도 합니다.

카운트

시뮬레이션에 현재 존재하는 컴포넌트 T의 양은 Frame.ComponentCount<T>()를 사용하여 검색할 수 있습니다. 플래그 컴포넌트와 함께 사용하면 특정 유형의 단위를 빠르게 계산할 수 있습니다.

추가 / 제거

플래그 컴포넌트 또는 마이크로 컴포넌트만 엔티티에 임시로 부착해야 하는 경우 추가제거 작업이 모두 O(1)이므로 적절한 옵션으로 남아 있습니다.

글로벌 목록

플래그 컴포넌트에 대한 대안으로 "덜" ECS와 같은 것이기는 하지만 FrameContext.User.cs에 전역 목록을 유지하는 것이 있습니다. N개 팀을 추적해야 하는 경우 이 방법이 반드시 확장되지는 않지만 부분 집합이 제한된 세트에서는 편리합니다.

상태가 50% 미만인 모든 플레이어를 강조 표시하려면 글로벌 목록을 보류하고 다음을 수행할 수 있습니다.

  • 시뮬레이션을 시작할 때 entity_ref를 목록에 추가/제거하는 시스템이 있어야 합니다.
  • 이후의 모든 시스템에서 동일한 목록을 사용합니다.

주의: 이러한 유형의 조건만 산발적으로 식별하면 되는 경우 글로벌 목록을 유지하는 것보다 필요할 때 동적으로 계산하는 것이 좋습니다.

Back to top