DOTS – EntitesSample (3)

13. Custom2DTransforms

Transform2DAuthoring.cs

Transform2DAuthoring 클래스의 주요 목표는 기존 Unity의 Transform 시스템을 사용하지 않고,

ECS(Entity Component System)에서 동작하는 최적화된 커스텀 2D 트랜스폼 시스템을 구현하는 것

using System.Globalization;
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace HelloCube.CustomTransforms
{
    public class Transform2DAuthoring : MonoBehaviour
    {
        class Baker : Baker<Transform2DAuthoring>
        {
            public override void Bake(Transform2DAuthoring authoring)
            {
                // 표준 Transform 컴포넌트가 추가되지 않도록 보장합니다.
                // TransformUsageFlags.ManualOverride를 사용하면 ECS가 해당 엔티티에 대해
                // 기본 Transform 컴포넌트(Translation, Rotation, Scale 등)를 자동으로 추가하지 않도록 제어
                // Unity의 표준 트랜스폼 처리 로직을 비활성화
                // 커스텀 트랜스폼 시스템을 사용하면 2D 전용으로 최적화하여 불필요한 계산을 줄이고 성능을 향상
                var entity = GetEntity(TransformUsageFlags.ManualOverride);
                AddComponent(entity, new LocalTransform2D
                {
                    Scale = 1
                });
                AddComponent(entity, new LocalToWorld
                {
                    Value = float4x4.Scale(1)
                });

                var parentGO = authoring.transform.parent;
                if (parentGO != null)
                {
                    AddComponent(entity, new Parent
                    {
                        Value = GetEntity(parentGO, TransformUsageFlags.None)
                    });
                }
            }
        }
    }

    // LocalToWorld 쓰기 그룹에 LocalTransform2D를 포함시킴으로써,
    // LocalTransform2D를 가진 엔티티는 표준 Transform 시스템에 의해 처리되지 않습니다.
    [WriteGroup(typeof(LocalToWorld))]
    public struct LocalTransform2D : IComponentData
    {
        public float2 Position; // 2D 공간에서의 위치를 나타내는 float2
        public float Scale; // 엔티티의 크기를 나타내는 float
        public float Rotation; // 2D 공간에서의 회전을 나타내며, 단위는 degree(도)

        public override string ToString()
        {
            return $"Position={Position.ToString()} Rotation={Rotation.ToString()} Scale={Scale.ToString(CultureInfo.InvariantCulture)}";
        }

        // 트랜스폼 데이터를 기반으로 4x4 변환 매트릭스를 생성
        // math.radians(Rotation)으로 도(degree)를 라디안으로 변환
        // float4x4.TRS를 사용하여 위치(Translation), 회전(Rotation), 스케일(Scale)을 포함하는 매트릭스를 생성
        public float4x4 ToMatrix()
        {
            quaternion rotation = quaternion.RotateZ(math.radians(Rotation));
            return float4x4.TRS(new float3(Position.xy, 0f), rotation, Scale);
        }
    }
}

MovementSystem.cs

커스텀 2D 트랜스폼 데이터(LocalTransform2D)를 사용하여 객체를 움직이고 크기 및 회전을 조정하는 시스템을 구현

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

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            float rotation = SystemAPI.Time.DeltaTime * 180f; // Half a rotation every second (in degrees)
            float elapsedTime = (float) SystemAPI.Time.ElapsedTime;
            float xPosition = math.sin(elapsedTime) * 2f - 1f;
            float scale = math.sin(elapsedTime * 2f) + 1f; // math.sin(elapsedTime * 2f)를 사용하여 스케일이 0에서 2까지 진동
            scale = scale <= 0.001f ? 0f : scale;

            foreach (var localTransform2D in SystemAPI
                .Query<RefRW<LocalTransform2D>>()
                .WithNone<Parent>())
            {
                localTransform2D.ValueRW.Position.x = xPosition;
                localTransform2D.ValueRW.Rotation = localTransform2D.ValueRO.Rotation + rotation;
                localTransform2D.ValueRW.Scale = scale;
            }
        }
    }
}

LocalToWorld2DSystem.cs

LocalTransform2D 데이터를 기반으로 각 엔티티의 월드 공간(LocalToWorld 변환 행렬)을 계산합니다.

추가적으로 부모-자식 관계를 고려하여 계층 구조를 통해 변환을 계산합니다.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Assertions;
using Unity.Burst.Intrinsics;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Unity.Transforms;

namespace HelloCube.CustomTransforms
{
    // 이 시스템은 LocalTransform2D를 가진 각 엔티티에 대해 변환 행렬(Transform Matrix)을 계산합니다.

    // 부모가 없는 루트 레벨/월드 공간 엔티티의 경우,
    // LocalToWorld는 엔티티의 LocalTransform2D에서 직접 계산될 수 있습니다.

    // 자식 엔티티의 경우, 각 고유 계층 구조를 재귀적으로 탐색하며,
    // 부모의 변환과 자식의 변환을 합성하여 각 자식의 LocalToWorld를 계산합니다.

    [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] // 이 시스템은 기본 월드와 에디터 월드에서 작동하도록 설정
    [UpdateInGroup(typeof(TransformSystemGroup))] // TransformSystemGroup: 변환 계산과 관련된 시스템 그룹에서 실행
    [UpdateAfter(typeof(ParentSystem))] // ParentSystem 이후에 업데이트: 부모-자식 관계가 올바르게 계산된 후 실행
    [BurstCompile]
    public partial struct LocalToWorld2DSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<LocalTransform2D>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // LocalTransform2D와 LocalToWorld를 가진 엔티티, 부모가 없는 루트 레벨 엔티티.
            var rootsQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform2D>()
                .WithAllRW<LocalToWorld>()
                .WithNone<Parent>().Build();

            // LocalTransform2D와 자식(Child) 정보를 가진 엔티티.부모가 없는 상위 레벨 엔티티.
            var parentsQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform2D, Child>()
                .WithAllRW<LocalToWorld>()
                .WithNone<Parent>().Build();

            var localToWorldWriteGroupMask = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform2D, Parent>()
                .WithAllRW<LocalToWorld>().Build().GetEntityQueryMask();

            // ComputeRootLocalToWorldJob을 실행하여 루트 엔티티의 LocalToWorld를 계산
            var rootJob = new ComputeRootLocalToWorldJob
            {
                LocalTransform2DTypeHandleRO = SystemAPI.GetComponentTypeHandle<LocalTransform2D>(true),
                PostTransformMatrixTypeHandleRO = SystemAPI.GetComponentTypeHandle<PostTransformMatrix>(true),
                LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(),
                LastSystemVersion = state.LastSystemVersion,
            };
            state.Dependency = rootJob.ScheduleParallelByRef(rootsQuery, state.Dependency);

            // ComputeChildLocalToWorldJob을 실행하여 자식 엔티티 계층을 따라가며 LocalToWorld를 계산
            var childJob = new ComputeChildLocalToWorldJob
            {
                LocalToWorldWriteGroupMask = localToWorldWriteGroupMask,
                ChildTypeHandle = SystemAPI.GetBufferTypeHandle<Child>(true),
                ChildLookup = SystemAPI.GetBufferLookup<Child>(true),
                LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(),
                LocalTransform2DLookup = SystemAPI.GetComponentLookup<LocalTransform2D>(true),
                PostTransformMatrixLookup = SystemAPI.GetComponentLookup<PostTransformMatrix>(true),
                LocalToWorldLookup = SystemAPI.GetComponentLookup<LocalToWorld>(),
                LastSystemVersion = state.LastSystemVersion,
            };
            state.Dependency = childJob.ScheduleParallelByRef(parentsQuery, state.Dependency);
        }

        [BurstCompile]
        unsafe struct ComputeRootLocalToWorldJob : IJobChunk
        {
            [ReadOnly] public ComponentTypeHandle<LocalTransform2D> LocalTransform2DTypeHandleRO;
            [ReadOnly] public ComponentTypeHandle<PostTransformMatrix> PostTransformMatrixTypeHandleRO;
            public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW;
            public uint LastSystemVersion;

            public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
                in v128 chunkEnabledMask)
            {
                Assert.IsFalse(useEnabledMask);

                LocalTransform2D* chunk2DLocalTransforms =
                    (LocalTransform2D*)chunk.GetRequiredComponentDataPtrRO(ref LocalTransform2DTypeHandleRO);

                // 변경 감지(chunk.DidChange)를 통해 변환이 변경된 엔티티만 업데이트
                if (chunk.DidChange(ref LocalTransform2DTypeHandleRO, LastSystemVersion) ||
                    chunk.DidChange(ref PostTransformMatrixTypeHandleRO, LastSystemVersion))
                {
                    LocalToWorld* chunkLocalToWorlds =
                        (LocalToWorld*)chunk.GetRequiredComponentDataPtrRW(ref LocalToWorldTypeHandleRW);
                    PostTransformMatrix* chunkPostTransformMatrices =
                        (PostTransformMatrix*)chunk.GetComponentDataPtrRO(ref PostTransformMatrixTypeHandleRO);
                    if (chunkPostTransformMatrices != null)
                    {
                        for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
                        {
                            chunkLocalToWorlds[i].Value = math.mul(chunk2DLocalTransforms[i].ToMatrix(),
                                chunkPostTransformMatrices[i].Value);
                        }
                    }
                    else
                    {
                        for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i)
                        {
                            chunkLocalToWorlds[i].Value = chunk2DLocalTransforms[i].ToMatrix();
                        }
                    }
                }
            }
        }

        [BurstCompile]
        unsafe struct ComputeChildLocalToWorldJob : IJobChunk
        {
            [NativeDisableContainerSafetyRestriction]
            public ComponentLookup<LocalToWorld> LocalToWorldLookup;

            [ReadOnly] public EntityQueryMask LocalToWorldWriteGroupMask;
            [ReadOnly] public BufferTypeHandle<Child> ChildTypeHandle;
            [ReadOnly] public BufferLookup<Child> ChildLookup;
            public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW;
            [ReadOnly] public ComponentLookup<LocalTransform2D> LocalTransform2DLookup;
            [ReadOnly] public ComponentLookup<PostTransformMatrix> PostTransformMatrixLookup;
            public uint LastSystemVersion;

            public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
                in v128 chunkEnabledMask)
            {
                Assert.IsFalse(useEnabledMask);

                bool updateChildrenTransform = chunk.DidChange(ref ChildTypeHandle, LastSystemVersion);
                BufferAccessor<Child> chunkChildBuffers = chunk.GetBufferAccessor(ref ChildTypeHandle);
                updateChildrenTransform = updateChildrenTransform ||
                                          chunk.DidChange(ref LocalToWorldTypeHandleRW, LastSystemVersion);
                LocalToWorld* chunkLocalToWorlds =
                    (LocalToWorld*)chunk.GetRequiredComponentDataPtrRO(ref LocalToWorldTypeHandleRW);
                for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)
                {
                    var localToWorld = chunkLocalToWorlds[i].Value;
                    var children = chunkChildBuffers[i];
                    for (int j = 0, childCount = children.Length; j < childCount; j++)
                    {
                        ChildLocalToWorldFromTransformMatrix(localToWorld, children[j].Value, updateChildrenTransform);
                    }
                }
            }

            void ChildLocalToWorldFromTransformMatrix(in float4x4 parentLocalToWorld, Entity childEntity,
                bool updateChildrenTransform)
            {
                updateChildrenTransform = updateChildrenTransform
                                          || PostTransformMatrixLookup.DidChange(childEntity, LastSystemVersion)
                                          || LocalTransform2DLookup.DidChange(childEntity, LastSystemVersion);

                float4x4 localToWorld;
                if (updateChildrenTransform && LocalToWorldWriteGroupMask.MatchesIgnoreFilter(childEntity))
                {
                    var localTransform2D = LocalTransform2DLookup[childEntity];
                    localToWorld = math.mul(parentLocalToWorld, localTransform2D.ToMatrix());
                    if (PostTransformMatrixLookup.HasComponent(childEntity))
                    {
                        localToWorld = math.mul(localToWorld, PostTransformMatrixLookup[childEntity].Value);
                    }

                    LocalToWorldLookup[childEntity] = new LocalToWorld { Value = localToWorld };
                }
                else
                {
                    localToWorld = LocalToWorldLookup[childEntity].Value;
                    updateChildrenTransform = LocalToWorldLookup.DidChange(childEntity, LastSystemVersion);
                }

                if (ChildLookup.TryGetBuffer(childEntity, out DynamicBuffer<Child> children))
                {
                    for (int i = 0, childCount = children.Length; i < childCount; i++)
                    {
                        ChildLocalToWorldFromTransformMatrix(localToWorld, children[i].Value, updateChildrenTransform);
                    }
                }
            }
        }
    }
}

14. StateChange

ConfigAuthoring.cs

Unity에서 해당 클래스가 포함된 게임 오브젝트가 속한 엔티티로 컴포넌트를 변환하는 역할을 합니다.

ConfigAuthoring은 주로 Baker 클래스를 통해 엔티티와 연결됩니다.

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

namespace HelloCube.StateChange
{
    public class ConfigAuthoring : MonoBehaviour
    {
        public GameObject Prefab;
        public uint Size;
        public float Radius;
        public Mode Mode;

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

                AddComponent(entity, new Config
                {
                    Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic),
                    Size = authoring.Size,
                    Radius = authoring.Radius,
                    Mode = authoring.Mode,
                });
                AddComponent<Hit>(entity);
#if UNITY_EDITOR
                AddComponent<StateChangeProfilerModule.FrameData>(entity);
#endif
            }
        }
    }

    // Config는 IComponentData를 구현한 구조체로, ECS의 컴포넌트 데이터로 사용
    public struct Config : IComponentData
    {
        public Entity Prefab;
        public uint Size;
        public float Radius;
        public Mode Mode;
    }

    // Hit는 엔티티의 충돌 상태를 추적하는 IComponentData입니다.
    // Value는 충돌 위치를 저장하는 float3 타입이고,
    // HitChanged는 충돌 상태가 변경되었는지 여부를 나타내는 bool 타입
    public struct Hit : IComponentData
    {
        public float3 Value;
        public bool HitChanged;
    }

    // Spin은 엔티티가 회전하는 상태를 추적하는 컴포넌트
    // IsSpinning은 회전 여부를 나타내는 bool 타입이며,
    // IEnableableComponent 인터페이스를 구현하여 이 컴포넌트를 활성화하거나 비활성화
    public struct Spin : IComponentData, IEnableableComponent
    {
        public bool IsSpinning;
    }

    // Mode는 엔티티의 동작 모드를 정의하는 열거형
    public enum Mode
    {
        VALUE = 1, // VALUE: 일반적인 값 모드.
        STRUCTURAL_CHANGE = 2, // STRUCTURAL_CHANGE: 구조적 변경 모드.
        ENABLEABLE_COMPONENT = 3 // ENABLEABLE_COMPONENT: 활성화 가능한 컴포넌트 모드.
    }
}

InputSystem.cs

Unity의 ECS(엔티티 컴포넌트 시스템)를 사용하여 입력 시스템을 구현

마우스 클릭으로 발생한 충돌을 감지하고, 그 충돌 지점을 Hit 컴포넌트에 저장하는 기능을 수행합니다.

충돌 상태가 변경되었음을 HitChanged 플래그로 표시

using Unity.Burst;
using Unity.Entities;
using UnityEngine;

namespace HelloCube.StateChange
{
    public partial struct InputSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Hit>();
            state.RequireForUpdate<Config>();
            state.RequireForUpdate<ExecuteStateChange>();
        }

        public void OnUpdate(ref SystemState state)
        {
            var hit = SystemAPI.GetSingletonRW<Hit>();
            hit.ValueRW.HitChanged = false;

            // 마우스 입력이 없거나, 메인 카메라가 존재하지 않는 경우 업데이트를 하지 않음
            if (Camera.main == null || !Input.GetMouseButton(0))
            {
                return;
            }
            // 마우스 포인터가 화면에서 가리키는 위치를 기준으로 카메라에서 시작되는 레이를 생성
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            // Plane을 사용하여 레이와 평면의 교차 여부를 확인
            // 레이가 평면과 교차하는 지점을 찾아 dist 변수에 그 거리 값을 저장
            if (new Plane(Vector3.up, 0f).Raycast(ray, out var dist))
            {
                hit.ValueRW.HitChanged = true;
                hit.ValueRW.Value = ray.GetPoint(dist);
            }
        }
    }
}

SetStateSystem.cs

사용자가 클릭한 위치(Hit)와 설정된 반경(Radius)을 기준으로 엔티티들의 상태를 변경하는 로직

상태 변경 방식은 세 가지 모드 : VALUE, STRUCTURAL_CHANGE, ENABLEABLE_COMPONENT

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using Unity.Profiling.LowLevel.Unsafe;

namespace HelloCube.StateChange
{
    public partial struct SetStateSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Hit>();
            state.RequireForUpdate<Config>();
            state.RequireForUpdate<ExecuteStateChange>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var config = SystemAPI.GetSingleton<Config>();
            var hit = SystemAPI.GetSingleton<Hit>();

            // HitChanged 플래그가 false일 경우, 즉 이전에 충돌이 없거나 변경되지 않았다면 상태 변경을 하지 않습니다.
            if (!hit.HitChanged)
            {
#if UNITY_EDITOR
                // StateChangeProfilerModule.FrameData: 성능 데이터를 저장하는 구조체로,
                // SetStatePerf와 같은 성능 정보를 기록하는 데 사용
                SystemAPI.GetSingletonRW<StateChangeProfilerModule.FrameData>().ValueRW.SetStatePerf = 0;
#endif
                return;
            }

            var radiusSq = config.Radius * config.Radius;
            var ecbSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();

            state.Dependency.Complete();
            var before = ProfilerUnsafeUtility.Timestamp;

            if (config.Mode == Mode.VALUE)
            {
                // SetValueJob은 각 엔티티의 LocalTransform과 Hit 위치를 비교하여,
                // 엔티티가 Hit 위치와 반지름 내에 있을 경우 색상과 회전 여부를 적용
                new SetValueJob
                {
                    RadiusSq = radiusSq,
                    Hit = hit.Value
                }.ScheduleParallel();
            }
            else if (config.Mode == Mode.STRUCTURAL_CHANGE)
            {
                // Hit 위치와 반지름 내에 있는 엔티티에 Spin 컴포넌트를 추가 및 제거합니다.
                // 그리고 그 엔티티의 색상을 빨간색으로 바꿉니다.
                new AddSpinJob
                {
                    RadiusSq = radiusSq,
                    Hit = hit.Value,
                    ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()
                }.ScheduleParallel();

                new RemoveSpinJob
                {
                    RadiusSq = radiusSq,
                    Hit = hit.Value,
                    ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()
                }.ScheduleParallel();
            }
            else if (config.Mode == Mode.ENABLEABLE_COMPONENT)
            {
                // Hit 위치와 반지름 내에 있는 엔티티에서 Spin 컴포넌트를 활성화/비활성화합니다.
                new EnableSpinJob
                {
                    RadiusSq = radiusSq,
                    Hit = hit.Value,
                    ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()
                }.ScheduleParallel();

                new DisableSpinJob
                {
                    RadiusSq = radiusSq,
                    Hit = hit.Value,
                }.ScheduleParallel();
            }

            state.Dependency.Complete();
            var after = ProfilerUnsafeUtility.Timestamp;

#if UNITY_EDITOR
            // profiling
            var conversionRatio = ProfilerUnsafeUtility.TimestampToNanosecondsConversionRatio;
            var elapsed = (after - before) * conversionRatio.Numerator / conversionRatio.Denominator;
            SystemAPI.GetSingletonRW<StateChangeProfilerModule.FrameData>().ValueRW.SetStatePerf = elapsed;
#endif
        }
    }

    [BurstCompile]
    partial struct SetValueJob : IJobEntity
    {
        public float RadiusSq;
        public float3 Hit;

        void Execute(ref URPMaterialPropertyBaseColor color, ref Spin spin, in LocalTransform transform)
        {
            if (math.distancesq(transform.Position, Hit) <= RadiusSq)
            {
                color.Value = (Vector4)Color.red;
                spin.IsSpinning = true;
            }
            else
            {
                color.Value = (Vector4)Color.white;
                spin.IsSpinning = false;
            }
        }
    }

    [WithNone(typeof(Spin))]
    [BurstCompile]
    partial struct AddSpinJob : IJobEntity
    {
        public float RadiusSq;
        public float3 Hit;
        public EntityCommandBuffer.ParallelWriter ECB;

        void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform,
            [ChunkIndexInQuery] int chunkIndex)
        {
            // If cube is inside the hit radius.
            if (math.distancesq(transform.Position, Hit) <= RadiusSq)
            {
                color.Value = (Vector4)Color.red;
                ECB.AddComponent<Spin>(chunkIndex, entity);
            }
        }
    }

    [WithAll(typeof(Spin))]
    [BurstCompile]
    partial struct RemoveSpinJob : IJobEntity
    {
        public float RadiusSq;
        public float3 Hit;
        public EntityCommandBuffer.ParallelWriter ECB;

        void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, [ChunkIndexInQuery] int chunkIndex)
        {
            // If cube is NOT inside the hit radius.
            if (math.distancesq(transform.Position, Hit) > RadiusSq)
            {
                color.Value = (Vector4)Color.white;
                ECB.RemoveComponent<Spin>(chunkIndex, entity);
            }
        }
    }

    [WithNone(typeof(Spin))]
    [BurstCompile]
    public partial struct EnableSpinJob : IJobEntity
    {
        public float RadiusSq;
        public float3 Hit;
        public EntityCommandBuffer.ParallelWriter ECB;

        void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform,
            [ChunkIndexInQuery] int chunkIndex)
        {
            // If cube is inside the hit radius.
            if (math.distancesq(transform.Position, Hit) <= RadiusSq)
            {
                color.Value = (Vector4)Color.red;
                ECB.SetComponentEnabled<Spin>(chunkIndex, entity, true);
            }
        }
    }

    [BurstCompile]
    public partial struct DisableSpinJob : IJobEntity
    {
        public float RadiusSq;
        public float3 Hit;

        void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, EnabledRefRW<Spin> spinnerEnabled)
        {
            // If cube is NOT inside the hit radius.
            if (math.distancesq(transform.Position, Hit) > RadiusSq)
            {
                color.Value = (Vector4)Color.white;
                spinnerEnabled.ValueRW = false;
            }
        }
    }
}

CubeSpawnSystem.cs

Config 컴포넌트의 설정에 따라 큐브를 생성하고, 해당 큐브들에 대한 상태를 설정하는 작업

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

namespace HelloCube.StateChange
{
    public partial struct CubeSpawnSystem : ISystem
    {
        Config priorConfig;

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Config>();
            state.RequireForUpdate<ExecuteStateChange>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var config = SystemAPI.GetSingleton<Config>();

            // Config를 가져와서 이전의 Config와 비교합니다.
            // ConfigEquals 함수는 이전 설정과 새로운 설정을 비교하여,설정에 차이가 없으면 큐브를 재생성하지 않도록 합니다.
            if (ConfigEquals(priorConfig, config))
            {
                return;
            }
            priorConfig = config;

            // URPMaterialPropertyBaseColor 컴포넌트를 가진 모든 엔티티들을 파괴하는 작업을 수행
            var query = SystemAPI.QueryBuilder().WithAll<URPMaterialPropertyBaseColor>().Build();
            state.EntityManager.DestroyEntity(query);

            // Prefab을 기반으로 큐브를 생성
            var entities = state.EntityManager.Instantiate(config.Prefab, (int)(config.Size * config.Size), Allocator.Temp);

            var center = (config.Size - 1) / 2f;
            int i = 0;
            foreach (var transform in SystemAPI.Query<RefRW<LocalTransform>>())
            {
                transform.ValueRW.Scale = 1;
                transform.ValueRW.Position.x = (i % config.Size - center) * 1.5f;
                transform.ValueRW.Position.z = (i / config.Size - center) * 1.5f;
                i++;
            }

            var spinQuery = SystemAPI.QueryBuilder().WithAll<Spin>().Build();

            if (config.Mode == Mode.VALUE)
            {
                state.EntityManager.AddComponent<Spin>(query);
            }
            else if (config.Mode == Mode.ENABLEABLE_COMPONENT)
            {
                state.EntityManager.AddComponent<Spin>(query);
                state.EntityManager.SetComponentEnabled<Spin>(spinQuery, false);
            }
        }

        bool ConfigEquals(Config c1, Config c2)
        {
            return c1.Size == c2.Size && c1.Radius == c2.Radius && c1.Mode == c2.Mode;
        }
    }
}

댓글 달기

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

위로 스크롤