본 문서는 Unity Entities 101 공식 문서의 한국어 번역본입니다
엔티티와 컴포넌트
엔티티는 GameObject의 경량화된 비관리 대체재입니다. 엔티티는 여러 면에서 GameObject와 유사하며 비슷한 역할을 수행할 수 있지만, 다음과 같은 중요한 차이점이 있습니다:
- GameObject와 달리 엔티티는 관리 객체가 아니라 단순한 고유 식별 번호입니다.
- 엔티티의 컴포넌트는 일반적으로 구조체 값입니다.
- 엔티티의 컴포넌트에는
MonoBehaviour의 "이벤트 함수"(예:OnUpdate,OnStart)에 해당하는 기능이 없습니다. - 엔티티 컴포넌트 타입에 메서드를 포함할 수는 있지만, 일반적으로 권장되지 않습니다.
- 단일 엔티티는 주어진 타입에 대해 하나의 컴포넌트만 가질 수 있습니다. 즉, 단일 엔티티가 두 개의 Foo 타입 컴포넌트를 동시에 가질 수 없습니다.
- 엔티티에는 내장된 부모-자식 관계 개념이 없습니다. 대신, 표준
Parent컴포넌트가 다른 엔티티에 대한 참조를 포함하여 엔티티 변환 계층 구조를 구축할 수 있게 합니다.
기본 컴포넌트 타입은 IComponentData를 구현하는 구조체를 생성하여 정의합니다.
// 두 개의 필드를 가진 엔티티 컴포넌트 타입
public struct Health : IComponentData
{
public int HitPoints;
public float ArmourRating;
}
IComponentData 구조체는 비관리 타입이어야 하므로 관리 필드 타입을 포함할 수 없습니다. 허용되는 필드 타입은 다음과 같습니다:
- Blittable 타입
- bool
- char
BlobAssetReference<T>(Blob 데이터 구조체 참조)Collections.FixedString(고정 크기 문자 버퍼)Collections.FixedList- Fixed array (안전하지 않은 컨텍스트에서만 허용)
- 동일한 제한 사항을 따르는 다른 구조체 타입
엔티티 월드와 엔티티 매니저
World는 엔티티의 모음입니다. 엔티티의 ID 번호는 해당 월드 내에서만 고유합니다. 즉, 서로 다른 월드에 있는 동일한 ID의 엔티티는 서로 완전히 무관합니다.
월드는 또한 시스템을 보유하며, 이 시스템은 메인 스레드에서 실행되는 코드 단위로, 일반적으로 매 프레임마다 실행됩니다. 월드의 엔티티는 일반적으로 해당 월드의 시스템과 그 시스템이 예약한 작업(job)에서만 접근할 수 있습니다(필수 제한 사항은 아님).
월드의 엔티티는 월드의 EntityManager를 통해 생성, 파괴 및 수정되며, 그 메서드는 다음과 같습니다:
CreateEntity(): 새 엔티티를 생성합니다.Instantiate(): 기존 엔티티의 모든 컴포넌트를 복사하여 새 엔티티를 생성합니다.DestroyEntity(): 기존 엔티티를 파괴합니다.AddComponent<T>(): 기존 엔티티에 T 타입의 컴포넌트를 추가합니다.RemoveComponent<T>(): 기존 엔티티에서 T 타입의 컴포넌트를 제거합니다.HasComponent<T>(): 엔티티가 현재 T 타입의 컴포넌트를 가지고 있으면 true를 반환합니다.GetComponent<T>(): 엔티티의 T 타입 컴포넌트 값을 가져옵니다.SetComponent<T>(): 엔티티의 T 타입 컴포넌트 값을 덮어씁니다.
원형(Archetype)
원형은 월드 내 컴포넌트 타입의 특정 고유 조합을 나타냅니다. 특정 컴포넌트 타입 집합을 가진 월드의 모든 엔티티는 동일한 원형에 저장됩니다. 예를 들어:
- 컴포넌트 타입 A, B, C를 모두 포함하는 모든 엔티티는 동일한 원형에 저장됩니다.
- 컴포넌트 타입 A와 B만 포함하고 C는 포함하지 않는 엔티티는 두 번째 원형에 저장됩니다.
- 컴포넌트 타입 B와 D를 모두 포함하는 모든 엔티티는 세 번째 원형에 저장됩니다.
실제로 엔티티에 컴포넌트를 추가하거나 제거하면 해당 엔티티가 속한 원형이 변경되므로, EntityManager가 엔티티를 새 원형으로 이동시켜야 합니다.
엔티티에 컴포넌트를 추가하거나 제거할 때 EntityManager는 엔티티를 해당 원형으로 이동시킵니다. 예를 들어, 어떤 엔티티가 원래 X, Y, Z 세 가지 컴포넌트 타입을 가지고 있을 때 Y 컴포넌트를 제거하면, EntityManager는 해당 엔티티를 X와 Z 컴포넌트만 포함하는 원형으로 이동시키면서 X와 Z의 값을 보존합니다. 현재 월드에 그러한 원형이 없으면 EntityManager가 자동으로 생성합니다.
참고: 원형 간에 많은 수의 엔티티를 자주 이동하면 무시할 수 없는 오버헤드가 누적될 수 있습니다.
원형은 엔티티를 생성하고 수정할 때 EntityManager에 의해 자동으로 생성되므로 명시적으로 생성할 필요가 없습니다.
특정 원형의 모든 엔티티가 제거되더라도 해당 원형은 속한 월드가 파괴될 때까지 계속 존재합니다.
청크(Chunk)
원형의 엔티티는 해당 원형에 속하는 16KiB 메모리 블록에 저장되며, 이 블록을 청크라고 합니다. 각 청크는 최대 128개의 엔티티를 저장합니다. (원형의 각 엔티티에 필요한 공간이 16KiB/128을 초과하는 경우 각 청크의 최대 엔티티 수는 그에 따라 줄어듭니다.)
각 타입의 엔티티 ID와 컴포넌트는 청크 내부의 별도 배열에 저장됩니다. 예를 들어, 컴포넌트 타입 A와 B를 포함하는 엔티티 원형에서 각 청크는 세 개의 배열을 저장합니다:
- 엔티티 ID를 위한 배열
- A 컴포넌트를 위한 배열
- B 컴포넌트를 위한 배열
청크의 첫 번째 엔티티의 ID와 컴포넌트는 이 배열들의 인덱스 0에 저장되고, 두 번째 엔티티는 인덱스 1, 세 번째 엔티티는 인덱스 2에 저장되는 식입니다.
청크의 배열은 항상 빈틈없이 유지됩니다:
- 새 엔티티가 청크에 추가되면 배열의 첫 번째 빈 인덱스 위치에 저장됩니다.
- 엔티티가 청크에서 제거되면(엔티티가 파괴되거나 다른 원형으로 이동되어) 청크의 마지막 엔티티가 빈 자리를 채우기 위해 이동됩니다.
청크의 생성과 파괴는 EntityManager가 처리합니다:
- 기존 청크가 모두 찬 원형에 엔티티가 추가될 때만 새 청크가 생성됩니다.
- 청크의 마지막 엔티티가 제거될 때만 청크가 파괴됩니다.
청크 내에서 엔티티를 추가, 제거 또는 이동하는 모든 EntityManager 작업을 구조 변경이라고 합니다. 이러한 변경은 메인 스레드에서만 수행할 수 있으며 작업(job) 내에서는 실행할 수 없습니다. (하지만 나중에 논의할 EntityCommandBuffer를 우회 방법으로 사용할 수 있습니다.)
쿼리
EntityQuery는 지정된 컴포넌트 타입 집합을 가진 모든 엔티티를 효율적으로 찾습니다. 예를 들어, 컴포넌트 타입 A와 B를 가진 모든 엔티티를 찾는 쿼리는 해당 원형이 다른 컴포넌트 타입을 포함하더라도 A와 B를 포함하는 모든 원형의 청크를 수집합니다. 이러한 쿼리는 컴포넌트 타입 A와 B를 가진 엔티티뿐만 아니라 예를 들어 컴포넌트 타입 A, B, C를 가진 엔티티도 일치시킵니다.
참고: 쿼리와 일치하는 원형은 다음에 새 원형이 월드에 추가될 때까지 캐시됩니다. 월드의 기존 원형 컬렉션은 프로그램 수명 주기 초기에 안정화되는 경향이 있으므로, 이 캐싱 메커니즘은 일반적으로 쿼리 오버헤드를 크게 줄이는 데 도움이 됩니다.
쿼리는 일치하는 원형에서 제외할 컴포넌트 타입을 지정할 수도 있습니다. 예를 들어, "컴포넌트 타입 A와 B를 가지고 있지만 컴포넌트 타입 C는 포함하지 않는 모든 엔티티를 찾는" 쿼리는 A와 B 컴포넌트 타입을 가진 엔티티는 일치시키지만 A, B, C 컴포넌트 타입을 모두 가진 엔티티는 일치시키지 않습니다.
엔티티 ID
엔티티 ID는 두 개의 정수 매개변수 index와 version을 포함하는 Entity 구조체로 표현됩니다.
ID로 엔티티를 찾기 위해 월드의 EntityManager는 엔티티 메타데이터 배열을 유지 관리합니다. 엔티티의 인덱스는 메타데이터 배열에서의 해당 슬롯을 나타내며, 이 슬롯은 엔티티가 있는 청크에 대한 포인터와 청크 내 인덱스를 저장합니다. 특정 인덱스에 엔티티가 없으면 해당 인덱스의 청크 포인터는 null입니다. 예를 들어, 현재 인덱스 1, 2, 5에 엔티티가 없으므로 해당 슬롯의 청크 포인터는 모두 null입니다:

엔티티 버전 번호는 엔티티가 파괴된 후에도 엔티티 인덱스를 재사용할 수 있게 합니다. 엔티티가 파괴되면 해당 인덱스에 저장된 버전 번호가 증가하므로, ID의 버전 번호가 인덱스에 저장된 버전 번호와 일치하지 않으면 해당 ID는 파괴되었거나 존재한 적이 없는 엔티티를 가리키는 것입니다.
태그 컴포넌트
필드가 없는 IComponentData 구조체를 태그 컴포넌트라고 합니다. 태그 컴포넌트는 데이터를 저장하지 않지만, 다른 컴포넌트 타입처럼 엔티티에 추가하고 제거할 수 있으며 쿼리에 매우 유용합니다. 예를 들어, 몬스터를 나타내는 모든 엔티티에 Monster 태그 컴포넌트가 있다면, Monster 컴포넌트 타입을 쿼리하여 모든 몬스터 엔티티를 일치시킬 수 있습니다.
// a tag component
public struct Monster : IComponentData
{
}
동적 버퍼 컴포넌트
DynamicBuffer는 크기 조정이 가능한 배열 컴포넌트 타입입니다. DynamicBuffer 컴포넌트 타입을 정의하려면 IBufferElementData 인터페이스를 구현하는 구조체를 생성해야 합니다.
// a dynamic buffer component type
public struct Waypoint : IBufferElementData
{
public float3 Value;
}
각 엔티티의 버퍼는 길이, 용량 및 포인터를 저장합니다:
Length는 버퍼의 요소 수입니다. 0부터 시작하며 버퍼에 값을 추가할 때 증가합니다.Capacity는 버퍼의 저장 공간 크기입니다. 처음에는 내부 버퍼 용량과 일치합니다(기본값은 128/sizeof(T)이지만IBufferElementData구조체의InternalBufferCapacity속성으로 지정할 수 있습니다). 용량을 설정하면 버퍼 크기가 조정됩니다.- 포인터는 버퍼 내용의 위치를 나타냅니다. 초기 상태에서는 null이며, 내용이 청크에 직접 저장되어 있음을 의미합니다. 설정된 용량이 내부 버퍼 용량을 초과하면 청크 외부에 더 큰 새 배열이 할당되고, 내용이 이 외부 배열로 복사되며 포인터가 이 새 배열을 가리킵니다. 버퍼 길이가 외부 배열의 용량을 초과하면 버퍼 내용이 청크 외부의 더 큰 다른 새 배열로 복사되고 이전 배열은 해제됩니다. 버퍼를 축소할 수도 있습니다.
EntityManager가 청크 자체를 파괴하면 내부 버퍼 용량과 외부 용량(존재하는 경우)이 해제됩니다.
참고: 동적 버퍼가 청크 외부에 저장되면 내부 용량이 실제로 낭비되며 버퍼 내용에 접근할 때 추가 포인터 추적이 필요합니다. 내부 용량 제한을 초과하지 않도록 보장하면 이러한 오버헤드를 피할 수 있습니다. 물론 많은 경우 이 제한 내에서 유지하려면 너무 큰 내부 용량이 필요할 수 있습니다. 다른 옵션은 내부 용량을 0으로 설정하는 것입니다. 이렇게 하면 비어 있지 않은 모든 버퍼가 항상 청크 외부에 저장됩니다. 이로 인해 버퍼에 접근할 때 항상 포인터를 추적해야 하지만 청크에서 사용되지 않는 공간 낭비를 방지할 수 있습니다.
EntityManager는 동적 버퍼를 위해 다음과 같은 주요 메서드를 제공합니다:
AddComponent<T>(): 엔티티에 T 타입의 컴포넌트를 추가합니다. 여기서 T는 동적 버퍼 컴포넌트 타입일 수 있습니다.AddBuffer<T>(): 엔티티에 T 타입의 동적 버퍼 컴포넌트를 추가합니다. 새 버퍼를DynamicBuffer<T>로 반환합니다.RemoveComponent<T>(): 엔티티에서 T 타입의 컴포넌트를 제거합니다. 여기서 T는 동적 버퍼 컴포넌트 타입일 수 있습니다.HasBuffer<T>(): 엔티티가 현재 T 타입의 동적 버퍼 컴포넌트를 가지고 있으면 true를 반환합니다.GetBuffer<T>(): 엔티티의 T 타입 동적 버퍼 컴포넌트를DynamicBuffer<T>로 반환합니다.
DynamicBuffer<T>는 단일 엔티티의 T 타입 동적 버퍼 컴포넌트를 나타냅니다. 주요 속성과 메서드는 다음과 같습니다:
Length: 버퍼의 길이를 가져오거나 설정합니다.Capacity: 버퍼의 용량을 가져오거나 설정합니다.Item[Int32]: 지정된 인덱스의 요소를 가져오거나 설정합니다.Add(): 요소를 버퍼 끝에 추가하며, 필요한 경우 크기를 조정합니다.Insert(): 지정된 인덱스에 요소를 삽입하며, 필요한 경우 크기를 조정합니다.RemoveAt(): 지정된 인덱스의 요소를 제거합니다.
참고: 구조 변경 작업을 수행하면 DynamicBuffer가 "무효화"되어, 이후에 해당 DynamicBuffer를 사용하면 예외가 발생합니다. 구조 변경 후 버퍼를 다시 사용하려면 다시 가져와야 합니다.
시스템
시스템은 엔티티 월드에 속하는 코드 단위로, 메인 스레드에서 실행됩니다(일반적으로 매 프레임 한 번). 일반적으로 시스템은 자신이 속한 월드의 엔티티에만 접근하지만, 이는 강제 사항이 아닙니다.
시스템은 ISystem 인터페이스를 구현하는 구조체로 정의되며, 세 가지 주요 메서드를 포함합니다:
OnUpdate(): 일반적으로 매 프레임 호출됩니다(시스템이 속한 시스템 그룹에 따라 다름).OnCreate():OnUpdate가 처음 호출되기 전과 시스템이 재개될 때 호출됩니다.OnDestroy(): 시스템이 파괴될 때 호출됩니다.
참고: 이러한 메서드에는 기본적으로 빈 구현이 있으므로 필요하지 않으면 시스템에서 생략할 수 있습니다. 예를 들어 OnCreate 메서드 본문을 비워두면 메서드 전체를 생략할 수 있습니다.
시스템의 Enabled 속성이 false로 설정되면 업데이트가 건너뛰어집니다.
시스템은 ISystemStartStop 인터페이스를 추가로 구현할 수 있으며, 이 인터페이스에는 다음 메서드가 포함됩니다:
OnStartRunning():OnUpdate가 처음 호출되기 전과 시스템이 다시 활성화된 후(시스템의Enabled속성이false에서true로 변경될 때) 호출됩니다.OnStopRunning():OnDestroy전과 시스템이 비활성화된 후(시스템의Enabled속성이true에서false로 변경될 때) 호출됩니다.
시스템 그룹과 시스템 업데이트 순서
월드의 시스템은 시스템 그룹으로 구성됩니다. 각 시스템 그룹에는 정렬된 하위 항목 목록(시스템 및 다른 시스템 그룹)이 있으며, 기본적으로 각 시스템 그룹은 정렬 순서에 따라 하위 항목의 OnUpdate를 호출합니다. 실제로 시스템 그룹은 시스템 업데이트 순서를 결정하는 계층 구조를 형성합니다.
- 시스템 그룹은
ComponentSystemGroup에서 상속받는 클래스로 정의됩니다. - 시스템 그룹이 업데이트되면 일반적으로 정렬 순서에 따라 하위 항목을 업데이트하지만, 그룹의 업데이트 메서드를 재정의하여 이 기본 동작을 변경할 수 있습니다. 예를 들어,
FixedStepSimulationGroup은 사용자 정의 업데이트 동작을 가지고 있어 매 프레임 0회 이상 하위 항목을 업데이트하여 고정 업데이트 간격을 근사화합니다. - 그룹에 하위 항목이 추가되거나 제거될 때마다 그룹의 하위 항목이 다시 정렬됩니다.
UpdateInGroup속성을 통해 하위 항목이 시스템 그룹에 추가됩니다. 이 속성을 사용하지 않으면 시스템과 시스템 그룹은 기본적으로SimulationSystemGroup에 추가됩니다.UpdateBefore및UpdateAfter속성을 사용하여 그룹 내 하위 항목 간의 상대적 정렬 순서를 결정할 수 있습니다. 예를 들어, FooSystem에[UpdateBefore(typeof(BarSystem))]속성이 있으면 FooSystem은 정렬 순서에서 BarSystem보다 앞에 배치됩니다. 하지만 FooSystem과 BarSystem이 동일한 시스템 그룹에 속하지 않으면 이 속성은 경고를 발생시키는 것 외에는 효과가 없습니다.- 그룹의 하위 항목 정렬 속성에 모순이 있는 경우(예: 하위 항목 A가 하위 항목 B보다 먼저 업데이트되도록 표시되었지만 하위 항목 B도 하위 항목 A보다 먼저 업데이트되도록 표시된 경우) 해당 그룹의 하위 항목을 정렬할 때 예외가 발생합니다.
// A system group.
// Because this group doesn't override OnUpdate, it follows the default
// behaviour: its children will be updated in their sorted order.
public class MonsterSystemGroup : ComponentSystemGroup
{
}
// a child system of the MonsterSystemGroup
[UpdateInGroup(typeof(MonsterSystemGroup))]
public struct VampireSystem : ISystem
{
...
}
월드와 시스템 생성
실행 모드로 전환될 때 기본 자동 부트스트랩 프로세스는 세 개의 시스템 그룹을 포함하는 기본 월드를 생성합니다:
InitializationSystemGroup: Unity 플레이어 루프의 초기화 단계 끝에서 업데이트됩니다.SimulationSystemGroup: Unity 플레이어 루프의Update단계 끝에서 업데이트됩니다. 일반적으로 게임 로직을 배치하는 곳입니다.PresentationSystemGroup: Unity 플레이어 루프의PreLateUpdate단계 끝에서 업데이트됩니다. 일반적으로 렌더링 코드를 배치하는 곳입니다.
자동 부트스트랩 프로세스는 각 시스템과 시스템 그룹의 인스턴스를 생성합니다(DisableAutoCreation 속성이 있는 시스템 제외). 이러한 인스턴스는 UpdateInGroup 속성으로 재정의되지 않는 한 SimulationSystemGroup에 추가됩니다. 예를 들어, 시스템에 [UpdateInGroup(typeof(InitializationSystemGroup))] 속성이 있으면 해당 시스템은 SimulationSystemGroup이 아닌 InitializationSystemGroup에 추가됩니다.
스크립트 정의를 통해 자동 부트스트랩 프로세스를 비활성화할 수 있습니다:
#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD: 기본 월드의 자동 부트스트랩을 비활성화합니다.#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_EDITOR_WORLD: 에디터 월드의 자동 부트스트랩을 비활성화합니다.#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP: 기본 월드와 에디터 월드의 자동 부트스트랩을 모두 비활성화합니다.
자동 부트스트랩이 비활성화되면 코드에서 다음을 처리해야 합니다:
- 월드를 생성합니다.
World.GetOrCreateSystem<T>()를 호출하여 시스템 및 시스템 그룹 인스턴스를 월드에 추가합니다.- 업데이트를 위해 최상위 시스템 그룹(예:
SimulationSystemGroup)을 Unity 플레이어 루프에 등록합니다.
또는 ICustomBootstrap 인터페이스를 구현하는 클래스를 생성하여 부트스트랩 로직을 사용자 정의할 수 있습니다.
월드와 시스템의 시간
각 월드에는 Time 속성이 있으며, 이 속성은 프레임 델타 시간과 경과 시간을 포함하는 TimeData 구조체를 반환합니다. 이 시간 값은 월드의 UpdateWorldTimeSystem에 의해 업데이트됩니다. 다음 World 메서드를 통해 시간 값을 조작할 수 있습니다:
SetTime: 시간 값을 설정합니다.PushTime: 시간 값을 임시로 변경합니다.PopTime: 가장 최근 푸시 이전의 시간 값으로 복원합니다.
일부 시스템 그룹(예: FixedStepSimulationSystemGroup)은 하위 항목을 업데이트하기 전에 시간 값을 푸시하고 업데이트를 완료한 후 값을 팝합니다. 실제로 이러한 시스템 그룹은 하위 항목에 "가상" 시간 값을 제공합니다.
SystemState
시스템의 OnUpdate(), OnCreate() 및 OnDestroy() 메서드는 SystemState 매개변수를 받습니다. SystemState는 시스템 인스턴스의 상태를 나타내며, 다음과 같은 중요한 메서드와 속성을 포함합니다:
- 월드: 이 시스템이 속한 월드.
- EntityManager: 이 시스템이 속한 월드의 엔티티 매니저.
- Dependency: 시스템 간에 작업 종속성을 전달하는 데 사용되는
JobHandle. GetEntityQuery(): 엔티티 쿼리를 반환합니다.GetComponentTypeHandle<T>():ComponentTypeHandle<T>를 반환합니다.GetComponentLookup<T>():ComponentLookup<T>를 반환합니다.
중요: EntityManager에서 직접 엔티티 쿼리, 컴포넌트 타입 핸들 및 컴포넌트 룩업을 얻을 수 있지만, 일반적으로 시스템은 SystemState에서만 이러한 항목을 가져와야 합니다. SystemState를 통해 가져오면 시스템이 접근하는 컴포넌트 타입이 추적되며, 이는 시스템의 Dependency 속성이 시스템 간 작업 종속성을 올바르게 전달하는 데 중요합니다(나중에 논의).
SystemAPI
SystemAPI 클래스에는 World, EntityManager 및 SystemState 클래스와 기능 범위가 상당 부분 겹치는 많은 정적 편의 메서드가 포함되어 있습니다.
SystemAPI 메서드는 소스 생성기에 의존하므로 시스템 내부와 IJobEntity에서만 사용할 수 있습니다(IJobChunk에서는 사용 불가). SystemAPI를 사용할 때의 장점은 이러한 메서드가 두 컨텍스트에서 동일한 결과를 생성하므로, SystemAPI를 사용하는 코드는 일반적으로 두 시나리오 간에 더 쉽게 복사하여 붙여넣을 수 있다는 것입니다.
참고: Entities 핵심 기능을 어디서 찾아야 할지 확실하지 않은 경우, 일반적인 지침은 먼저 SystemAPI를 확인하는 것입니다. SystemAPI가 필요한 기능을 제공하지 않으면 SystemState를 확인하고, 그래도 없으면 마지막으로 EntityManager와 World를 확인하세요.
SystemAPI는 또한 특별한 편의 Query() 메서드를 제공하며, 소스 생성기를 통해 쿼리와 일치하는 엔티티 및 컴포넌트를 반복하는 foreach 루프를 빠르게 생성할 수 있습니다(아래 예시 참조).
작업(Job)에서 엔티티 접근
C# 작업 시스템을 통해 엔티티 데이터 처리를 작업자 스레드에 오프로드할 수 있습니다. Entities 패키지는 엔티티에 접근하는 작업을 정의하기 위한 두 가지 작업 인터페이스를 제공합니다:
IJobChunk인터페이스:Execute()메서드는 쿼리와 일치하는 각 개별 청크에 대해 한 번 호출됩니다.IJobEntity인터페이스:Execute()메서드는 쿼리와 일치하는 각 엔티티에 대해 한 번 호출됩니다.
IJobEntity가 일반적으로 작성 및 사용이 더 편리하지만, IJobChunk는 더 정밀한 제어를 제공합니다. 대부분의 경우 두 작업은 동등한 작업을 수행할 때 동일한 성능을 보입니다.
참고: IJobEntity는 실제로 "진정한" 작업 타입이 아닙니다. 소스 생성이 IJobChunk 구현을 통해 IJobEntity 구조체를 확장하므로, 실제로 IJobEntity는 궁극적으로 IJobChunk로 예약됩니다.
IJobChunk 또는 IJobEntity의 작업을 여러 스레드에 분산하려면 Schedule() 대신 ScheduleParallel()을 호출하여 작업을 예약하세요. ScheduleParallel()을 사용하면 쿼리와 일치하는 청크가 여러 배치로 분할되어 작업자 스레드에 할당됩니다.
구조 변경은 작업 내에서 실행할 수 없으므로 메인 스레드에서만 수행해야 합니다. 하지만 작업은 EntityCommandBuffer(나중에 논의)를 통해 구조 변경 명령을 기록할 수 있으며, 이 명령은 나중에 메인 스레드에서 재생될 수 있습니다.
동기화 지점
메인 스레드의 일부 작업은 현재 예약된 작업의 일부 또는 전부를 완료하는 "동기화 지점"을 트리거합니다. 예를 들어, EntityManager.AddComponent<T>()를 호출하면 현재 예약된 모든 T 컴포넌트 접근 작업이 먼저 완료됩니다. 마찬가지로 EntityQuery 메서드 ToComponentDataArray<T>(), ToEntityArray() 및 ToArchetypeChunkArray()도 쿼리와 동일한 컴포넌트에 접근하는 현재 예약된 작업을 먼저 완료해야 합니다.
많은 경우 이러한 동기화 지점은 특정 타입의 기존 인스턴스, 특히 DynamicBuffer 및 ComponentLookup<T>를 "무효화"합니다. 인스턴스가 무효화되면 해당 메서드를 호출하면 보안 검사 예외가 throw됩니다. 무효화된 인스턴스를 계속 사용해야 하는 경우 새 인스턴스를 가져와 교체해야 합니다.
컴포넌트 안전 핸들
네이티브 컬렉션과 유사하게 각 컴포넌트 타입은 각 월드에 연결된 작업 안전 핸들을 가지고 있습니다. 즉, 동일한 월드에서 동일한 컴포넌트 타입에 접근하는 두 작업에 대해 안전 검사는 이러한 작업이 동시에 예약되는 것을 허용하지 않습니다. 예를 들어, 컴포넌트 타입 Foo에 접근하는 작업을 예약하려고 할 때 이미 예약된 작업도 컴포넌트 타입 Foo에 접근하면 안전 검사가 예외를 발생시킵니다. 이 예외를 방지하려면:
- 새 작업을 예약하기 전에 이미 예약된 작업을 완료해야 합니다.
- ...또는 새 작업이 이미 예약된 작업에 종속되어야 합니다.
참고: 두 작업이 동일한 컴포넌트 타입에 대해 모두 읽기 전용 접근 권한을 가지면 동시 예약이 안전합니다. 작업에서 절대 쓰지 않는 모든 컴포넌트 타입에 대해 ReadOnly 속성으로 컴포넌트 타입 핸들을 표시하여 안전 검사 시스템에 알려주십시오.
DynamicBuffer<T> 인스턴스 자체는 안전 핸들을 보유합니다:
- 동일한 버퍼 컴포넌트 타입에 접근하는 예약된 작업이 아직 완료되지 않은 경우
DynamicBuffer<T>의 내용에 접근할 수 없습니다. - 그러나 미완료 작업이 모두 버퍼 컴포넌트 타입에 대해 읽기 전용 접근 권한만 가지고 있으면 메인 스레드가 버퍼를 읽는 것이 허용됩니다.
SystemState.Dependency
시스템에서 작업을 예약할 때, 해당 작업이 다른 시스템에서 예약되었더라도 새 작업과 충돌할 수 있는 현재 예약된 작업에 종속되기를 원합니다. 이러한 종속성을 관리하기 위해 SystemState에는 Dependency라는 JobHandle 속성이 있습니다.
시스템이 업데이트되기 직전:
- 시스템의
Dependency속성이 완료됩니다. - ...그런 다음
Dependency에는 이 시스템과 동일한 컴포넌트 타입에 접근하는 다른 모든 시스템의Dependency작업 핸들의 조합이 할당됩니다. 예를 들어, Foo 및 Bar 컴포넌트 타입에 접근하는 시스템에서, Foo 또는 Bar에 접근하는 월드의 다른 모든 시스템의Dependency가 하나의 작업 핸들로 결합되어 이 시스템의Dependency에 할당됩니다.
각 시스템에서 다음 두 가지 규칙을 따라야 합니다:
- 시스템 업데이트에서 예약된 모든 작업은 업데이트 전에
Dependency속성에 할당된 작업 핸들에 (직접 또는 간접적으로) 종속되어야 합니다. - 시스템 업데이트가 반환되기 전에
Dependency속성에 해당 업데이트의 모든 예약된 작업을 포함하는 핸들이 할당되어야 합니다.
이 두 가지 규칙을 따르면 시스템 업데이트에서 예약된 각 작업은 동일한 컴포넌트 타입에 접근할 수 있는 다른 시스템의 예약된 모든 작업에 종속됩니다.
중요: 시스템은 사용하는 네이티브 컨테이너를 추적하지 않으므로 Dependency 속성은 컴포넌트 타입만 고려하고 네이티브 컨테이너는 포함하지 않습니다. 따라서 두 시스템이 동일한 네이티브 컨테이너를 사용하는 작업을 예약하는 경우, 해당 Dependency 작업 핸들이 다른 시스템의 Dependency 속성에 할당되는 작업 핸들에 반드시 포함되지는 않으므로 다른 시스템의 작업은 예상대로 상호 종속되지 않습니다. 이러한 경우 시스템 간에 작업 핸들을 수동으로 공유하여 종속성을 조정할 수 있지만, 일반적으로 더 나은 해결책은 네이티브 컨테이너를 컴포넌트에 저장하는 것입니다. 두 규칙을 따르고 두 시스템이 동일한 컴포넌트 타입을 통해 해당 컨테이너에 접근