DOTS – EntitesSample (2)

8. HelloCube_GameObjectSync

Directory.cs

using UnityEngine;
using UnityEngine.UI;

namespace HelloCube.GameObjectSync
{
    // "Directory"는 게임 오브젝트 프리팹과 관리되는 객체들을 참조하는 중앙 장소로 작동합니다.
    // 그런 다음 시스템들은 이곳에서 관리되는 객체들에 대한 참조를 얻을 수 있습니다.
    // (큰 프로젝트에서는 모든 관리되는 객체를 한 곳에 모으는 것이 비효율적일 수 있기 때문에,
    // "디렉토리"가 하나 이상 필요할 수도 있습니다.)

    public class Directory : MonoBehaviour
    {
        public GameObject RotatorPrefab;
        public Toggle RotationToggle;
    }
}

Directory 클래스는 게임 오브젝트 및 UI 요소에 대한 중앙 관리를 담당합니다.

여러 시스템이 이 클래스를 통해 게임 오브젝트나 UI 요소에 접근할 수 있기 때문에 객체 관리가 용이해집니다.

프로젝트가 커질 경우 Directory 클래스를 통해 관리되는 객체들이 많아질 수 있습니다.

이때 더 많은 디렉토리 클래스를 만들어 객체들을 분류할 수 있습니다.

예를 들어, 하나는 UI 관련 객체를 관리하고, 다른 하나는 게임 로직 관련 객체를 관리하는 식으로 확장할 수 있습니다.

DirectoryInitSystem.cs

using System;
using Unity.Entities;
using UnityEngine;
using UnityEngine.UI;
using Unity.Burst;

namespace HelloCube.GameObjectSync
{
#if !UNITY_DISABLE_MANAGED_COMPONENTS
    public partial struct DirectoryInitSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // 씬이 로드된 후에만 업데이트가 필요하므로,
            // 씬에서 적어도 하나의 컴포넌트 타입이 로드되었음을 요구해야 합니다.

            state.RequireForUpdate<ExecuteGameObjectSync>();
        }

        public void OnUpdate(ref SystemState state)
        {
            // 시스템을 일시적으로 비활성화하여, 코드가 실행되는 동안 무한 루프나 불필요한 호출을 방지
            state.Enabled = false;

            // GameObject.Find("Directory")로 씬에서 "Directory"라는 이름의 GameObject를 찾고, 이를 directory 변수로 저장
            var go = GameObject.Find("Directory");
            if (go == null)
            {
                throw new Exception("GameObject 'Directory' not found.");
            }

            var directory = go.GetComponent<Directory>();

            var directoryManaged = new DirectoryManaged();
            directoryManaged.RotatorPrefab = directory.RotatorPrefab;
            directoryManaged.RotationToggle = directory.RotationToggle;

            // state.EntityManager.CreateEntity()로 새 엔티티를 생성하고
            // 생성한 엔티티에 DirectoryManaged 데이터를 추가
            var entity = state.EntityManager.CreateEntity();
            state.EntityManager.AddComponentData(entity, directoryManaged);
        }
    }

    public class DirectoryManaged : IComponentData
    {
        public GameObject RotatorPrefab;
        public Toggle RotationToggle;

        // IComponentData 클래스는 반드시 기본 생성자(파라미터가 없는 생성자)를 가져야함
        public DirectoryManaged()
        {
        }
    }
#endif
}

DirectoryInitSystemGameObject에서 Directory라는 이름의 오브젝트를 찾아

그 안에 포함된 RotatorPrefabRotationToggle을 가져와 IComponentData로 처리하는 역할을 합니다.

RotatorInitSystem.cs

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

namespace HelloCube.GameObjectSync
{
#if !UNITY_DISABLE_MANAGED_COMPONENTS
    [UpdateInGroup(typeof(InitializationSystemGroup))]
    public partial struct RotatorInitSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<DirectoryManaged>();
            state.RequireForUpdate<ExecuteGameObjectSync>();
        }

        // 이 OnUpdate는 관리되는 객체를 접근하므로, 버스트 컴파일을 할 수 없습니다.
        // 관리되는 객체(GameObject)를 사용하기 때문에 [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var directory = SystemAPI.ManagedAPI.GetSingleton<DirectoryManaged>();
            var ecb = new EntityCommandBuffer(Allocator.Temp);

            // 프리팹에서 관련된 GameObject를 인스턴스화합니다.
            foreach (var (goPrefab, entity) in SystemAPI
                .Query<RotationSpeed>()
                .WithNone<RotatorGO>()
                .WithEntityAccess())
            {
                var go = GameObject.Instantiate(directory.RotatorPrefab); // 관리되는 객체

                // 엔티티에 구성 요소를 반복적으로 추가할 수 없으므로 ECB를 통해서 변경합니다.
                // ECS의 성능에 영향을 주지 않도록 하며, 엔티티의 변환을 안전하게 처리
                ecb.AddComponent(entity, new RotatorGO(go));
            }

            ecb.Playback(state.EntityManager);
        }
    }

    public class RotatorGO : IComponentData
    {
        public GameObject Value;

        public RotatorGO(GameObject value)
        {
            Value = value;
        }

        // 모든 IComponentData 클래스에는 매개변수가 없는 생성자가 있어야 합니다.
        public RotatorGO()
        {
        }
    }
#endif
}

RotationSystem.cs

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

namespace HelloCube.GameObjectSync
{
#if !UNITY_DISABLE_MANAGED_COMPONENTS
    public partial struct RotationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<DirectoryManaged>();
            state.RequireForUpdate<ExecuteGameObjectSync>();
        }

        // [BurstCompile]
        // 이 OnUpdate는 관리되는 객체에 액세스하므로 버스트 컴파일할 수 없습니다.
        public void OnUpdate(ref SystemState state)
        {
            var directory = SystemAPI.ManagedAPI.GetSingleton<DirectoryManaged>();
            if (!directory.RotationToggle.isOn)
            {
                return;
            }

            float deltaTime = SystemAPI.Time.DeltaTime;

            foreach (var (transform, speed, go) in
                     SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>, RotatorGO>())
            {
                transform.ValueRW = transform.ValueRO.RotateY(
                    speed.ValueRO.RadiansPerSecond * deltaTime);

                // 관련된 GameObject의 변환을 일치하도록 업데이트합니다.
                go.Value.transform.rotation = transform.ValueRO.Rotation;
            }
        }
    }
#endif
}

9. HelloCube_CrossQuery

VelocityAuthoring.cs

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

namespace HelloCube.CrossQuery
{
    public class VelocityAuthoring : MonoBehaviour
    {
        public Vector3 Value;

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

                Velocity component = default(Velocity);
                component.Value = authoring.Value;

                AddComponent(entity, component);
            }
        }
    }

    public struct Velocity : IComponentData
    {
        public float3 Value;
    }

}

MoveSystem.cs

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

namespace HelloCube.CrossQuery
{
    public partial struct MoveSystem : ISystem
    {
        public float moveTimer;

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

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

            moveTimer += dt;

            // periodically reverse direction and reset timer
            bool flip = false;
            if (moveTimer > 3.0f)
            {
                moveTimer = 0;
                flip = true;
            }

            foreach (var (transform, velocity) in SystemAPI.Query<RefRW<LocalTransform>, RefRW<Velocity>>())
            {
                if (flip)
                {
                    velocity.ValueRW.Value *= -1;
                }

                // move
                transform.ValueRW.Position += velocity.ValueRO.Value * dt;
            }
        }
    }
}

DefaultColorAuthoring.cs

DefaultColor 컴포넌트의 데이터를 편리하게 설정할 수 있도록 도와주는 역할

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

namespace HelloCube.CrossQuery
{
    public class DefaultColorAuthoring : MonoBehaviour
    {
        public Color WhenNotColliding;

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

                DefaultColor component = default(DefaultColor);
                component.Value = (Vector4)authoring.WhenNotColliding;

                AddComponent(entity, component);
            }
        }
    }

    // Color는 RGBA 값을 가진 float 타입의 데이터를 사용하지만, 
    // ECS의 IComponentData 구조체는 float4 타입을 사용하여 데이터를 저장
    public struct DefaultColor : IComponentData
    {
        public float4 Value;
    }
}

SpawnSystem.cs

20개의 박스를 생성하고, 그들의 위치와 속성(색상, 속도 등)을 초기화

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

namespace HelloCube.CrossQuery
{
    [UpdateInGroup(typeof(InitializationSystemGroup))]
    [BurstCompile]
    public partial struct SpawnSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<PrefabCollection>();
            state.RequireForUpdate<ExecuteCrossQuery>();
        }

        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            state.Enabled = false;
            var prefabCollection = SystemAPI.GetSingleton<PrefabCollection>();

            // boxes 생성
            state.EntityManager.Instantiate(prefabCollection.Box, 20, Allocator.Temp);

            // init the newly spawned boxes
            int i = 0;
            foreach (var (velocity, trans, defaultColor, colorProperty) in 
                SystemAPI.Query<RefRW<Velocity>, RefRW<LocalTransform>,
                RefRW<DefaultColor>, RefRW<URPMaterialPropertyBaseColor>>())
            {
                if (i < 10)
                {
                    // black box on left
                    velocity.ValueRW.Value = new float3(2, 0, 0);
                    var verticalOffset = i * 2;
                    trans.ValueRW.Position = new float3(-3, -8 + verticalOffset, 0);
                    defaultColor.ValueRW.Value = new float4(0, 0, 0, 1);
                    colorProperty.ValueRW.Value = new float4(0, 0, 0, 1);
                }
                else
                {
                    // white box on right
                    velocity.ValueRW.Value = new float3(-2, 0, 0);
                    var verticalOffset = (i - 10) * 2;
                    trans.ValueRW.Position = new float3(3, -8 + verticalOffset, 0);
                    defaultColor.ValueRW.Value = new float4(1, 1, 1, 1);
                    colorProperty.ValueRW.Value = new float4(1, 1, 1, 1);
                }

                i++;
            }
        }
    }
}

PrefabCollectionAuthoring.cs

Box 프리팹을 IComponentData 형태로 엔티티에 저장하고, 이를 Baker 클래스를 통해 ECS (Entity Component System)에서 사용할 수 있도록 설정

using Unity.Entities;
using UnityEngine;

namespace HelloCube.CrossQuery
{
    // Box 프리팹을 가져와 ECS 구조에 맞게 변환하고, 이 정보를 ECS 시스템에서 다른 작업에 활용할 수 있도록 준비
    public class PrefabCollectionAuthoring : MonoBehaviour
    {
        public GameObject Box;

        // MonoBehaviour를 IComponentData로 변환하는 작업을 담당
        class Baker : Baker<PrefabCollectionAuthoring>
        {

            public override void Bake(PrefabCollectionAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.None);

                PrefabCollection component = default;
                component.Box = GetEntity(authoring.Box, TransformUsageFlags.Dynamic);

                AddComponent(entity, component);
            }
        }
    }

    public struct PrefabCollection : IComponentData
    {
        public Entity Box;
    }
}

CollisionSystem.cs

1번 – 쉬운 방법

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

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var boxQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform, DefaultColor, URPMaterialPropertyBaseColor>().Build();

            // 충돌 감지를 처리하는 두 가지 방법 중 하나를 선택하는 코드
            // 주석 처리된 if false 블록은 복잡한 방법을 사용
            // CollisionJob을 병렬로 실행하는 방법을 정의
            // ToArchetypeChunkArray는 다른 chunk들을 배열로 변환하여,
            // 여러 박스들 간의 충돌을 병렬로 처리할 수 있도록 합니다.
            // 복잡한 해결책으로, 박스 컴포넌트들을 임시 복사하지 않고, ArchetypeChunk를 사용하여 병렬로 처리하는 방식입니다.
            // 그러나 이 방법은 더 복잡하고, IJobChunk를 사용하여 병렬화를 최적화한 방식입니다.
#if false
            // More complex solution, but it avoids creating temporary copies of the box components
            new CollisionJob
            {
                LocalTransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(true),
                DefaultColorTypeHandle = SystemAPI.GetComponentTypeHandle<DefaultColor>(true),
                BaseColorTypeHandle = SystemAPI.GetComponentTypeHandle<URPMaterialPropertyBaseColor>(),
                EntityTypeHandle = SystemAPI.GetEntityTypeHandle(),
                OtherChunks = boxQuery.ToArchetypeChunkArray(state.WorldUpdateAllocator)
            }.ScheduleParallel(boxQuery, state.Dependency).Complete();
#else
            // 간단한 해결책이지만, 모든 박스의 변환 정보와 엔티티 ID의 임시 복사본을 생성해야 합니다.
            var boxTransforms = boxQuery.ToComponentDataArray<LocalTransform>(Allocator.Temp);
            var boxEntities = boxQuery.ToEntityArray(Allocator.Temp);

            foreach (var (transform, defaultColor, color, entity) in
                     SystemAPI.Query<RefRO<LocalTransform>, RefRO<DefaultColor>, RefRW<URPMaterialPropertyBaseColor>>()
                         .WithEntityAccess())
            {
                // 박스의 색상을 기본값으로 리셋합니다
                color.ValueRW.Value = defaultColor.ValueRO.Value;

                // 이 박스가 다른 박스와 교차하면 색상을 변경합니다.
                for (int i = 0; i < boxTransforms.Length; i++)
                {
                    var otherEnt = boxEntities[i];
                    var otherTrans = boxTransforms[i];

                    // 박스는 자기 자신과 교차해서는 안 되므로, 다른 엔티티의 ID가 현재 엔티티의 ID와 일치하는지 확인합니다.
                    if (entity != otherEnt && math.distancesq(transform.ValueRO.Position, otherTrans.Position) < 1)
                    {
                        color.ValueRW.Value.y = 0.5f; // set green channel
                        break;
                    }
                }
            }
#endif
        }
    }

    [BurstCompile]
    public struct CollisionJob : IJobChunk
    {
        [ReadOnly] public ComponentTypeHandle<LocalTransform> LocalTransformTypeHandle;
        [ReadOnly] public ComponentTypeHandle<DefaultColor> DefaultColorTypeHandle;
        public ComponentTypeHandle<URPMaterialPropertyBaseColor> BaseColorTypeHandle;
        [ReadOnly] public EntityTypeHandle EntityTypeHandle;

        [ReadOnly] public NativeArray<ArchetypeChunk> OtherChunks;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
            in v128 chunkEnabledMask)
        {
            var transforms = chunk.GetNativeArray(ref LocalTransformTypeHandle);
            var defaultColors = chunk.GetNativeArray(ref DefaultColorTypeHandle);
            var baseColors = chunk.GetNativeArray(ref BaseColorTypeHandle);
            var entities = chunk.GetNativeArray(EntityTypeHandle);

            for (int i = 0; i < transforms.Length; i++)
            {
                var transform = transforms[i];
                var baseColor = baseColors[i];
                var entity = entities[i];

                // reset to default color
                baseColor.Value = defaultColors[i].Value;

                for (int j = 0; j < OtherChunks.Length; j++)
                {
                    var otherChunk = OtherChunks[j];
                    var otherTranslations = otherChunk.GetNativeArray(ref LocalTransformTypeHandle);
                    var otherEntities = otherChunk.GetNativeArray(EntityTypeHandle);

                    for (int k = 0; k < otherChunk.Count; k++)
                    {
                        var otherTranslation = otherTranslations[k];
                        var otherEntity = otherEntities[k];

                        if (entity != otherEntity && math.distancesq(transform.Position, otherTranslation.Position) < 1)
                        {
                            baseColor.Value.y = 0.5f; // set green channel
                            break;
                        }
                    }
                }

                baseColors[i] = baseColor;
            }
        }
    }
}

2번 방법

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

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var boxQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform, DefaultColor, URPMaterialPropertyBaseColor>().Build();

            // 충돌 감지를 처리하는 두 가지 방법 중 하나를 선택하는 코드
            // 주석 처리된 if false 블록은 복잡한 방법을 사용
            // CollisionJob을 병렬로 실행하는 방법을 정의
            // ToArchetypeChunkArray는 다른 chunk들을 배열로 변환하여,
            // 여러 박스들 간의 충돌을 병렬로 처리할 수 있도록 합니다.
            // 복잡한 해결책으로, 박스 컴포넌트들을 임시 복사하지 않고, ArchetypeChunk를 사용하여 병렬로 처리하는 방식입니다.
            // 그러나 이 방법은 더 복잡하고, IJobChunk를 사용하여 병렬화를 최적화한 방식입니다.
#if true
            // More complex solution, but it avoids creating temporary copies of the box components
            new CollisionJob
            {
                LocalTransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(true),
                DefaultColorTypeHandle = SystemAPI.GetComponentTypeHandle<DefaultColor>(true),
                BaseColorTypeHandle = SystemAPI.GetComponentTypeHandle<URPMaterialPropertyBaseColor>(),
                EntityTypeHandle = SystemAPI.GetEntityTypeHandle(),
                OtherChunks = boxQuery.ToArchetypeChunkArray(state.WorldUpdateAllocator)
            }.ScheduleParallel(boxQuery, state.Dependency).Complete();
#else
            // 간단한 해결책이지만, 모든 박스의 변환 정보와 엔티티 ID의 임시 복사본을 생성해야 합니다.
            var boxTransforms = boxQuery.ToComponentDataArray<LocalTransform>(Allocator.Temp);
            var boxEntities = boxQuery.ToEntityArray(Allocator.Temp);

            foreach (var (transform, defaultColor, color, entity) in
                     SystemAPI.Query<RefRO<LocalTransform>, RefRO<DefaultColor>, RefRW<URPMaterialPropertyBaseColor>>()
                         .WithEntityAccess())
            {
                // 박스의 색상을 기본값으로 리셋합니다
                color.ValueRW.Value = defaultColor.ValueRO.Value;

                // 이 박스가 다른 박스와 교차하면 색상을 변경합니다.
                for (int i = 0; i < boxTransforms.Length; i++)
                {
                    var otherEnt = boxEntities[i];
                    var otherTrans = boxTransforms[i];

                    // 박스는 자기 자신과 교차해서는 안 되므로, 다른 엔티티의 ID가 현재 엔티티의 ID와 일치하는지 확인합니다.
                    if (entity != otherEnt && math.distancesq(transform.ValueRO.Position, otherTrans.Position) < 1)
                    {
                        color.ValueRW.Value.y = 0.5f; // set green channel
                        break;
                    }
                }
            }
#endif
        }
    }

    [BurstCompile]
    public struct CollisionJob : IJobChunk
    {
        // LocalTransform 컴포넌트에 대한 핸들로, 각 엔티티의 위치, 회전 및 스케일 정보를 얻는 데 사용
        [ReadOnly] public ComponentTypeHandle<LocalTransform> LocalTransformTypeHandle;

        // DefaultColor 컴포넌트에 대한 핸들로, 각 엔티티의 기본 색상 정보를 얻는 데 사용
        [ReadOnly] public ComponentTypeHandle<DefaultColor> DefaultColorTypeHandle;

        // BaseColorTypeHandle: URPMaterialPropertyBaseColor 컴포넌트에 대한 핸들로,
        // 각 엔티티의 현재 색상 정보를 수정하는 데 사용
        public ComponentTypeHandle<URPMaterialPropertyBaseColor> BaseColorTypeHandle;

        // 엔티티에 대한 핸들로, 각 엔티티의 ID를 가져오는 데 사용됩니다.
        [ReadOnly] public EntityTypeHandle EntityTypeHandle;

        // 다른 청크들을 나타내는 NativeArray<ArchetypeChunk>입니다.
        // 충돌을 감지할 다른 엔티티들의 위치 정보를 포함
        [ReadOnly] public NativeArray<ArchetypeChunk> OtherChunks;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
        {
            // transforms: LocalTransform 컴포넌트에서 엔티티의 위치를 가져옵니다.
            var transforms = chunk.GetNativeArray(ref LocalTransformTypeHandle);
            // 각 엔티티의 기본 색상 (DefaultColor)을 가져옵니다.
            var defaultColors = chunk.GetNativeArray(ref DefaultColorTypeHandle);
            // 현재 색상 정보를 가져옵니다.
            var baseColors = chunk.GetNativeArray(ref BaseColorTypeHandle);
            // 각 엔티티의 ID를 가져옵니다.
            var entities = chunk.GetNativeArray(EntityTypeHandle);

            for (int i = 0; i < transforms.Length; i++)
            {
                var transform = transforms[i];
                var baseColor = baseColors[i];
                var entity = entities[i];

                // 색상을 기본 색상으로 초기화
                baseColor.Value = defaultColors[i].Value;

                for (int j = 0; j < OtherChunks.Length; j++)
                {
                    var otherChunk = OtherChunks[j];
                    var otherTranslations = otherChunk.GetNativeArray(ref LocalTransformTypeHandle);
                    var otherEntities = otherChunk.GetNativeArray(EntityTypeHandle);

                    for (int k = 0; k < otherChunk.Count; k++)
                    {
                        var otherTranslation = otherTranslations[k];
                        var otherEntity = otherEntities[k];

                        // 단순 거리를 비교하여 충돌을 확인
                        if (entity != otherEntity && math.distancesq(transform.Position, otherTranslation.Position) < 1)
                        {
                            baseColor.Value.y = 0.5f; // set green channel
                            break;
                        }
                    }
                }

                baseColors[i] = baseColor;
            }
        }
    }
}

10. RandomSpawn

ConfigAuthoring.cs

ConfigAuthoring 클래스는 Prefab 게임 오브젝트를 Config 컴포넌트 데이터로 변환하여 엔티티에 추가하는 역할을 합니다.

Config 컴포넌트는 Prefab의 엔티티 참조를 저장하여 다른 시스템에서 사용할 수 있도록 합니다.

using Unity.Entities;
using UnityEngine;

namespace HelloCube.RandomSpawn
{
    public class ConfigAuthoring : MonoBehaviour
    {
        public GameObject Prefab;

        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)
                });
            }
        }
    }

    public struct Config : IComponentData
    {
        public Entity Prefab;
    }
}

CubeAuthoring.cs

CubeAuthoring 클래스는 Cube와 NewSpawn 컴포넌트를 엔티티에 추가하는 역할을 합니다.

Cube와 NewSpawn은 각각 IComponentData 구조체로, 엔티티에 특정 데이터를 저장하기 위해 사용됩니다.

using Unity.Entities;
using UnityEngine;

namespace HelloCube.RandomSpawn
{
    public class CubeAuthoring : MonoBehaviour
    {
        public class Baker : Baker<CubeAuthoring>
        {
            public override void Bake(CubeAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent<Cube>(entity);
                AddComponent<NewSpawn>(entity);
            }
        }
    }

    public struct Cube : IComponentData
    {
    }

    public struct NewSpawn : IComponentData
    {
    }
}

MovementSystem.cs

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

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // EndSimulationEntityCommandBufferSystem은 Unity의 DOTS(Entity Component System, ECS)에서 중요한 역할을 하는 시스템으로,
            // 주로 엔티티의 상태를 변경하거나 삭제하는 작업을 효율적으로 처리하기 위해 사용됩니다.
            // 이 시스템은 엔티티의 변경 사항을 "커맨드 버퍼"에 기록하고, 그런 다음 해당 변경 사항을 실제로 적용
            var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

            new FallingCubeJob
            {
                Movement = new float3(0, SystemAPI.Time.DeltaTime * -20, 0),
                ECB = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()
            }.ScheduleParallel();
        }
    }

    [WithAll(typeof(Cube))]
    [BurstCompile]
    public partial struct FallingCubeJob : IJobEntity
    {
        public float3 Movement;
        public EntityCommandBuffer.ParallelWriter ECB;

        // 각 Cube 엔티티에 대해 Y축 방향으로 속도를 더해가며 떨어지게 하고,
        // Y 위치가 0 이하로 내려가면 해당 엔티티를 삭제합니다.
        void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref LocalTransform cubeTransform)
        {
            cubeTransform.Position += Movement;
            if (cubeTransform.Position.y < 0)
            {
                ECB.DestroyEntity(chunkIndex, entity); // 삭제
            }
        }
    }
}

SpawnSystem.cs

3D 공간에 무작위로 오브젝트를 스폰하는 시스템을 구현합니다. 이 시스템은 매 프레임마다 지정된 수의 오브젝트를 생성하고, 각 오브젝트를 무작위 위치에 배치

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

namespace HelloCube.RandomSpawn
{
    public partial struct SpawnSystem : ISystem
    {
        uint seedOffset;
        float spawnTimer;

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            const int count = 200;
            const float spawnWait = 0.05f; // 0.05 seconds

            spawnTimer -= SystemAPI.Time.DeltaTime;
            if (spawnTimer > 0)
            {
                return;
            }

            spawnTimer = spawnWait;

            // Remove the NewSpawn tag component from the entities spawned in the prior frame.
            var newSpawnQuery = SystemAPI.QueryBuilder().WithAll<NewSpawn>().Build();
            state.EntityManager.RemoveComponent<NewSpawn>(newSpawnQuery);

            // Spawn the boxes
            var prefab = SystemAPI.GetSingleton<Config>().Prefab;
            state.EntityManager.Instantiate(prefab, count, Allocator.Temp);

            // Every spawned box needs a unique seed, so the
            // seedOffset must be incremented by the number of boxes every frame.
            seedOffset += count;

            new RandomPositionJob
            {
                SeedOffset = seedOffset
            }.ScheduleParallel();
        }
    }

    [WithAll(typeof(NewSpawn))]
    [BurstCompile]
    partial struct RandomPositionJob : IJobEntity
    {
        public uint SeedOffset;

        public void Execute([EntityIndexInQuery] int index, ref LocalTransform transform)
        {
            // Random instances with similar seeds produce similar results, so to get proper
            // randomness here, we use CreateFromIndex, which hashes the seed.
            var random = Random.CreateFromIndex(SeedOffset + (uint)index);
            var xz = random.NextFloat2Direction() * 50;
            transform.Position = new float3(xz[0], 50, xz[1]);
        }
    }
}

11. FirstPersonController

InputSystem.cs

Unity의 ECS 시스템을 사용하여 입력을 처리하는 시스템을 구현

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

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

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            ref var inputState = ref SystemAPI.GetSingletonRW<InputState>().ValueRW;
            inputState.Horizontal = Input.GetAxisRaw("Horizontal");
            inputState.Vertical = Input.GetAxisRaw("Vertical");
            inputState.MouseX = Input.GetAxisRaw("Mouse X");
            inputState.MouseY = Input.GetAxisRaw("Mouse Y");
            inputState.Space = Input.GetKeyDown(KeyCode.Space);
        }
    }
}

ControllerAuthoring.cs

FirstPersonController 시스템을 위한 컴포넌트와 아우터(Authoring) 클래스를 정의하고 있습니다.

Unity의 ECS (Entity Component System)에서 입력 상태플레이어의 속성을 관리하는 역할을 합니다.

using Unity.Entities;
using UnityEngine;
using UnityEngine.Serialization;

namespace HelloCube.FirstPersonController
{
    public class ControllerAuthoring : MonoBehaviour
    {
        public float MouseSensitivity = 50.0f;
        public float PlayerSpeed = 5.0f;
        public float JumpSpeed = 5.0f;

        class Baker : Baker<ControllerAuthoring>
        {
            public override void Bake(ControllerAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent(entity, new Controller
                {
                    MouseSensitivity = authoring.MouseSensitivity,
                    PlayerSpeed = authoring.PlayerSpeed,
                    JumpSpeed = authoring.JumpSpeed,
                });
                AddComponent<InputState>(entity);
            }
        }
    }

    public struct InputState : IComponentData
    {
        public float Horizontal;
        public float Vertical;
        public float MouseX;
        public float MouseY;
        public bool Space;
    }

    public struct Controller : IComponentData
    {
        public float MouseSensitivity;
        public float PlayerSpeed;
        public float JumpSpeed;
        public float VerticalSpeed;
        public float CameraPitch;
    }
}

CameraSystem.cs

플레이어의 카메라를 컨트롤러의 위치와 회전에 동기화하는 CameraSystem을 정의합니다.

이를 통해 카메라가 1인칭 시점(First-Person View)으로 작동하도록 설정합니다.

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

namespace HelloCube.FirstPersonController
{
    [UpdateAfter(typeof(ControllerSystem))]
    public partial struct CameraSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Controller>();
            state.RequireForUpdate<ExecuteFirstPersonController>();
        }

        // 이 OnUpdate 메서드는 관리되는 객체(managed objects)에 접근하므로 Burst-컴파일을 사용할 수 없습니다.
        public void OnUpdate(ref SystemState state)
        {
            if (Camera.main != null)
            {
                var transformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true);
                var controllerEntity = SystemAPI.GetSingletonEntity<Controller>();
                var controller = SystemAPI.GetSingleton<Controller>();

                var controllerTransform = transformLookup[controllerEntity];
                var cameraTransform = Camera.main.transform; // managed object

                cameraTransform.position = controllerTransform.Position;
                // Unity의 ECS 및 수학 라이브러리를 활용하여 카메라의 회전을 계산하는 코드, 
                // 계산된 최종 회전값을 Unity 카메라의 Transform에 적용
                cameraTransform.rotation = math.mul(controllerTransform.Rotation, quaternion.RotateX(controller.CameraPitch));
            }
        }
    }
}

ControllerSystem.cs

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

namespace HelloCube.FirstPersonController
{
    [UpdateAfter(typeof(InputSystem))]
    public partial struct ControllerSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<InputState>();
            state.RequireForUpdate<ExecuteFirstPersonController>();
        }

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

            foreach (var (transform, controller) in
                     SystemAPI.Query<RefRW<LocalTransform>, RefRW<Controller>>())
            {
                // Move around with WASD
                var move = new float3(input.Horizontal, 0, input.Vertical);
                move = move * controller.ValueRO.PlayerSpeed * SystemAPI.Time.DeltaTime;
                move = math.mul(transform.ValueRO.Rotation, move);

                // Fall down / gravity
                controller.ValueRW.VerticalSpeed -= 10.0f * SystemAPI.Time.DeltaTime;
                controller.ValueRW.VerticalSpeed = math.max(-10.0f, controller.ValueRO.VerticalSpeed);
                move.y = controller.ValueRO.VerticalSpeed * SystemAPI.Time.DeltaTime;

                transform.ValueRW.Position += move;
                if (transform.ValueRO.Position.y < 0)
                {
                    // 바닥에 도달하면 Y축 위치를 0으로 고정
                    transform.ValueRW.Position *= new float3(1, 0, 1);
                }

                // Turn player
                // 마우스 X축 입력으로 플레이어의 좌우 회전 처리
                var turnPlayer = input.MouseX * controller.ValueRO.MouseSensitivity * SystemAPI.Time.DeltaTime;
                transform.ValueRW = transform.ValueRO.RotateY(turnPlayer);

                // Camera look up/down
                // 마우스 Y축 입력으로 카메라 상하 회전 처리
                var turnCam = -input.MouseY * controller.ValueRO.MouseSensitivity * SystemAPI.Time.DeltaTime;
                controller.ValueRW.CameraPitch += turnCam;

                // Jump
                // 스페이스바 입력 시 점프 속도 설정
                if (input.Space)
                {
                    controller.ValueRW.VerticalSpeed = controller.ValueRO.JumpSpeed;
                }
            }
        }
    }
}

12. FixedTimestep

VariableRateSpawnerSystem

  • 시스템 업데이트당 하나의 엔티티를 생성합니다.
  • 표시되는 프레임마다 한 번씩 업데이트됩니다.
    (프레임 속도에 의존적)

FixedRateSpawnerSystem

  • 시스템 업데이트당 하나의 엔티티를 생성합니다.
  • 업데이트 속도는 아래 슬라이더로 제어됩니다.
    (프레임 속도에 독립적)

ProjectileAuthoring.cs

ECS(Entity Component System)를 사용하여 Projectile(투사체) 엔티티를 정의하고 초기화하는 역할

Unity 에디터에서 투사체를 설정하고 ECS 시스템에서 사용할 수 있도록 변환

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

namespace HelloCube.FixedTimestep
{
    public class ProjectileAuthoring : MonoBehaviour
    {
        class Baker : Baker<ProjectileAuthoring>
        {
            public override void Bake(ProjectileAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent<Projectile>(entity);
            }
        }
    }

    public struct Projectile : IComponentData
    {
        public float SpawnTime;
        public float3 SpawnPos;
    }
}

MoveProjectilesSystem.cs

Burst 컴파일러를 사용하여 투사체 이동 및 수명 관리 시스템을 구현한 것

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

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

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

            new MoveJob
            {
                TimeSinceLoad = (float)SystemAPI.Time.ElapsedTime,
                ProjectileSpeed = 5.0f,
                ECBWriter = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()
            }.ScheduleParallel();
        }
    }

    // 작업(Job)을 병렬로 실행하여 모든 투사체를 이동시키고 수명을 검사
    [BurstCompile]
    public partial struct MoveJob : IJobEntity
    {
        public float TimeSinceLoad;
        public float ProjectileSpeed;
        public EntityCommandBuffer.ParallelWriter ECBWriter;

        // in 키워드는 읽기 전용으로 전달되는 매개변수\
        // [ChunkIndexInQuery] 엔티티가 속한 청크(Chunk)의 인덱스를 가져오는 매개변수
        void Execute(Entity projectileEntity, [ChunkIndexInQuery] int chunkIndex, ref LocalTransform transform, in Projectile projectile)
        {
            float aliveTime = TimeSinceLoad - projectile.SpawnTime;
            if (aliveTime > 5.0f)
            {
                ECBWriter.DestroyEntity(chunkIndex, projectileEntity); // 파괴
            }
            transform.Position.x = projectile.SpawnPos.x + aliveTime * ProjectileSpeed;
        }
    }
}

SliderHandler.cs

ECS(엔티티 컴포넌트 시스템)를 사용하여 고정 프레임 레이트를 설정하는 SliderHandler 스크립트

using Unity.Entities;
using UnityEngine;
using UnityEngine.UI;

namespace HelloCube.FixedTimestep
{
    public class SliderHandler : MonoBehaviour
    {
        public Text sliderValueText;

        public void OnSliderChange()
        {
            float fixedFps = GetComponent<Slider>().value;

            // 경고: World.DefaultGameObjectInjectionWorld에 접근하는 것은 비중요 프로젝트에서 잘못된 패턴입니다.
            // ECS와의 상호작용은 일반적으로 반대 방향으로 진행되어야 합니다. GameObjects가 ECS 데이터 및 코드를 접근하는 대신,
            // ECS 시스템이 GameObjects를 접근해야 합니다.
            // -------------------------------------------------------------------------------------
            // Unity의 ECS(Entity Component System)와 GameObject 간의 상호작용에 관한 것
            // 간단히 말해서, GameObject가 ECS 데이터를 접근하는 방식은 큰 프로젝트에서 문제를 일으킬 수 있다는 경고입니다.
            var fixedSimulationGroup = World.DefaultGameObjectInjectionWorld ?.GetExistingSystemManaged<FixedStepSimulationSystemGroup>();
            if (fixedSimulationGroup != null)
            {
                // The group timestep can be set at runtime:
                fixedSimulationGroup.Timestep = 1.0f / fixedFps;
                // The current timestep can also be retrieved:
                sliderValueText.text = $"{(int)(1.0f / fixedSimulationGroup.Timestep)} updates/sec";
            }
        }
    }
}

FixedRateSpawnerAuthoring.cs

ECS(Entity Component System)를 사용하여 고정 주기적으로 발사되는 발사기 시스템을 설정하는 스크립트

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

namespace HelloCube.FixedTimestep
{
    public class FixedRateSpawnerAuthoring : MonoBehaviour
    {
        public GameObject projectilePrefab;

        // Baker는 Baker<T>를 상속받은 클래스입니다.
        // Baker<T>는 MonoBehaviour에서 ECS로 데이터를 "베이킹(baking)"할 때 사용
        // FixedRateSpawnerAuthoring 데이터를 ECS의 엔티티와 컴포넌트로 변환하는 역할을 합니다.
        class Baker : Baker<FixedRateSpawnerAuthoring>
        {
            public override void Bake(FixedRateSpawnerAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.None);

                var spawnerData = new FixedRateSpawner
                {
                    // Prefab을 ECS 엔티티로 변환하여 Prefab 컴포넌트에 할당
                    Prefab = GetEntity(authoring.projectilePrefab, TransformUsageFlags.Dynamic),
                    SpawnPos = GetComponent<Transform>().position,
                };
                AddComponent(entity, spawnerData);
            }
        }
    }

    public struct FixedRateSpawner : IComponentData
    {
        public Entity Prefab;
        public float3 SpawnPos;
    }
}

DefaultRateSpawnerAuthoring.cs

FixedRateSpawnerAuthoring과 비슷한 방식으로, Unity ECS(Entity Component System)를 사용하여 기본 주기적으로 발사되는 발사기 시스템을 설정하는 스크립트

이 코드에서 다루는 발사기의 타이밍이 “고정된” 주기가 아니라 “기본” 주기를 따르며, 발사 타이밍이나 처리 방식이 시스템에 따라 다를 수 있다는 점

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

namespace HelloCube.FixedTimestep
{
    // Baker는 Baker<T>를 상속받아 MonoBehaviour를 ECS 시스템에 맞게 변환하는 역할을 합니다.
    // Bake 메서드 내에서 DefaultRateSpawnerAuthoring을 ECS 엔티티와 컴포넌트로 변환합니다.
    public class DefaultRateSpawnerAuthoring : MonoBehaviour
    {
        public GameObject projectilePrefab;

        class Baker : Baker<DefaultRateSpawnerAuthoring>
        {
            public override void Bake(DefaultRateSpawnerAuthoring authoring)
            {
                // DefaultRateSpawnerAuthoring 객체에 해당하는 ECS 엔티티를 생성
                var entity = GetEntity(TransformUsageFlags.None);

                var spawnerData = new DefaultRateSpawner
                {
                    Prefab = GetEntity(authoring.projectilePrefab, TransformUsageFlags.Dynamic),
                    SpawnPos = GetComponent<Transform>().position,
                };
                AddComponent(entity, spawnerData);
            }
        }
    }

    // DefaultRateSpawner는 IComponentData를 구현한 구조체
    public struct DefaultRateSpawner : IComponentData
    {
        public Entity Prefab;
        public float3 SpawnPos;
    }
}

DefaultRateSpawnerSystem.cs

주기적으로 발사를을 생성하는 시스템(DefaultRateSpawnerSystem) 코드

이 시스템은 특정 시간 간격마다 발사기를 통해 새로운 발사체(Projectile)을 생성하고, 생성된 발사체에 필요한 정보를 설정합니다.

주기적인 발사와 발사체의 위치 및 기타 데이터를 설정하는 로직을 포함합니다.

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

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

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

            foreach (var spawner in
                     SystemAPI.Query<RefRW<DefaultRateSpawner>>())
            {
                var projectileEntity = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);
                var spawnPos = spawner.ValueRO.SpawnPos;
                spawnPos.y += 0.3f * math.sin(5.0f * spawnTime);

                // SetComponent는 지정된 엔티티에 컴포넌트를 추가하거나 기존 컴포넌트의 값을 수정
                SystemAPI.SetComponent(projectileEntity, LocalTransform.FromPosition(spawnPos));
                SystemAPI.SetComponent(projectileEntity, new Projectile
                {
                    SpawnTime = spawnTime,
                    SpawnPos = spawnPos,
                });
            }
        }
    }
}

FixedRateSpawnerSystem.cs

Unity ECS를 사용하여 고정 주기로 프로젝트일을 생성하는 시스템을 구현한 코드

FixedRateSpawnerSystemFixedStepSimulationSystemGroup에서 업데이트되며, 이는 고정된 시간 간격에 따라 시스템이 실행되도록 보장

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

namespace HelloCube.FixedTimestep
{
    // Unity ECS를 사용하여 고정 주기로 프로젝트일을 생성하는 시스템을 구현한 것입니다.
    // FixedRateSpawnerSystem은 **FixedStepSimulationSystemGroup**에서 업데이트되며,
    // 이는 고정된 시간 간격에 따라 시스템이 실행되도록 보장
    //  -----------------------
    // FixedStepSimulationSystemGroup는 고정된 타임스텝(예: 0.02초 등)으로 시스템을 실행하도록 보장
    [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    public partial struct FixedRateSpawnerSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteFixedTimestep>();
        }

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

            foreach (var spawner in
                     SystemAPI.Query<RefRO<FixedRateSpawner>>())
            {
                var projectileEntity = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);
                var spawnPos = spawner.ValueRO.SpawnPos;
                spawnPos.y += 0.3f * math.sin(5.0f * spawnTime);

                SystemAPI.SetComponent(projectileEntity, LocalTransform.FromPosition(spawnPos));
                SystemAPI.SetComponent(projectileEntity, new Projectile
                {
                    SpawnTime = spawnTime,
                    SpawnPos = spawnPos,
                });
            }
        }
    }
}

댓글 달기

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

위로 스크롤