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 }
DirectoryInitSystem
는 GameObject
에서 Directory
라는 이름의 오브젝트를 찾아
그 안에 포함된 RotatorPrefab
과 RotationToggle
을 가져와 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를 사용하여 고정 주기로 프로젝트일을 생성하는 시스템을 구현한 코드
FixedRateSpawnerSystem
은 FixedStepSimulationSystemGroup
에서 업데이트되며, 이는 고정된 시간 간격에 따라 시스템이 실행되도록 보장
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, }); } } } }