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

