DOTS – EntitesSample (1)

1. HelloCube_MainThreadSubscene

RotationSystem.cs

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

namespace HelloCube.MainThread
{
    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteMainThread>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            float deltaTime = SystemAPI.Time.DeltaTime;

            // 모든 엔티티에 대해 LocalTransform 컴포넌트와 RotationSpeed 컴포넌트가 있는지 반복합니다.
            // 각 반복에서 transform은 LocalTransform에 대한 읽기-쓰기 참조로 할당되고,
            // speed는 RotationSpeed 컴포넌트에 대한 읽기 전용 참조로 할당됩니다.
            // SystemAPI.Query와 같은 작업은 메인 스레드에서 수행
            foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
            {
                // ValueRW와 ValueRO는 모두 실제 컴포넌트 값에 대한 참조를 반환합니다.
                // 차이점
                // ValueRW는 읽기-쓰기 접근에 대한 안전성 검사를 수행
                // ValueRO는 읽기 전용 접근에 대한 안전성 검사를 수행
                transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
            }
        }
    }
}

OnCreate()는 시스템이 생성될 때 호출됩니다.

RequireForUpdate를 사용하여 시스템 업데이트를 위해 ExecuteMainThread 컴포넌트가 필요함을 선언합니다.

OnUpdate()는 시스템이 업데이트될 때 호출됩니다.

SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>를 사용하여 LocalTransform과 RotationSpeed 컴포넌트를 가진 모든 엔티티를 반복합니다.

transform은 LocalTransform 컴포넌트의 읽기-쓰기 참조를 나타내고,

speed는 RotationSpeed 컴포넌트의 읽기 전용 참조를 나타냅니다.

transform.ValueRW를 업데이트하여 회전 속도(RadiansPerSecond)와 deltaTime을 기반으로 엔티티를 회전시킵니다.

RotationSpeedAuthoring.cs

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

namespace HelloCube
{
    // Authoring 컴포넌트는 일반적인 MonoBehavior입니다.
    public class RotationSpeedAuthoring : MonoBehaviour
    {
        public float DegreesPerSecond = 360.0f;

        // 베이킹 중에 이 Baker는 엔티티 서브씬에 있는 모든 RotationSpeedAuthoring 인스턴스에 대해 한 번 실행됩니다.
        // (Authoring 컴포넌트의 Baker 클래스를 중첩하는 것은 단지 스타일상의 선택일 뿐입니다.)
        class Baker : Baker<RotationSpeedAuthoring>
        {
            public override void Bake(RotationSpeedAuthoring authoring)
            {
                // 엔티티가 이동됩니다.
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent(entity, new RotationSpeed
                {
                    RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
                });
            }
        }
    }
    // RotationSpeed 구조체는 IComponentData를 구현합니다.
    public struct RotationSpeed : IComponentData
    {
        public float RadiansPerSecond;
    }
}

Baker 클래스는 Baker를 상속받는 중첩 클래스입니다.

Bake 메서드는 엔티티를 가져와 해당 엔티티에 RotationSpeed 컴포넌트를 추가합니다.

TransformUsageFlags.Dynamic 플래그를 사용하여 엔티티를 동적으로 사용할 수 있도록 설정합니다.

Baker 클래스는 엔티티에 컴포넌트와 속성을 적용하는 역할을 합니다.

이 클래스는 Baker을 상속받아 엔티티 서브씬에서 RotationSpeedAuthoring 인스턴스를 기반으로 작업을 수행

IComponentData 인터페이스를 구현

RadiansPerSecond라는 float 변수를 통해 회전 속도를 저장

2. HelloCube_IJobEntity

HelloCube_MainThreadSubscene과 RotationSpeedAuthoring코드는 같음

RotationSystem.cs

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

namespace HelloCube.JobEntity
{
    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteIJobEntity>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var job = new RotateAndScaleJob
            {
                deltaTime = SystemAPI.Time.DeltaTime,
                elapsedTime = (float)SystemAPI.Time.ElapsedTime
            };
            job.Schedule();
        }
    }

    [BurstCompile]
    partial struct RotateAndScaleJob : IJobEntity
    {
        public float deltaTime;
        public float elapsedTime;

        // 소스 생성 시, Execute()의 매개변수에서 쿼리가 생성됩니다.
        // 여기에서 쿼리는 LocalTransform, PostTransformMatrix 및 RotationSpeed 컴포넌트를 가진 모든 엔티티와 일치합니다.
        // (씬에서 루트 큐브는 비균일 스케일을 가지므로, 베이킹 시 PostTransformMatrix 컴포넌트를 부여받습니다.)
        void Execute(ref LocalTransform transform, ref PostTransformMatrix postTransform, in RotationSpeed speed)
        {
            transform = transform.RotateY(speed.RadiansPerSecond * deltaTime);
            postTransform.Value = float4x4.Scale(1, math.sin(elapsedTime), 1);
        }
    }
}

IJobEntity를 사용하여 작업을 병렬로 실행할 수 있어 성능을 최적화 할 수 있습니다.

일반적인 LocalTransform 컴포넌트는 엔티티의 위치, 회전, 스케일을 나타내지만, 비균일 스케일을 적용하는 데 적합하지 않을 수 있습니다.

따라서, 비균일 스케일을 지원하기 위해 PostTransformMatrix 컴포넌트를 사용합니다.

이 컴포넌트는 객체의 후처리 변환 행렬을 저장하고 적용하는 데 사용됩니다.

PostTransformMatrix를 사용하여 시간에 따라 Y축 스케일을 동적으로 변경하는 애니메이션을 적용합니다

3. HelloCube_Aspects

HelloCube_MainThreadSubscene, HelloCube_IJobEntity과 RotationSpeedAuthoring 코드는 같음

RotationSystem.cs

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

namespace HelloCube.Aspects
{
    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteAspects>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            float deltaTime = SystemAPI.Time.DeltaTime;
            double elapsedTime = SystemAPI.Time.ElapsedTime;

            // Aspect를 사용하지 않고 큐브를 직접 회전시킵니다.
            // 이 쿼리는 LocalTransform 및 RotationSpeed 컴포넌트를 가진 모든 엔티티와 일치합니다.
            foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
            {
                transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
            }

            // Aspect를 사용하여 큐브를 회전시킵니다.
            // 이 쿼리는 VerticalMovementAspect의 모든 컴포넌트를 포함합니다.
            // 컴포넌트와 달리, SystemAPI.Query의 Aspect 타입 매개변수는 RefRW 또는 RefRO로 래핑되지 않습니다.
            foreach (var movement in SystemAPI.Query<VerticalMovementAspect>())
            {
                movement.Move(elapsedTime);
            }
        }
    }

    // 이 Aspect의 인스턴스는 하나의 엔티티의 LocalTransform 및 RotationSpeed 컴포넌트를 래핑합니다.
    // (이 간단한 예시는 Aspects의 유용한 사용 사례가 아닐 수 있지만, 더 큰 예제는 그들의 유용성을 더 잘 보여줍니다.)
    readonly partial struct VerticalMovementAspect : IAspect
    {
        readonly RefRW<LocalTransform> m_Transform;
        readonly RefRO<RotationSpeed> m_Speed;

        public void Move(double elapsedTime)
        {
            m_Transform.ValueRW.Position.y = (float)math.sin(elapsedTime * m_Speed.ValueRO.RadiansPerSecond);
        }
    }
}

차이점 : IAspect

VerticalMovementAspectLocalTransformRotationSpeed 컴포넌트를 묶어 수직 이동을 담당합니다.

IAspect를 사용하는 이유는 코드를 더 구조화하고, 유지보수를 쉽게 하며, 성능을 향상시키기 위해서 사용

1. 코드 구조화 및 가독성 향상

  • 컴포넌트의 그룹화:
    여러 컴포넌트를 하나의 애스펙트로 묶어 관리할 수 있어, 코드의 가독성이 향상됩니다.
    예를 들어, VerticalMovementAspectLocalTransformRotationSpeed 컴포넌트를 함께 래핑합니다.
  • 명확한 역할 분담:
    각 애스펙트는 특정 기능이나 역할을 담당하게 되어, 시스템의 각 부분이 무엇을 하는지 명확하게 이해할 수 있습니다.

2. 유지보수 용이성

  • 코드 재사용성 증가:
    애스펙트를 사용하면 공통 기능을 쉽게 재사용할 수 있습니다.
    예를 들어, 여러 시스템에서 동일한 애스펙트를 사용하여 일관된 동작을 구현할 수 있습니다.
  • 변경 용이성:
    컴포넌트나 기능을 변경해야 할 때, 애스펙트 내부에서만 수정하면 되므로 유지보수가 용이합니다.

3. 성능 최적화

  • 효율적인 데이터 접근:
    애스펙트는 관련된 컴포넌트를 함께 묶어서 처리하기 때문에, 데이터 접근이 더 효율적입니다.
    이는 특히 큰 규모의 ECS 시스템에서 성능 향상에 기여할 수 있습니다.
  • 캐시 친화성:
    애스펙트를 사용하면 메모리 레이아웃이 더 최적화되어, 캐시 친화적인 접근 방식을 제공할 수 있습니다.

4. HelloCube_Prefabs

SpawnerAuthoring.cs

using Unity.Entities;
using UnityEngine;

namespace HelloCube.Prefabs
{
    // Authoring 컴포넌트는 Baker<T> 클래스를 가지는 일반적인 MonoBehavior입니다.
    public class SpawnerAuthoring : MonoBehaviour
    {
        public GameObject Prefab;

        // ECS에서 데이터를 변환하는 과정인 베이킹(Baking)
        // 베이킹 중에 이 Baker는 서브씬의 모든 SpawnerAuthoring 인스턴스에 대해 한 번 실행됩니다.
        // (Authoring 컴포넌트의 Baker 클래스를 Authoring MonoBehaviour 클래스 안에 중첩하는 것은 단지 스타일상의 선택일 뿐입니다.)
        class Baker : Baker<SpawnerAuthoring>
        {
            
            public override void Bake(SpawnerAuthoring authoring)
            {
                // SpawnerAuthoring 컴포넌트를 ECS의 엔티티(Entity)로 변환
                var entity = GetEntity(TransformUsageFlags.None);

                // 변환된 엔티티에 ECS 구성 요소(Component)를 추가
                AddComponent(entity, new Spawner
                {
                    // Editor에서 설정된 Prefab(GameObject)을 동적으로 변환
                    Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic)
                });
            }
        }
    }

    struct Spawner : IComponentData
    {
        public Entity Prefab;
    }

}

SpawnSystem.cs

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

namespace HelloCube.Prefabs
{
    public partial struct SpawnSystem : ISystem
    {
        uint updateCounter;

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // Spawner 컴포넌트를 가진 엔티티가 월드에 적어도 하나 존재하지 않으면 시스템이 업데이트되지 않도록 합니다.
            state.RequireForUpdate<Spawner>();
            // ExecutePrefabs 컴포넌트를 가진 엔티티가 월드에 적어도 하나 존재하지 않으면 시스템이 업데이트되지 않도록 합니다.
            state.RequireForUpdate<ExecutePrefabs>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // RotationSpeed 컴포넌트를 가진 모든 엔티티에 매칭되는 쿼리를 생성합니다.
            // (이 쿼리는 소스 생성 과정에서 캐시되므로, 매번 업데이트 시 재생성되는 비용이 발생하지 않습니다.)
            var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed>().Build();

            // 현재 회전하는 큐브가 하나도 없을 때만 큐브를 스폰합니다.
            if (spinningCubesQuery.IsEmpty)
            {
                var prefab = SystemAPI.GetSingleton<Spawner>().Prefab;

                // 엔티티를 인스턴스화하면 동일한 컴포넌트 타입과 값을 가진 복사 엔티티들이 생성됩니다.
                var instances = state.EntityManager.Instantiate(prefab, 500, Allocator.Temp);

                // new Random()과는 다르게, CreateFromIndex()는 랜덤 시드를 해싱하여
                // 유사한 시드가 유사한 결과를 생성하지 않도록 합니다.
                var random = Random.CreateFromIndex(updateCounter++);

                foreach (var entity in instances)
                {
                    // 엔티티의 LocalTransform 컴포넌트를 새로운 위치로 업데이트합니다.
                    var transform = SystemAPI.GetComponentRW<LocalTransform>(entity);
                    transform.ValueRW.Position = (random.NextFloat3() - new float3(0.5f, 0, 0.5f)) * 20;
                }
            }
        }
    }
}

FallAndDestroySystem.cs

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

namespace HelloCube.Prefabs
{
    public partial struct FallAndDestroySystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // ExecutePrefabs 컴포넌트를 가진 엔티티가 존재해야만 시스템이 업데이트되도록 설정합니다.
            state.RequireForUpdate<ExecutePrefabs>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // 회전 처리
            float deltaTime = SystemAPI.Time.DeltaTime;
            foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
            {
                // ValueRW와 ValueRO는 모두 실제 컴포넌트 값에 대한 참조를 반환합니다.
                // 차이점은 ValueRW는 읽기-쓰기 접근에 대해 안전성 검사를 수행하고,
                // ValueRO는 읽기 전용 접근에 대해 안전성 검사를 수행한다는 점입니다.
                transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
            }

            // EntityCommandBufferSystem.Singleton에서 생성된 EntityCommandBuffer는
            // EntityCommandBufferSystem이 다음에 업데이트될 때 실행되고 소멸됩니다.
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

            // 아래 방향 벡터
            var movement = new float3(0, -SystemAPI.Time.DeltaTime * 5f, 0);

            // WithAll()은 쿼리에 RotationSpeed를 포함하지만,
            // RotationSpeed 컴포넌트 값에 접근하지는 않습니다.
            // WithEntityAccess()는 엔티티 ID를 튜플의 마지막 요소로 포함시킵니다.
            foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>()
                .WithAll<RotationSpeed>()
                .WithEntityAccess())
            {
                transform.ValueRW.Position += movement;
                if (transform.ValueRO.Position.y < 0)
                {
                    // 구조적 변경을 하면 현재 반복 중인 쿼리가 무효화될 수 있으므로,
                    // 대신 해당 엔티티를 나중에 삭제하도록 명령을 기록합니다.
                    ecb.DestroyEntity(entity);
                }
            }
        }
    }
}

EntityCommandBuffer (ECB)는 모든 OnUpdate가 끝난 이후 실행

왜 EntityCommandBuffer가 필요한가

  • 즉시 구조적 변경은 위험:
    • ECS에서는 구조적 변경(엔티티 생성, 삭제, 컴포넌트 추가/삭제)을 즉시 수행하면 현재 실행 중인 쿼리나 시스템에서 사용하는 데이터가 무효화될 수 있습니다.
      이는 성능 저하나 예기치 못한 동작을 유발할 수 있습니다.
  • ECB는 안전한 방식:
    • EntityCommandBuffer를 사용하면 시스템에서 즉시 구조적 변경을 수행하지 않고, 변경 명령을 큐에 저장합니다.
      저장된 명령은 해당 프레임의 ECS 업데이트 단계가 모두 완료된 후 실행됩니다.

5. HelloCube_IJobChunk

RotationSystem.cs

using Unity.Assertions;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;

namespace HelloCube.JobChunk
{
    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // 시스템이 업데이트되기 위해서는 ExecuteIJobChunk 컴포넌트가 적어도 하나 이상 있어야 함을 지정
            state.RequireForUpdate<ExecuteIJobChunk>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // RotationSpeed와 LocalTransform 컴포넌트를 가진 모든 엔티티를 쿼리하는 쿼리를 생성합니다.
            var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed, LocalTransform>().Build();

            var job = new RotationJob
            {
                TransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(),
                RotationSpeedTypeHandle = SystemAPI.GetComponentTypeHandle<RotationSpeed>(true), // 읽기 전용으로 설정
                DeltaTime = SystemAPI.Time.DeltaTime
            };

            // IJobEntity와 달리, IJobChunk는 쿼리를 수동으로 전달해야 합니다.
            // 또한, IJobChunk는 state.Dependency JobHandle을 암시적으로 전달하거나 할당하지 않습니다.
            // (state.Dependency를 전달하고 할당하는 이 패턴은 서로 다른 시스템에서 예약된 엔티티 작업이 필요한 대로 의존하도록 보장합니다.)
            state.Dependency = job.Schedule(spinningCubesQuery, state.Dependency);
        }
    }


    [BurstCompile]
    struct RotationJob : IJobChunk
    {
        public ComponentTypeHandle<LocalTransform> TransformTypeHandle;
        [ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
        public float DeltaTime;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
        {
            // useEnabledMask 매개변수는 쿼리 내 구성 요소 중 하나 이상이 비활성화된 엔티티가 있을 때 true가 됩니다.
            // 쿼리 구성 요소 유형에 IEnableableComponent를 구현한 것이 없으면, useEnabledMask는 항상 false라고 가정할 수 있습니다.
            // 그러나 누군가 나중에 쿼리나 구성 요소 유형을 변경할 가능성을 고려하여 이 검사를 추가하는 것이 좋은 습관입니다.
            Assert.IsFalse(useEnabledMask);

            // 청크에서 LocalTransform 컴포넌트를 가져옵니다.
            var transforms = chunk.GetNativeArray(ref TransformTypeHandle);
            // 청크에서 RotationSpeed 컴포넌트를 읽기 전용으로 가져옵니다.
            var rotationSpeeds = chunk.GetNativeArray(ref RotationSpeedTypeHandle);

            // 청크 내 모든 엔티티에 대해 반복하면서 회전을 계산합니다.
            for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)
            {
                // 엔티티의 LocalTransform을 업데이트하여 Y축으로 회전시킵니다.
                transforms[i] = transforms[i].RotateY(rotationSpeeds[i].RadiansPerSecond * DeltaTime);
            }
        }
    }
}

Aspects, IJobChunk, IJobEntity

Aspects, IJobChunk, IJobEntity는 모두 Unity의 ECS(Entity Component System)에서 엔티티 기반의 데이터 처리를 위한 접근 방식이지만 각각 다른 방식으로 동작하고 사용

이들은 크게 Aspects 기반 코드, IJobChunk 기반 코드, IJobEntity 기반 코드로 구분할 수 있으며, 각기 다른 목적과 사용 상황에 맞게 선택됩니다.

1. Aspects 기반 코드

  • Aspects는 ECS에서 데이터를 객체 지향적으로 다룰 수 있는 방법을 제공합니다.
    Aspects는 여러 컴포넌트를 하나의 구조로 묶어 엔티티에 대한 로직을 캡슐화하며, 주로 복잡한 데이터 구조를 처리하는 데 유용합니다.
    이를 통해 특정 데이터를 가진 엔티티들을 쉽게 다룰 수 있으며, 코드가 간결하고 유지보수가 용이합니다.
  • 주요 특징:
    • 코드 간결성
    • 높은 가독성
    • 데이터와 로직을 객체 지향적으로 묶어 처리
    • 유지보수가 쉬운 구조 제공
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace HelloCube.Aspects
{
    // Aspect 정의
    readonly partial struct RotationAspect : IAspect
    {
        readonly RefRW<LocalTransform> transform;
        readonly RefRO<RotationSpeed> speed;

        public void Rotate(float deltaTime)
        {
            transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
        }
    }

    public partial struct RotationSystem : ISystem
    {
        public void OnCreate(ref SystemState state) {}

        public void OnUpdate(ref SystemState state)
        {
            float deltaTime = SystemAPI.Time.DeltaTime;

            // Aspect를 사용하여 회전 처리
            foreach (var rotationAspect in SystemAPI.Query<RotationAspect>())
            {
                rotationAspect.Rotate(deltaTime);
            }
        }
    }
}

2. IJobChunk 기반 코드

  • IJobChunk큰 데이터 집합을 효율적으로 처리하기 위해 청크 단위로 엔티티를 다루는 방법입니다.
    데이터를 더 최적화된 방식으로 처리할 수 있으며, 성능을 크게 향상시킬 수 있습니다.
    IJobChunk는 기본적으로 모든 엔티티를 청크 단위로 처리하고, 이를 멀티스레드 환경에서 병렬로 실행할 수 있습니다.
  • 주요 특징:
    • 성능 최적화 (특히 대규모 엔티티 처리)
    • 청크 단위로 데이터 처리
    • 고성능 작업이 필요한 경우 적합
    • 복잡한 코드 (비교적 많은 설정이 필요)
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

namespace HelloCube.JobChunk
{
    [BurstCompile]
    struct RotationJob : IJobChunk
    {
        public ComponentTypeHandle<LocalTransform> TransformTypeHandle;
        [ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
        public float DeltaTime;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
        {
            var transforms = chunk.GetNativeArray(ref TransformTypeHandle);
            var speeds = chunk.GetNativeArray(ref RotationSpeedTypeHandle);

            for (int i = 0; i < chunk.Count; i++)
            {
                transforms[i] = transforms[i].RotateY(speeds[i].RadiansPerSecond * DeltaTime);
            }
        }
    }

    public partial struct RotationSystem : ISystem
    {
        public void OnCreate(ref SystemState state) {}

        public void OnUpdate(ref SystemState state)
        {
            var job = new RotationJob
            {
                TransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(),
                RotationSpeedTypeHandle = SystemAPI.GetComponentTypeHandle<RotationSpeed>(true),
                DeltaTime = SystemAPI.Time.DeltaTime
            };

            var query = SystemAPI.QueryBuilder().WithAll<RotationSpeed, LocalTransform>().Build();
            state.Dependency = job.Schedule(query, state.Dependency);
        }
    }
}

3. IJobEntity 기반 코드

  • IJobEntity는 각 엔티티에 대해 작업을 수행하는 방식입니다.
    IJobEntity는 간단하고 직관적인 코드 작성이 가능하지만, 대규모 데이터를 처리할 때 성능이 떨어질 수 있습니다.
    그러나 작은 규모의 엔티티를 다룰 때는 유용하고, 코드 작성이 쉽습니다.
  • 주요 특징:
    • 코드 간결성
    • 소규모 엔티티 처리에 적합
    • 복잡한 데이터 구조가 필요하지 않음
    • 성능 최적화 측면에서는 IJobChunk보다는 낮음
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;

namespace HelloCube.JobEntity
{
    [BurstCompile]
    public partial struct RotationSystem : ISystem
    {
        public void OnCreate(ref SystemState state) {}

        public void OnUpdate(ref SystemState state)
        {
            // 각 엔티티를 쿼리하여 회전 처리
            foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
            {
                float deltaTime = SystemAPI.Time.DeltaTime;
                transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
            }
        }
    }
}
기능Aspects 기반 코드IJobChunk 기반 코드IJobEntity 기반 코드
코드 간결성높은 수준의 가독성 제공상대적으로 코드가 더 복잡비교적 간결하고 직관적인 코드
성능 요구 사항소규모/중간 규모의 엔티티 처리에 적합대규모 엔티티 처리와 성능 최적화에 적합소규모에서 중간 규모 엔티티 처리에 적합
유지 보수성Aspect로 동작 분리 가능쿼리 및 데이터 처리가 분리되어 유지보수가 복잡할 수 있음코드가 간결하여 유지보수가 용이
캐시 효율성기본 캐시 사용캐시 최적화에 더 적합기본 캐시 사용 및 IJobChunk보다 덜 최적화됨

6. HelloCube_Reparenting

ReparentingSystem.cs

public partial struct ReparentingSystem : ISystem
{
    bool attached;
    float timer;
    const float interval = 0.7f;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        timer = interval;
        attached = true;
        state.RequireForUpdate<ExecuteReparenting>();
        state.RequireForUpdate<RotationSpeed>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        timer -= SystemAPI.Time.DeltaTime; // 타이머를 감소시켜 일정 시간 간격을 유지합니다.
        if (timer > 0)
        {
            return; // 타이머가 0보다 크면 업데이트를 중단하고 계속 대기
        }
        timer = interval; // 타이머를 다시 초기화 (간격 유지)

        var rotatorEntity = SystemAPI.GetSingletonEntity<RotationSpeed>(); // 회전 속도를 가진 엔티티 가져오기
        var ecb = new EntityCommandBuffer(Allocator.Temp); // EntityCommandBuffer 생성, 임시 할당으로 사용

        if (attached)
        {
            // 부모에서 분리: 'RotationSpeed' 컴포넌트를 가진 엔티티를 찾고, 그 자식들을 분리합니다.
            DynamicBuffer<Child> children = SystemAPI.GetBuffer<Child>(rotatorEntity);
            for (int i = 0; i < children.Length; i++)
            {
                ecb.RemoveComponent<Parent>(children[i].Value); // 자식들의 'Parent' 컴포넌트 제거
            }
        }
        else
        {
            // 부모에 붙이기: 'RotationSpeed' 컴포넌트가 없는 모든 엔티티를 부모에 추가합니다.
            foreach (var (transform, entity) in
                     SystemAPI.Query<RefRO<LocalTransform>>()
                         .WithNone<RotationSpeed>() // RotationSpeed가 없는 엔티티만
                         .WithEntityAccess()) // Entity를 참조로 포함
            {
                ecb.AddComponent(entity, new Parent { Value = rotatorEntity }); // 'Parent' 컴포넌트 추가
            }
        }

        ecb.Playback(state.EntityManager); // 명령 버퍼의 명령들을 실행

        attached = !attached; // 부모-자식 관계를 번갈아가며 변경
    }
}

timer를 기반으로 부모-자식 관계를 주기적으로 바꿉니다.

attached 변수를 사용해 부모에서 분리하거나 부모에 다시 붙입니다.

EntityCommandBuffer를 사용하여 엔티티의 부모-자식 관계를 효율적으로 수정합니다.

RotationSystem.cs

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

public partial struct RotationSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<ExecuteReparenting>(); // ReparentingSystem이 먼저 실행될 것임을 명시
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime; // 델타 시간(프레임 간 시간 차이)

        // 'RotationSpeed'가 있는 모든 엔티티에 대해 회전 적용
        foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
        {
            transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime); // Y축을 중심으로 회전
        }
    }
}

7. HelloCube_EnableableComponents

EnableableComponents를 사용하여 회전 속도를 제어하는 시스템을 구현

RotationSpeedAuthoring.cs

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

namespace HelloCube.EnableableComponents
{
    public class RotationSpeedAuthoring : MonoBehaviour
    {
        public bool StartEnabled;
        public float DegreesPerSecond = 360.0f;

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

                // RotationSpeed 컴포넌트를 엔티티에 추가하고, DegreesPerSecond를 라디안으로 변환하여 설정
                AddComponent(entity, new RotationSpeed { RadiansPerSecond = math.radians(authoring.DegreesPerSecond) });

                // RotationSpeed 컴포넌트를 활성화하거나 비활성화
                SetComponentEnabled<RotationSpeed>(entity, authoring.StartEnabled);
            }
        }
    }

    struct RotationSpeed : IComponentData, IEnableableComponent
    {
        public float RadiansPerSecond;
    }
}

Bake 메서드는 AddComponent를 통해 RotationSpeed 컴포넌트를 엔티티에 추가하며, SetComponentEnabled를 사용해 해당 컴포넌트를 활성화하거나 비활성화합니다.

RotationSystem.cs

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

namespace HelloCube.EnableableComponents
{
    public partial struct RotationSystem : ISystem
    {
        float timer;
        const float interval = 1.3f;

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            timer = interval;
            state.RequireForUpdate<ExecuteEnableableComponents>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            float deltaTime = SystemAPI.Time.DeltaTime; // 델타 타임을 가져옵니다.
            timer -= deltaTime;

            // RotationSpeed가 활성화된 모든 엔티티에 대해 회전 상태를 주기적으로 토글합니다.
            if (timer < 0)
            {
                foreach (var rotationSpeedEnabled in SystemAPI.Query<EnabledRefRW<RotationSpeed>>()
                    .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState))
                {
                    rotationSpeedEnabled.ValueRW = !rotationSpeedEnabled.ValueRO; // 활성화 상태를 반전시킵니다.
                }

                timer = interval; // 타이머를 초기화합니다.
            }

            // RotationSpeed가 활성화된 엔티티에 대해서만 회전을 적용합니다.
            foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
            {
                transform.ValueRW = transform.ValueRO.RotateY(
                    speed.ValueRO.RadiansPerSecond * deltaTime);
            }
        }
    }
}

EnabledRefRW<RotationSpeed>RotationSpeed 컴포넌트가 활성화된 엔티티를 찾습니다.

활성화 상태를 반전시키는 방식으로 주기적인 상태 변경을 구현합니다.

댓글 달기

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

위로 스크롤