HelloCube로 엔티티 알아보기

Tutorial

intermediate

+10XP

30 mins

11

Unity Technologies

HelloCube로 엔티티 알아보기

이 HelloCube 튜토리얼은 DOTS(데이터 지향 기술 스택)에 대해 초보자도 쉽게 이해할 수 있도록 도와주는 입문서입니다. 이 튜토리얼에서는 ECS(엔티티 컴포넌트 시스템) 아키텍처를 구현하는 DOTS의 핵심 부분인 Entities 패키지의 기본에 대해 알아봅니다.

이 튜토리얼에서 배울 내용은 다음과 같습니다.

  • 게임 오브젝트에서 엔티티를 생성하기 위해 하위 씬을 베이크합니다.
  • 코드의 프리팹에서 엔티티를 생성합니다.
  • Entities API를 사용하는 코드를 작성해 엔티티를 조작합니다.

Resources

1. 개요

이 튜토리얼에서는 Entities 패키지 사용의 기초를 소개하고 시스템에서 엔티티를 생성하고 조작하는 방법을 다룹니다.

큐브 엔티티를 생성한 다음 시스템 코드를 사용하여 회전시켜 보겠습니다.

이 튜토리얼은 Entities 샘플 저장소에 있는 HelloCube 샘플을 간단히 소개한 버전입니다.

2. 시작하기 전에

이 튜토리얼을 완료하려면 C#과 Unity 게임 오브젝트를 잘 이해하고 있어야 합니다.

엔티티와 DOTS(데이터 지향 기술 스택)의 나머지 구성 요소를 처음 접하는 경우 다음 링크에서 몇 가지 배경 정보를 얻을 수 있습니다.

Unity 프로젝트 설정

이 튜토리얼을 위한 Unity 프로젝트를 설정하는 방법은 다음과 같습니다.

1. 3D (URP) 템플릿을 사용하여 새 Unity 프로젝트를 생성합니다.

2. 패키지 관리자를 통해 Entities Graphics 패키지를 설치합니다.

엔티티를 렌더링하려면 Entities Graphics 패키지가 필요하며 이 패키지를 설치하면 종속 관계인 Entities 패키지도 자동으로 설치됩니다.

3. 베이킹을 위한 하위 씬 생성

엔티티는 코드로 생성되거나 씬에서 로드될 수 있습니다. 이 첫 번째 단계에서는 씬에서 엔티티를 로드합니다.

엔티티는 Unity 씬에 직접 추가할 수 없지만 Unity 씬이 다른 씬 내부에 하위 씬으로 중첩된 경우 베이킹이라고 하는 프로세스를 통해 하위 씬의 각 게임 오브젝트에 해당하는 엔티티가 생성됩니다. 런타임 시 하위 씬이 로드되면 게임 오브젝트가 아닌 베이크된 엔티티만 로드됩니다.

아래 동영상에서 베이킹에 대한 개요를 확인하세요.

하위 씬을 생성하는 방법은 다음과 같습니다.

1. 계층(Hierarchy) 창에서 오른쪽 클릭(macOS: Ctrl+클릭)한 다음 New Sub Scene > Empty Scene을 선택합니다. 기본 이름인 ‘New Sub Scene’을 사용합니다.

새로운 New Sub Scene 게임 오브젝트가 Sub Scene 컴포넌트가 있는 계층 창에 나타납니다. 이 Sub Scene 컴포넌트는 New Sub Scene 씬 에셋을 참조합니다.

2. 계층 창에서 New Sub Scene 게임 오브젝트의 체크박스를 선택하여 편집할 새 하위 씬을 엽니다.

4. 하위 씬에 큐브 추가

계층 창에서 New Sub Scene 게임 오브젝트에 체크박스가 있습니다. 체크박스가 활성화되면 하위 씬이 열려서 편집할 수 있으며 해당 하위 씬의 콘텐츠가 New Sub Scene 게임 오브젝트 아래에 나타납니다. 체크박스가 비활성화되면 하위 씬이 닫히고 하위 씬의 콘텐츠가 계층 창에 나타나지 않습니다.

하위 씬은 비어 있는 상태로 시작하지만 New Sub Scene 게임 오브젝트를 오른쪽 클릭한 다음 3D Object > Cube를 선택하여 큐브를 추가해 보겠습니다. 이렇게 하면 Transform, Mesh Filter, Mesh Renderer, Box Collider 컴포넌트가 포함된 Cube 게임 오브젝트가 추가됩니다.

하위 씬이 열리면 새 큐브가 New Sub Scene의 자식 게임 오브젝트로 계층 창에 나타납니다.

참고: 새 큐브는 실제로 New Sub Scene 게임 오브젝트의 자식이 아닌 하위 씬의 멤버입니다.

게임 오브젝트를 추가 또는 제거하거나 해당 컴포넌트를 수정하여 하위 씬의 콘텐츠를 편집할 때마다 다시 베이크하도록 트리거됩니다. 베이킹은 다음 작업을 아래의 순서대로 수행합니다.

  1. 베이킹은 하위 씬의 각 게임 오브젝트에 엔티티를 생성합니다.
  2. 연관된 Baker 클래스가 있는 각 게임 오브젝트 컴포넌트에 Baker의 Bake 메서드가 호출됩니다. 이러한 Bake 메서드는 엔티티에 컴포넌트를 추가하고 설정할 수 있습니다.
  3. 엔티티는 파일로 베이크됩니다. 런타임 시 하위 씬이 로드될 때 이렇게 직렬화된 엔티티가 로드됩니다.

하위 씬에서 Cube 게임 오브젝트를 선택합니다. 인스펙터(Inspector) 창 하단에 있는 Entity Baking Preview 섹션에는 이 게임 오브젝트에서 생성된 엔티티에 베이킹이 추가한 모든 Entity 컴포넌트가 나열됩니다.

Entities Graphics 패키지는 표준 렌더링 컴포넌트용 Baker 클래스를 제공하기 때문에 큐브 엔티티에 Unity.Rendering.RenderMeshArray, Unity.Rendering.WorldRenderBounds 같은 다양한 엔티티 렌더링 컴포넌트가 제공되는 것을 볼 수 있습니다.

Unity.Physics 패키지는 Box Collider 컴포넌트 같은 표준 물리 컴포넌트용 Baker 클래스가 포함되어 있습니다. 이 프로젝트에서는 충돌을 고려하지 않기 때문에 Unity.Physics 패키지를 포함하지 않았습니다. 따라서 Box Collider 컴포넌트는 베이킹 시 무시되며 제거해도 문제가 발생하지 않습니다.

이제 플레이 모드에 들어가면 이 하위 씬에서 로드되는 하나의 큐브 엔티티가 보입니다. 큐브를 여러 번 복제하고 새 큐브가 서로 겹치지 않도록 위치를 변경한 다음 모든 큐브가 명확하게 보이도록 카메라 위치를 변경합니다.

5. 큐브 엔티티에 컴포넌트 추가

큐브가 특정 속도로 회전하도록 지정하기 위해 큐브에 추가할 새 컴포넌트를 정의하겠습니다. 먼저 RotationSpeedAuthoring.cs라는 새 파일을 생성하고 다음 코드를 추가합니다.

using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;

public struct RotationSpeed : IComponentData {     
    public float RadiansPerSecond;  // 엔티티의 회전 속도 
}

베이킹 시 이 컴포넌트를 엔티티에 추가하려면 새로운 저작 MonoBehaviour와 Baker 클래스를 정의합니다. 동일한 파일에 다음 클래스를 추가합니다.

public class RotationSpeedAuthoring : MonoBehaviour
{
    public float DegreesPerSecond = 360.0f;
}

class RotationSpeedBaker : Baker<RotationSpeedAuthoring>
{
    public override void Bake(RotationSpeedAuthoring authoring)
    {
        var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);

        var rotationSpeed = new RotationSpeed
        {
            RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
        };

        AddComponent(entity, rotationSpeed);
    }
}

참고: Baker 클래스의 이름을 무엇으로 정할지는 별로 중요하지 않습니다. 중요한 부분은 Baker<RotationSpeedAuthoring>을 확장하는 클래스가 있다는 것입니다.

Baker의 Bake 메서드는 GetEntity를 호출함으로써 베이크되는 엔티티를 가져오고 열거형 값 TransformUsageFlags.Dynamic을 전달하여 해당 엔티티에 LocalTransform을 포함한 표준 Transform 컴포넌트가 필요하다는 것을 지정합니다. 그런 다음 RotationSpeed 값을 생성하고 이를 엔티티에 새 컴포넌트로 추가합니다.

참고: RotationSpeedAuthoring MonoBehaviour는 회전 속도를 초당 각도로 지정하지만 RotationSpeed IComponentData는 회전 속도를 초당 라디안으로 지정하므로 Bake 메서드는 각도를 라디안으로 전환합니다. 이는 저작 데이터(하위 씬에서 지정하는 것)와 런타임 데이터(런타임에 로드될 베이크된 엔티티)가 정확히 일대일로 대응할 필요가 없는 매우 간단한 사례를 보여 줍니다.

이제 RotationSpeedAuthoring을 정의했으니 이를 하위 씬에 있는 큐브 게임 오브젝트의 일부(전부는 아님)에 추가하고 각 큐브의 DegreesPerSecond를 180~360 사이의 값으로 설정합니다.

다음 단계에서는 큐브를 실제로 회전시키는 코드를 작성하겠습니다.

6. 큐브 회전시키기

큐브를 회전시키려면 CubeRotationSystem.cs라는 새 스크립트 파일을 생성하고 다음 코드를 추가합니다.

using Unity.Entities;
using Unity.Transforms;
using Unity.Burst;

public partial struct CubeRotationSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
	// 나중에 여기에 큐브를 회전시키는 코드를 추가할 것입니다.
    }
}

MonoBehaviour와 달리 시스템은 명시적으로 씬에 추가되지 않습니다. 대신 기본적으로 각 시스템은 플레이 모드에 들어갈 때 자동으로 인스턴스화되므로 CubeRotationSystemOnUpdate는 프레임마다 한 번씩 호출됩니다. BurstCompile 속성은 OnUpdate가 버스트 컴파일되도록 표시합니다. 이는 OnUpdate가 관리되는 오브젝트에 액세스하지 않는 한 유효합니다(이 예제의 경우에 해당).

참고: 시스템 외부의 MonoBehaviour 또는 기타 코드에서 엔티티를 읽고 쓰는 것은 가능하지만 시스템만 잡 안전성 검사를 인식하기 때문에 일반적으로 권장되지 않습니다.

큐브를 회전하려면 OnUpdate에서 다음 세 가지 작업을 아래 순서대로 수행해야 합니다.

1. LocalTransformRotationSpeed 컴포넌트를 가진 모든 엔티티를 쿼리합니다.

2. 쿼리와 일치하는 모든 엔티티를 반복합니다.

3. 각 엔티티의 LocalTransform 컴포넌트를 수정하여 y축을 중심으로 일정한 속도로 회전시킵니다.

API에서 이를 표현하는 방법이 몇 가지 있지만 가장 간결하고 편리한 옵션을 사용해 보겠습니다. 시스템의 OnUpdate 안에 다음과 같은 코드를 추가합니다.

// CubeRotationSystem의 OnUpdate 내부 
var deltaTime = SystemAPI.Time.DeltaTime;

// 이 foreach는 LocalTransform과 RotationSpeed 컴포넌트를 가진 모든 엔티티를 반복합니다. 
// LocalTransform은 수정해야 하므로 RefRW(읽기/쓰기)로 래핑됩니다. 
// RotationSpeed는 읽기만 하면 되므로 RefRO(읽기 전용)로 래핑됩니다. 
foreach (var (transform, rotationSpeed) in
        SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
{
    // Y축을 중심으로 트랜스폼을 회전합니다. 
    var radians = rotationSpeed.ValueRO.RadiansPerSecond * deltaTime;
    transform.ValueRW = transform.ValueRW.RotateY(radians);
}

참고: SystemAPI.Query는 소스 생성에 의해 처리되는 특수한 메서드이며 foreach 루프의 in 절에서만 호출할 수 있습니다. 생성된 코드에서 쿼리가 생성되고 실행됩니다. SystemAPI.Query 호출에 2개 이상의 컴포넌트 유형이 포함된 경우 튜플을 반환합니다. 따라서 튜플을 분해하기 위해 transform과 speed 변수를 괄호로 묶습니다(표준 C# 기능임).

SystemAPI.Query 호출은 LocalTransformRotationSpeed 컴포넌트를 가진 모든 엔티티에 쿼리를 수행합니다. foreach는 쿼리와 일치하는 각 엔티티를 반복하며 transform 변수에 읽기/쓰기 레퍼런스를 할당하고 rotationSpeed 변수에 읽기 전용 레퍼런스를 할당합니다. 루프 본문에서는 ValueRW(읽기/쓰기) 프로퍼티를 통해 LocalTransform을 읽고 쓸 수 있으며 ValueRO(읽기 전용) 프로퍼티를 통해 RotationSpeed를 읽을 수 있습니다. LocalTransform 메서드 RotateY는 라디안 값을 사용하고 컴포넌트에 다시 할당하는 회전된 새 트랜스폼을 반환합니다.

이제 플레이 모드에 들어가면 RotationSpeed 컴포넌트를 가지며 RotationSpeedAuthoring에서 설정된 속도로 회전하는 큐브가 표시됩니다.

7. 프리팹에서 엔티티 생성

시스템에서 런타임 시 큐브를 생성하려면 큐브를 프리팹으로 베이크해야 합니다. 이를 처리하는 방법은 다음과 같습니다.

1. 회전하는 큐브 게임 오브젝트 중 하나를 하위 씬에서 프로젝트(Project) 창으로 드래그하여 프리팹 에셋을 생성한 다음 하위 씬에서 큐브를 삭제합니다.

2. 새 저작 컴포넌트인 SpawnerAuthoring.cs를 정의하고 다음 코드를 추가합니다.

using Unity.Entities;
using UnityEngine;

public class SpawnerAuthoring : MonoBehaviour
{
    public GameObject CubePrefab;

    class Baker : Baker<SpawnerAuthoring>
    {
        public override void Bake(SpawnerAuthoring authoring)
        {
            // 프리팹을 베이크하는 코드를 여기에 입력합니다.	
        }
    }
}

struct Spawner : IComponentData
{
    public Entity CubePrefab;
}

참고: Baker 클래스는 저작 MonoBehaviour 내에 중첩되어 있습니다. 이렇게 하는 것이 필수는 아니지만 가장 깔끔한 구조의 스타일일 것입니다.

3. 하위 씬에 새 게임 오브젝트를 생성하고 여기에 SpawnerAuthoring 컴포넌트를 추가한 다음 CubePrefab 필드가 큐브 프리팹을 참조하도록 설정합니다.

4. SpawnerAuthoring bake 메서드에 다음과 같은 코드를 추가합니다.

var entity = GetEntity(authoring, TransformUsageFlags.None);
var spawner = new Spawner
{
        Prefab = GetEntity(authoring.CubePrefab, TransformUsageFlags.Dynamic)
};
AddComponent(entity, spawner);

생성자(spawner) 엔티티는 보이지 않기 때문에 Transform 컴포넌트도 필요하지 않습니다. 따라서 첫 번째 GetEntity 호출에서 TransformUsageFlags.None이 지정됩니다.

두 번째 GetEntity 호출은 프리팹의 베이크된 엔티티를 반환합니다. 프리팹은 렌더링된 큐브를 나타내기 때문에 표준 Transform 컴포넌트가 필요합니다. 따라서 호출에서 TransformUsageFlags.Dynamic이 지정됩니다.

이제 큐브 프리팹의 베이크된 엔티티 형식을 참조하는 Spawn 컴포넌트를 가진 엔티티가 베이크된 하위 씬에 표시됩니다.

5. 새 파일을 생성하고 이름을 SpawnSystem.cs라고 지정한 뒤 다음 코드를 추가합니다.


using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

public partial struct SpawnSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<Spawner>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        state.Enabled = false;

        // 곧 여기에 프리팹을 생성하는 코드를 추가할 것입니다.
    }
}

SpawnSystem은 한 번만 업데이트하려는 시스템이므로 OnUpdate에서 SystemStateEnabled 프로퍼티가 false로 설정되어 시스템의 후속 업데이트를 방지합니다.

일반적으로 시스템은 초기 씬이 로드되기 전에 인스턴스화되고 업데이트를 시작하지만, 여기서는 Spawner 컴포넌트를 가진 엔티티가 하위 씬에서 로드된 후에 시스템이 업데이트되도록 만들려고 합니다. OnCreate에서 SystemState 메서드 RequireForUpdate<Spawner>()를 호출하면 Spawner 컴포넌트를 가진 엔티티가 하나 이상 존재하지 않는 한 프레임에서 시스템이 업데이트되지 않습니다.

6. 큐브 프리팹의 인스턴스를 생성하려면 OnUpdate에 다음 코드를 추가합니다.

var prefab = SystemAPI.GetSingleton<Spawner>().CubePrefab;
var instances = state.EntityManager.Instantiate(prefab, 10, Allocator.Temp);

Spawner 컴포넌트는 단 하나의 엔티티에만 있어야 하므로 SystemAPI.GetSingleton<Spawner>()를 호출하여 해당 컴포넌트에 편리하게 액세스할 수 있습니다.

참고: GetSingleton<T>는 컴포넌트 유형을 가진 엔티티가 없거나 2개 이상인 경우 예외 오류를 발생시킵니다.

Instantiate 호출은 프리팹 엔티티의 새 인스턴스 10개를 생성하고 새 엔티티 ID의 NativeArray를 반환합니다. 이 경우 배열은 OnUpdate 호출 기간 동안에만 필요하므로 Allocator.Temp가 사용됩니다.

7. 새 큐브 인스턴스가 서로 겹치지 않게 그려지도록 하려면 OnUpdate에 다음 코드를 추가합니다.

// 새 큐브의 위치를 무작위로 설정합니다.
// (고정 시드 123을 사용하지만 각 실행마다 다른 무작위성을 원할 경우 
// 경과 시간 값을 시드로 사용할 수 있습니다.)
var random = new Random(123);
foreach (var entity in instances)
{
    var transform = SystemAPI.GetComponentRW<LocalTransform>(entity);
    transform.ValueRW.Position = random.NextFloat3(new float3(10, 10, 10));
}

여기에 Unity.Mathematics 패키지의 난수 생성기가 임의로 선택된 고정 시드 123과 함께 사용됩니다. 루프에서 SystemAPI.GetComponentRW는 각 엔티티의 LocalTransform 컴포넌트에 대한 읽기/쓰기 레퍼런스를 반환하고 엔티티의 Transform은 이 레퍼런스를 통해 무작위 위치(각 축을 따라 0~10 사이의 범위)로 설정됩니다.

8. 이전 단계에서 씬에 추가된 큐브를 제거합니다.

이제 플레이 모드에 들어가면 10개의 큐브가 생성되어 원점 근처에 모여 있는 것을 볼 수 있습니다.

8. 추가로 시도해 볼 만한 작업

HelloCube 샘플의 여러 단계에 따라 코드와 베이킹을 통해 엔티티를 생성했으며 시스템의 엔티티에 액세스하고 이를 조작하기 위한 기본 API를 사용하기 시작했습니다. 이제 HelloCube 샘플에서 다음 주제를 추가로 살펴봐도 좋습니다.

  • 잡에서 엔티티에 액세스
  • 시스템에서 게임 오브젝트에 액세스하고 해당 상태를 엔티티와 동기화
  • 엔티티의 부모 지정
  • 사용 가능한 컴포넌트

HelloCube 샘플의 전체 목록을 확인하려면 GitHub의 Entity Component System 샘플 저장소에 있는HelloCube 섹션을 참조하세요.

9. 다음 단계

이 교육 과정의 다음 튜토리얼에서는 Unity의 C# 잡 시스템을 소개합니다. 이 시스템을 사용하면 멀티코어 프로세서를 완전히 활용하는 멀티 스레드 코드를 작성할 수 있습니다.

Complete this Tutorial