DOTS 정리 (3) – HelloCube로 엔티티 알아보기

이전 글


HelloCube로 엔티티 알아보기

Entity는 Unity 씬에 직접 추가할 수 없지만 Unity 씬이 다른 씬 내부에 하위 씬으로 중첩된 경우

베이킹이라고 하는 프로세스를 통해 하위 씬의 각 게임 오브젝트에 해당하는 Entity가 생성됩니다.

런타임 시 하위 씬이 로드되면 게임 오브젝트가 아닌 베이크된 Entity만 로드됩니다.

Baking과 SubScene

    SubScene: MonoBehaviour 컴포넌트로서 Scene 에셋을 참조하며, 이는 베이킹할 수 있습니다.
    Baking: SubScene에 있는 GameObject들을 직렬화된 Entity로 변환하는 과정입니다.

    Scene은 엔티티를 직접 포함할 수 없지만 SubScene에서 Entity를 로드할 수 있습니다

    SubScene은 다른 씬 안에 중첩된 씬이고 베이킹은 각 SubScene을 처리하여 직렬화된 엔티티 집합을 생성하는 기능입니다.

    런타임 시 SubScene이 로드될 때는 게임 오브젝트가 아닌 SubScene의 직렬화된 Entity가 로드됩니다.

    다른 씬처럼 씬 에셋을 직접 열고 편집할 수 있지만

    게임 오브젝트의 체크박스를 클릭하고 SubScene을 열어서 루트 씬의 컨텍스트에서 편집할 수도 있습니다.

    SubScene을 열면 에디터 내에서 게임 오브젝트가 로드되고 베이킹 전환 프로세스가 트리거됩니다.

    체크박스를 선택 해제하면 SubScene이 닫히고 에디터에서 게임 오브젝트가 언로드되지만 베이킹으로 생성된 엔티티는 로드된 상태입니다.

    SubScene에서 게임 오브젝트를 추가, 제거, 수정할 때마다 SubScene이 다시 베이크됩니다.

    예를 들어 하위 씬에 Cube 게임 오브젝트를 추가하고 머티리얼을 설정하면 SubScene이 자동으로 다시 베이크되는데

    여기서 실제 렌더링되는 것은 게임 오브젝트 자체가 아니고 게임 오브젝트에서 베이크된 엔티티입니다.

    즉 SubScene의 베이크된 엔티티만 런타임 시 로드되고 SubScene의 게임 오브젝트는 로드되지 않습니다

    하지만 열려 있는 SubScene의 게임 오브젝트가 플레이 모드에서 수정되면

    SubScene이 점진적으로 다시 베이크되고 베이크된 엔티티가 실시간으로 업데이트됩니다.

    예를 들어 Cube 게임 오브젝트의 트랜스폼을 수정하면 엔티티의 Transform 컴포넌트가 즉시 업데이트됩니다.

    추가적으로 플레이 모드에서 SubScene의 게임 오브젝트를 변경하면 이 내용은 플레이 모드가 종료된 후에도 유지됩니다.

    베이킹 프로세스 작동 원리

    1. SubScene을 위해 베이킹 월드를 생성합니다.
    2. 각 GameObject에 대한 엔티티를 생성합니다.
    3. Baker가 있는 각 컴포넌트가 Baker의 bake 메서드에 의해 처리됩니다.
    4. 베이킹 월드의 시스템을 실행합니다.
    5. 베이킹 월드를 직렬화합니다.

    1. SubScene에서 생성된 엔티티를 저장하기 위해서 베이킹 월드가 생성됩니다.

      2. SubScene의 각 게임 오브젝트마다 SubScene의 베이킹 월드에 엔티티가 추가됩니다.

      3. SubScene의 각 게임 오브젝트 컴포넌트 중 연관된 Baker 클래스가 있는 경우 해당 클래스의 Bake 메서드가 호출됩니다

      이러한 Baking 메서드에서 주로 할 수 있는 작업은 해당 게임 오브젝트의 엔티티에 컴포넌트를 추가하는 것입니다

      예를 들어 엔티티의 Graphics 패키지는 Mesh Renderer와 Mesh Filter 컴포넌트에 대한 Baker를 제공하고

      이러한 Baker는 적절한 렌더링 컴포넌트를 엔티티에 추가합니다

      인스펙터 하단의 Entity Baking Preview에서 확인할 수 있다.

      4. 모든 Baker가 실행된 후에는 베이킹 월드 시스템이 실행됩니다

      베이킹 시스템은 Baker 클래스와 달리 버스트 컴파일될 수 있고 베이크된 엔티티를 대량으로 처리합니다

      베이킹 월드 시스템은 엔티티를 자유롭게 조작할 수 있습니다

      즉, 엔티티를 생성, 삭제하거나 컴포넌트의 설정, 추가, 제거도 가능하다는 것입니다.

      하지만 베이킹 시스템에서 생성된 엔티티는 직렬화에 포함되지 않습니다

      베이킹 시스템은 일반적으로 단순한 목표에는 불필요하지만 베이킹 결과를 더 세부적으로 제어하려거나 베이크하려는 대상에 계산이 많이 필요한 경우 유용합니다

      베이킹 시스템의 실행이 완료된 이후에 베이킹의 마지막 단계는 베이킹 월드를 직렬화하는 것입니다.

      직렬화의 결과가 런타임 시 씬이 로드될 때 로드되는 것입니다.

      이제 Baker 클래스를 생성하는 과정을 살펴보면 SubScene의 베이크된 엔티티에 추가하려는 IComponentData 구조체 즉 Movement라는 구조체가 있다고 가정합니다.

      이걸 수행하려면 통상적으로 MovementAuthoring이라는 Monobehaviour가 필요한데 이 MovementAuthoring을 위해 Baker 클래스를 정의합니다.

      MovementAuthoring 컴포넌트 내부에 Baker 클래스를 중첩하는 것이 필수는 아니지만 일반적으로 Baker를 배치하기에 적절한 위치입니다

      Baker의 Bake 메서드에서는 먼저 베이크되는 엔티티를 가져온 다음 해당 엔티티에 원하는 컴포넌트를 추가하는데

      이 경우에는 Movement 컴포넌트만 추가합니다.

      이렇게 간단한 경우에는 보통 Authoring 컴포넌트의 각 필드를 Entity 컴포넌트의 해당 필드에 할당합니다.

      GetEntity 메서드에 전달하는 TransformUsageFlags는 엔티티가 가지고 있어야 하는 Transform 컴포넌트를 지정합니다.

      이 경우 렌더링을 위한 Transform 매트릭스를 가진 엔티티가 필요하고 런타임 시 엔티티를 옮겨야 하기 때문에 Dynamic 플래그를 사용합니다.

      또 다른 예로 설정 파라미터와 프리팹을 저장하는 Entity 컴포넌트가 필요한 경우를 살펴봅니다.

      여기서 MonsterPrefab이라는 Entity 필드는 인스턴스화하려는 프리팹을 참조하고

      Int 필드인 NumMonsters는 생성하려는 인스턴스의 수를 지정합니다.

      Baker의 Bake 메서드에서는 게임 오브젝트 프리팹 에셋을 GetEntity에 전달할 수 있고

      GetEntity는 프리팹의 베이크된 엔티티 형식을 반환한 다음 IComponentData의 MonsterPrefab 필드에 할당할 수 있습니다.

      이 GetEntity 호출은 트래킹할 프리팹 에셋도 등록하기 때문에 프리팹을 수정하면 Baker 재실행이 트리거됩니다.

      Baker가 종속된 다른 에셋의 경우에는 Baker는 종속성을 등록하기 위해서 DependsOn 메서드를 호출해야 합니다.

      등록된 에셋을 수정하면 Baker 재실행이 트리거됩니다.

      컴포넌트를 등록하는 다른 Baker 메서드로는 GetComponent와 GetComponentInChildren 등이 있습니다.

      종속성이 수정되면 Baker는 다시 실행됩니다.

      마지막으로 Baker는 CreateAdditionalEntity를 호출해서 추가 엔티티를 생성할 수도 있습니다

      베이킹 시스템에서 생성된 엔티티와는 달리 Baker에서 이 메서드로 생성된 추가적인 엔티티는 직렬화에 포함됩니다.

      Transform, Mesh Filter, Mesh Renderer, Box Collider 컴포넌트가 포함된 Cube 게임 오브젝트가 추가

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

      참고: 새 큐브는 실제로 New Sub Scene 게임 오브젝트의 자식이 아닌 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 컴포넌트는 베이킹 시 무시되며 제거해도 문제가 발생하지 않습니다.

      이제 플레이 모드에 들어가면 이 하위 씬에서 로드되는 하나의 큐브 엔티티가 보입니다.

      큐브를 여러 번 복제하고 새 큐브가 서로 겹치지 않도록 위치를 변경한 다음 모든 큐브가 명확하게 보이도록 카메라 위치를 변경합니다.

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

      RotationSpeedAuthoringTest.cs

      using Unity.Entities;
      using UnityEngine;
      using Unity.Mathematics;
      
      public struct RotationSpeedTest : IComponentData
      {
          public float RadiansPerSecond;  // 엔티티의 회전 속도 
      }
      
      public class RotationSpeedAuthoringTest : MonoBehaviour
      {
          public float DegreesPerSecond = 360.0f;
      
      }
      
      class RotationSpeedBaker : Baker<RotationSpeedAuthoringTest>
      {
          public override void Bake(RotationSpeedAuthoringTest authoring)
          {
              var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
      
              var rotationSpeed = new RotationSpeedTest
              {
                  RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
              };
      
              AddComponent(entity, rotationSpeed);
          }
      }

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

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

      Baker의 Bake 메서드는 GetEntity를 호출함으로써 베이크되는 엔티티를 가져오고 열거형 값 TransformUsageFlags.Dynamic을 전달하여

      해당 엔티티에 LocalTransform을 포함한 표준 Transform 컴포넌트가 필요하다는 것을 지정합니다.

      그런 다음 RotationSpeed 값을 생성하고 이를 엔티티에 새 컴포넌트로 추가합니다.

      참고: RotationSpeedAuthoringTest MonoBehaviour는 회전 속도를 초당 각도로 지정하지만

      RotationSpeedTest IComponentData는 회전 속도를 초당 라디안으로 지정하므로 Bake 메서드는 각도를 라디안으로 전환합니다.

      이는 저작 데이터(하위 씬에서 지정하는 것)와 런타임 데이터(런타임에 로드될 베이크된 엔티티)가 정확히 일대일로 대응할 필요가 없는 매우 간단한 사례를 보여 줍니다.

      이제 RotationSpeedAuthoringTest 을 정의했으니 이를 하위 씬에 있는 큐브 게임 오브젝트의 일부(전부는 아님)에 추가하고

      각 큐브의 DegreesPerSecond를 180~360 사이의 값으로 설정합니다.

      큐브 회전시키기

      CubeRotationSystemTest.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축을 중심으로 일정한 속도로 회전시킵니다.

      시스템의 OnUpdate 안에 다음과 같은 코드를 추가합니다.

      using Unity.Entities;
      using Unity.Transforms;
      using Unity.Burst;
      
      public partial struct CubeRotationSystemTest : ISystem
      {
          [BurstCompile]
          public void OnUpdate(ref SystemState state)
          {
              // CubeRotationSystem의 OnUpdate 내부 
              var deltaTime = SystemAPI.Time.DeltaTime;
      
              // 이 foreach는 LocalTransform과 RotationSpeed 컴포넌트를 가진 모든 엔티티를 반복합니다. 
              // LocalTransform은 수정해야 하므로 RefRW(읽기/쓰기)로 래핑됩니다. 
              // RotationSpeed는 읽기만 하면 되므로 RefRO(읽기 전용)로 래핑됩니다. 
              foreach (var (transform, rotationSpeed) in
                      SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeedTest>>())
              {
                  // Y축을 중심으로 트랜스폼을 회전합니다. 
                  var radians = rotationSpeed.ValueRO.RadiansPerSecond * deltaTime;
                  transform.ValueRW = transform.ValueRW.RotateY(radians);
              }
          }
      }
      

      참고: SystemAPI.Query는 소스 생성에 의해 처리되는 특수한 메서드이며 foreach 루프의 in 절에서만 호출할 수 있습니다.

      생성된 코드에서 쿼리가 생성되고 실행됩니다.

      SystemAPI.Query 호출에 2개 이상의 컴포넌트 유형이 포함된 경우 튜플을 반환합니다.

      따라서 튜플을 분해하기 위해 transform과 speed 변수를 괄호로 묶습니다

      SystemAPI.Query 호출은 LocalTransformRotationSpeed 컴포넌트를 가진 모든 엔티티에 쿼리를 수행합니다.

      foreach는 쿼리와 일치하는 각 엔티티를 반복하며 transform 변수에 읽기/쓰기 레퍼런스를 할당하고 rotationSpeed 변수에 읽기 전용 레퍼런스를 할당합니다.

      루프 본문에서는 ValueRW(읽기/쓰기) 프로퍼티를 통해 LocalTransform을 읽고 쓸 수 있으며 ValueRO(읽기 전용) 프로퍼티를 통해 RotationSpeed를 읽을 수 있습니다.

      LocalTransform 메서드 RotateY는 라디안 값을 사용하고 컴포넌트에 다시 할당하는 회전된 새 트랜스폼을 반환합니다.

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

      프리팹에서 엔티티 생성

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

      SpawnerAuthoringTest.cs를 정의하고 다음 코드를 추가합니다.

      using Unity.Entities;
      using UnityEngine;
      
      public class SpawnerAuthoringTest : MonoBehaviour
      {
          public GameObject CubePrefab;
      
          class Baker : Baker<SpawnerAuthoringTest>
          {
              public override void Bake(SpawnerAuthoringTest authoring)
              {
                  // 프리팹을 베이크하는 코드를 여기에 입력합니다.	
              }
          }
      }
      
      struct SpawnerTest : IComponentData
      {
          public Entity CubePrefab;
      }

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

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

      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 컴포넌트를 가진 엔티티가 베이크된 SubScene에 표시됩니다.

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

      using Unity.Burst;
      using Unity.Collections;
      using Unity.Entities;
      using Unity.Mathematics;
      using Unity.Transforms;
      
      public partial struct SpawnSystemTest : ISystem
      {
          [BurstCompile]
          public void OnCreate(ref SystemState state)
          {
              state.RequireForUpdate<SpawnerTest>();
          }
      
          [BurstCompile]
          public void OnUpdate(ref SystemState state)
          {
              state.Enabled = false;
      
              // 곧 여기에 프리팹을 생성하는 코드를 추가할 것입니다.
          }
      }

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

      일반적으로 시스템은 초기 씬이 로드되기 전에 인스턴스화되고 업데이트를 시작하지만, 여기서는 Spawner 컴포넌트를 가진 엔티티가 하위 씬에서 로드된 후에 시스템이 업데이트되도록 만들려고 합니다.

      OnCreate에서 SystemState 메서드 RequireForUpdate<Spawner>()를 호출하면 Spawner 컴포넌트를 가진 엔티티가 하나 이상 존재하지 않는 한 프레임에서 시스템이 업데이트되지 않습니다.

      큐브 프리팹의 인스턴스를 생성하려면 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가 사용됩니다.

      새 큐브 인스턴스가 서로 겹치지 않게 그려지도록 하려면 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 사이의 범위)로 설정됩니다.

      댓글 달기

      이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

      위로 스크롤