13. Custom2DTransforms
Transform2DAuthoring.cs
Transform2DAuthoring
클래스의 주요 목표는 기존 Unity의 Transform
시스템을 사용하지 않고,
ECS(Entity Component System)에서 동작하는 최적화된 커스텀 2D 트랜스폼 시스템을 구현하는 것
using System.Globalization; using UnityEngine; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; namespace HelloCube.CustomTransforms { public class Transform2DAuthoring : MonoBehaviour { class Baker : Baker<Transform2DAuthoring> { public override void Bake(Transform2DAuthoring authoring) { // 표준 Transform 컴포넌트가 추가되지 않도록 보장합니다. // TransformUsageFlags.ManualOverride를 사용하면 ECS가 해당 엔티티에 대해 // 기본 Transform 컴포넌트(Translation, Rotation, Scale 등)를 자동으로 추가하지 않도록 제어 // Unity의 표준 트랜스폼 처리 로직을 비활성화 // 커스텀 트랜스폼 시스템을 사용하면 2D 전용으로 최적화하여 불필요한 계산을 줄이고 성능을 향상 var entity = GetEntity(TransformUsageFlags.ManualOverride); AddComponent(entity, new LocalTransform2D { Scale = 1 }); AddComponent(entity, new LocalToWorld { Value = float4x4.Scale(1) }); var parentGO = authoring.transform.parent; if (parentGO != null) { AddComponent(entity, new Parent { Value = GetEntity(parentGO, TransformUsageFlags.None) }); } } } } // LocalToWorld 쓰기 그룹에 LocalTransform2D를 포함시킴으로써, // LocalTransform2D를 가진 엔티티는 표준 Transform 시스템에 의해 처리되지 않습니다. [WriteGroup(typeof(LocalToWorld))] public struct LocalTransform2D : IComponentData { public float2 Position; // 2D 공간에서의 위치를 나타내는 float2 public float Scale; // 엔티티의 크기를 나타내는 float public float Rotation; // 2D 공간에서의 회전을 나타내며, 단위는 degree(도) public override string ToString() { return $"Position={Position.ToString()} Rotation={Rotation.ToString()} Scale={Scale.ToString(CultureInfo.InvariantCulture)}"; } // 트랜스폼 데이터를 기반으로 4x4 변환 매트릭스를 생성 // math.radians(Rotation)으로 도(degree)를 라디안으로 변환 // float4x4.TRS를 사용하여 위치(Translation), 회전(Rotation), 스케일(Scale)을 포함하는 매트릭스를 생성 public float4x4 ToMatrix() { quaternion rotation = quaternion.RotateZ(math.radians(Rotation)); return float4x4.TRS(new float3(Position.xy, 0f), rotation, Scale); } } }
MovementSystem.cs
커스텀 2D 트랜스폼 데이터(LocalTransform2D
)를 사용하여 객체를 움직이고 크기 및 회전을 조정하는 시스템을 구현
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; namespace HelloCube.CustomTransforms { public partial struct MovementSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<LocalTransform2D>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { float rotation = SystemAPI.Time.DeltaTime * 180f; // Half a rotation every second (in degrees) float elapsedTime = (float) SystemAPI.Time.ElapsedTime; float xPosition = math.sin(elapsedTime) * 2f - 1f; float scale = math.sin(elapsedTime * 2f) + 1f; // math.sin(elapsedTime * 2f)를 사용하여 스케일이 0에서 2까지 진동 scale = scale <= 0.001f ? 0f : scale; foreach (var localTransform2D in SystemAPI .Query<RefRW<LocalTransform2D>>() .WithNone<Parent>()) { localTransform2D.ValueRW.Position.x = xPosition; localTransform2D.ValueRW.Rotation = localTransform2D.ValueRO.Rotation + rotation; localTransform2D.ValueRW.Scale = scale; } } } }
LocalToWorld2DSystem.cs
LocalTransform2D
데이터를 기반으로 각 엔티티의 월드 공간(LocalToWorld 변환 행렬)을 계산합니다.
추가적으로 부모-자식 관계를 고려하여 계층 구조를 통해 변환을 계산합니다.
using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Assertions; using Unity.Burst.Intrinsics; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using Unity.Transforms; namespace HelloCube.CustomTransforms { // 이 시스템은 LocalTransform2D를 가진 각 엔티티에 대해 변환 행렬(Transform Matrix)을 계산합니다. // 부모가 없는 루트 레벨/월드 공간 엔티티의 경우, // LocalToWorld는 엔티티의 LocalTransform2D에서 직접 계산될 수 있습니다. // 자식 엔티티의 경우, 각 고유 계층 구조를 재귀적으로 탐색하며, // 부모의 변환과 자식의 변환을 합성하여 각 자식의 LocalToWorld를 계산합니다. [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] // 이 시스템은 기본 월드와 에디터 월드에서 작동하도록 설정 [UpdateInGroup(typeof(TransformSystemGroup))] // TransformSystemGroup: 변환 계산과 관련된 시스템 그룹에서 실행 [UpdateAfter(typeof(ParentSystem))] // ParentSystem 이후에 업데이트: 부모-자식 관계가 올바르게 계산된 후 실행 [BurstCompile] public partial struct LocalToWorld2DSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<LocalTransform2D>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { // LocalTransform2D와 LocalToWorld를 가진 엔티티, 부모가 없는 루트 레벨 엔티티. var rootsQuery = SystemAPI.QueryBuilder() .WithAll<LocalTransform2D>() .WithAllRW<LocalToWorld>() .WithNone<Parent>().Build(); // LocalTransform2D와 자식(Child) 정보를 가진 엔티티.부모가 없는 상위 레벨 엔티티. var parentsQuery = SystemAPI.QueryBuilder() .WithAll<LocalTransform2D, Child>() .WithAllRW<LocalToWorld>() .WithNone<Parent>().Build(); var localToWorldWriteGroupMask = SystemAPI.QueryBuilder() .WithAll<LocalTransform2D, Parent>() .WithAllRW<LocalToWorld>().Build().GetEntityQueryMask(); // ComputeRootLocalToWorldJob을 실행하여 루트 엔티티의 LocalToWorld를 계산 var rootJob = new ComputeRootLocalToWorldJob { LocalTransform2DTypeHandleRO = SystemAPI.GetComponentTypeHandle<LocalTransform2D>(true), PostTransformMatrixTypeHandleRO = SystemAPI.GetComponentTypeHandle<PostTransformMatrix>(true), LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(), LastSystemVersion = state.LastSystemVersion, }; state.Dependency = rootJob.ScheduleParallelByRef(rootsQuery, state.Dependency); // ComputeChildLocalToWorldJob을 실행하여 자식 엔티티 계층을 따라가며 LocalToWorld를 계산 var childJob = new ComputeChildLocalToWorldJob { LocalToWorldWriteGroupMask = localToWorldWriteGroupMask, ChildTypeHandle = SystemAPI.GetBufferTypeHandle<Child>(true), ChildLookup = SystemAPI.GetBufferLookup<Child>(true), LocalToWorldTypeHandleRW = SystemAPI.GetComponentTypeHandle<LocalToWorld>(), LocalTransform2DLookup = SystemAPI.GetComponentLookup<LocalTransform2D>(true), PostTransformMatrixLookup = SystemAPI.GetComponentLookup<PostTransformMatrix>(true), LocalToWorldLookup = SystemAPI.GetComponentLookup<LocalToWorld>(), LastSystemVersion = state.LastSystemVersion, }; state.Dependency = childJob.ScheduleParallelByRef(parentsQuery, state.Dependency); } [BurstCompile] unsafe struct ComputeRootLocalToWorldJob : IJobChunk { [ReadOnly] public ComponentTypeHandle<LocalTransform2D> LocalTransform2DTypeHandleRO; [ReadOnly] public ComponentTypeHandle<PostTransformMatrix> PostTransformMatrixTypeHandleRO; public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW; public uint LastSystemVersion; public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { Assert.IsFalse(useEnabledMask); LocalTransform2D* chunk2DLocalTransforms = (LocalTransform2D*)chunk.GetRequiredComponentDataPtrRO(ref LocalTransform2DTypeHandleRO); // 변경 감지(chunk.DidChange)를 통해 변환이 변경된 엔티티만 업데이트 if (chunk.DidChange(ref LocalTransform2DTypeHandleRO, LastSystemVersion) || chunk.DidChange(ref PostTransformMatrixTypeHandleRO, LastSystemVersion)) { LocalToWorld* chunkLocalToWorlds = (LocalToWorld*)chunk.GetRequiredComponentDataPtrRW(ref LocalToWorldTypeHandleRW); PostTransformMatrix* chunkPostTransformMatrices = (PostTransformMatrix*)chunk.GetComponentDataPtrRO(ref PostTransformMatrixTypeHandleRO); if (chunkPostTransformMatrices != null) { for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i) { chunkLocalToWorlds[i].Value = math.mul(chunk2DLocalTransforms[i].ToMatrix(), chunkPostTransformMatrices[i].Value); } } else { for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; ++i) { chunkLocalToWorlds[i].Value = chunk2DLocalTransforms[i].ToMatrix(); } } } } } [BurstCompile] unsafe struct ComputeChildLocalToWorldJob : IJobChunk { [NativeDisableContainerSafetyRestriction] public ComponentLookup<LocalToWorld> LocalToWorldLookup; [ReadOnly] public EntityQueryMask LocalToWorldWriteGroupMask; [ReadOnly] public BufferTypeHandle<Child> ChildTypeHandle; [ReadOnly] public BufferLookup<Child> ChildLookup; public ComponentTypeHandle<LocalToWorld> LocalToWorldTypeHandleRW; [ReadOnly] public ComponentLookup<LocalTransform2D> LocalTransform2DLookup; [ReadOnly] public ComponentLookup<PostTransformMatrix> PostTransformMatrixLookup; public uint LastSystemVersion; public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { Assert.IsFalse(useEnabledMask); bool updateChildrenTransform = chunk.DidChange(ref ChildTypeHandle, LastSystemVersion); BufferAccessor<Child> chunkChildBuffers = chunk.GetBufferAccessor(ref ChildTypeHandle); updateChildrenTransform = updateChildrenTransform || chunk.DidChange(ref LocalToWorldTypeHandleRW, LastSystemVersion); LocalToWorld* chunkLocalToWorlds = (LocalToWorld*)chunk.GetRequiredComponentDataPtrRO(ref LocalToWorldTypeHandleRW); for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) { var localToWorld = chunkLocalToWorlds[i].Value; var children = chunkChildBuffers[i]; for (int j = 0, childCount = children.Length; j < childCount; j++) { ChildLocalToWorldFromTransformMatrix(localToWorld, children[j].Value, updateChildrenTransform); } } } void ChildLocalToWorldFromTransformMatrix(in float4x4 parentLocalToWorld, Entity childEntity, bool updateChildrenTransform) { updateChildrenTransform = updateChildrenTransform || PostTransformMatrixLookup.DidChange(childEntity, LastSystemVersion) || LocalTransform2DLookup.DidChange(childEntity, LastSystemVersion); float4x4 localToWorld; if (updateChildrenTransform && LocalToWorldWriteGroupMask.MatchesIgnoreFilter(childEntity)) { var localTransform2D = LocalTransform2DLookup[childEntity]; localToWorld = math.mul(parentLocalToWorld, localTransform2D.ToMatrix()); if (PostTransformMatrixLookup.HasComponent(childEntity)) { localToWorld = math.mul(localToWorld, PostTransformMatrixLookup[childEntity].Value); } LocalToWorldLookup[childEntity] = new LocalToWorld { Value = localToWorld }; } else { localToWorld = LocalToWorldLookup[childEntity].Value; updateChildrenTransform = LocalToWorldLookup.DidChange(childEntity, LastSystemVersion); } if (ChildLookup.TryGetBuffer(childEntity, out DynamicBuffer<Child> children)) { for (int i = 0, childCount = children.Length; i < childCount; i++) { ChildLocalToWorldFromTransformMatrix(localToWorld, children[i].Value, updateChildrenTransform); } } } } } }
14. StateChange
ConfigAuthoring.cs
Unity에서 해당 클래스가 포함된 게임 오브젝트가 속한 엔티티로 컴포넌트를 변환하는 역할을 합니다.
ConfigAuthoring은 주로 Baker 클래스를 통해 엔티티와 연결됩니다.
using Unity.Entities; using Unity.Mathematics; using UnityEngine; namespace HelloCube.StateChange { public class ConfigAuthoring : MonoBehaviour { public GameObject Prefab; public uint Size; public float Radius; public Mode Mode; class Baker : Baker<ConfigAuthoring> { public override void Bake(ConfigAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.None); AddComponent(entity, new Config { Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic), Size = authoring.Size, Radius = authoring.Radius, Mode = authoring.Mode, }); AddComponent<Hit>(entity); #if UNITY_EDITOR AddComponent<StateChangeProfilerModule.FrameData>(entity); #endif } } } // Config는 IComponentData를 구현한 구조체로, ECS의 컴포넌트 데이터로 사용 public struct Config : IComponentData { public Entity Prefab; public uint Size; public float Radius; public Mode Mode; } // Hit는 엔티티의 충돌 상태를 추적하는 IComponentData입니다. // Value는 충돌 위치를 저장하는 float3 타입이고, // HitChanged는 충돌 상태가 변경되었는지 여부를 나타내는 bool 타입 public struct Hit : IComponentData { public float3 Value; public bool HitChanged; } // Spin은 엔티티가 회전하는 상태를 추적하는 컴포넌트 // IsSpinning은 회전 여부를 나타내는 bool 타입이며, // IEnableableComponent 인터페이스를 구현하여 이 컴포넌트를 활성화하거나 비활성화 public struct Spin : IComponentData, IEnableableComponent { public bool IsSpinning; } // Mode는 엔티티의 동작 모드를 정의하는 열거형 public enum Mode { VALUE = 1, // VALUE: 일반적인 값 모드. STRUCTURAL_CHANGE = 2, // STRUCTURAL_CHANGE: 구조적 변경 모드. ENABLEABLE_COMPONENT = 3 // ENABLEABLE_COMPONENT: 활성화 가능한 컴포넌트 모드. } }
InputSystem.cs
Unity의 ECS(엔티티 컴포넌트 시스템)를 사용하여 입력 시스템을 구현
마우스 클릭으로 발생한 충돌을 감지하고, 그 충돌 지점을 Hit
컴포넌트에 저장하는 기능을 수행합니다.
충돌 상태가 변경되었음을 HitChanged
플래그로 표시
using Unity.Burst; using Unity.Entities; using UnityEngine; namespace HelloCube.StateChange { public partial struct InputSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<Hit>(); state.RequireForUpdate<Config>(); state.RequireForUpdate<ExecuteStateChange>(); } public void OnUpdate(ref SystemState state) { var hit = SystemAPI.GetSingletonRW<Hit>(); hit.ValueRW.HitChanged = false; // 마우스 입력이 없거나, 메인 카메라가 존재하지 않는 경우 업데이트를 하지 않음 if (Camera.main == null || !Input.GetMouseButton(0)) { return; } // 마우스 포인터가 화면에서 가리키는 위치를 기준으로 카메라에서 시작되는 레이를 생성 var ray = Camera.main.ScreenPointToRay(Input.mousePosition); // Plane을 사용하여 레이와 평면의 교차 여부를 확인 // 레이가 평면과 교차하는 지점을 찾아 dist 변수에 그 거리 값을 저장 if (new Plane(Vector3.up, 0f).Raycast(ray, out var dist)) { hit.ValueRW.HitChanged = true; hit.ValueRW.Value = ray.GetPoint(dist); } } } }
SetStateSystem.cs
사용자가 클릭한 위치(Hit
)와 설정된 반경(Radius
)을 기준으로 엔티티들의 상태를 변경하는 로직
상태 변경 방식은 세 가지 모드 : VALUE, STRUCTURAL_CHANGE, ENABLEABLE_COMPONENT
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; using UnityEngine; using Unity.Profiling.LowLevel.Unsafe; namespace HelloCube.StateChange { public partial struct SetStateSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<Hit>(); state.RequireForUpdate<Config>(); state.RequireForUpdate<ExecuteStateChange>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton<Config>(); var hit = SystemAPI.GetSingleton<Hit>(); // HitChanged 플래그가 false일 경우, 즉 이전에 충돌이 없거나 변경되지 않았다면 상태 변경을 하지 않습니다. if (!hit.HitChanged) { #if UNITY_EDITOR // StateChangeProfilerModule.FrameData: 성능 데이터를 저장하는 구조체로, // SetStatePerf와 같은 성능 정보를 기록하는 데 사용 SystemAPI.GetSingletonRW<StateChangeProfilerModule.FrameData>().ValueRW.SetStatePerf = 0; #endif return; } var radiusSq = config.Radius * config.Radius; var ecbSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>(); state.Dependency.Complete(); var before = ProfilerUnsafeUtility.Timestamp; if (config.Mode == Mode.VALUE) { // SetValueJob은 각 엔티티의 LocalTransform과 Hit 위치를 비교하여, // 엔티티가 Hit 위치와 반지름 내에 있을 경우 색상과 회전 여부를 적용 new SetValueJob { RadiusSq = radiusSq, Hit = hit.Value }.ScheduleParallel(); } else if (config.Mode == Mode.STRUCTURAL_CHANGE) { // Hit 위치와 반지름 내에 있는 엔티티에 Spin 컴포넌트를 추가 및 제거합니다. // 그리고 그 엔티티의 색상을 빨간색으로 바꿉니다. new AddSpinJob { RadiusSq = radiusSq, Hit = hit.Value, ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter() }.ScheduleParallel(); new RemoveSpinJob { RadiusSq = radiusSq, Hit = hit.Value, ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter() }.ScheduleParallel(); } else if (config.Mode == Mode.ENABLEABLE_COMPONENT) { // Hit 위치와 반지름 내에 있는 엔티티에서 Spin 컴포넌트를 활성화/비활성화합니다. new EnableSpinJob { RadiusSq = radiusSq, Hit = hit.Value, ECB = ecbSystem.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter() }.ScheduleParallel(); new DisableSpinJob { RadiusSq = radiusSq, Hit = hit.Value, }.ScheduleParallel(); } state.Dependency.Complete(); var after = ProfilerUnsafeUtility.Timestamp; #if UNITY_EDITOR // profiling var conversionRatio = ProfilerUnsafeUtility.TimestampToNanosecondsConversionRatio; var elapsed = (after - before) * conversionRatio.Numerator / conversionRatio.Denominator; SystemAPI.GetSingletonRW<StateChangeProfilerModule.FrameData>().ValueRW.SetStatePerf = elapsed; #endif } } [BurstCompile] partial struct SetValueJob : IJobEntity { public float RadiusSq; public float3 Hit; void Execute(ref URPMaterialPropertyBaseColor color, ref Spin spin, in LocalTransform transform) { if (math.distancesq(transform.Position, Hit) <= RadiusSq) { color.Value = (Vector4)Color.red; spin.IsSpinning = true; } else { color.Value = (Vector4)Color.white; spin.IsSpinning = false; } } } [WithNone(typeof(Spin))] [BurstCompile] partial struct AddSpinJob : IJobEntity { public float RadiusSq; public float3 Hit; public EntityCommandBuffer.ParallelWriter ECB; void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, [ChunkIndexInQuery] int chunkIndex) { // If cube is inside the hit radius. if (math.distancesq(transform.Position, Hit) <= RadiusSq) { color.Value = (Vector4)Color.red; ECB.AddComponent<Spin>(chunkIndex, entity); } } } [WithAll(typeof(Spin))] [BurstCompile] partial struct RemoveSpinJob : IJobEntity { public float RadiusSq; public float3 Hit; public EntityCommandBuffer.ParallelWriter ECB; void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, [ChunkIndexInQuery] int chunkIndex) { // If cube is NOT inside the hit radius. if (math.distancesq(transform.Position, Hit) > RadiusSq) { color.Value = (Vector4)Color.white; ECB.RemoveComponent<Spin>(chunkIndex, entity); } } } [WithNone(typeof(Spin))] [BurstCompile] public partial struct EnableSpinJob : IJobEntity { public float RadiusSq; public float3 Hit; public EntityCommandBuffer.ParallelWriter ECB; void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, [ChunkIndexInQuery] int chunkIndex) { // If cube is inside the hit radius. if (math.distancesq(transform.Position, Hit) <= RadiusSq) { color.Value = (Vector4)Color.red; ECB.SetComponentEnabled<Spin>(chunkIndex, entity, true); } } } [BurstCompile] public partial struct DisableSpinJob : IJobEntity { public float RadiusSq; public float3 Hit; void Execute(Entity entity, ref URPMaterialPropertyBaseColor color, in LocalTransform transform, EnabledRefRW<Spin> spinnerEnabled) { // If cube is NOT inside the hit radius. if (math.distancesq(transform.Position, Hit) > RadiusSq) { color.Value = (Vector4)Color.white; spinnerEnabled.ValueRW = false; } } } }
CubeSpawnSystem.cs
Config 컴포넌트의 설정에 따라 큐브를 생성하고, 해당 큐브들에 대한 상태를 설정하는 작업
using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Rendering; using Unity.Transforms; namespace HelloCube.StateChange { public partial struct CubeSpawnSystem : ISystem { Config priorConfig; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<Config>(); state.RequireForUpdate<ExecuteStateChange>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton<Config>(); // Config를 가져와서 이전의 Config와 비교합니다. // ConfigEquals 함수는 이전 설정과 새로운 설정을 비교하여,설정에 차이가 없으면 큐브를 재생성하지 않도록 합니다. if (ConfigEquals(priorConfig, config)) { return; } priorConfig = config; // URPMaterialPropertyBaseColor 컴포넌트를 가진 모든 엔티티들을 파괴하는 작업을 수행 var query = SystemAPI.QueryBuilder().WithAll<URPMaterialPropertyBaseColor>().Build(); state.EntityManager.DestroyEntity(query); // Prefab을 기반으로 큐브를 생성 var entities = state.EntityManager.Instantiate(config.Prefab, (int)(config.Size * config.Size), Allocator.Temp); var center = (config.Size - 1) / 2f; int i = 0; foreach (var transform in SystemAPI.Query<RefRW<LocalTransform>>()) { transform.ValueRW.Scale = 1; transform.ValueRW.Position.x = (i % config.Size - center) * 1.5f; transform.ValueRW.Position.z = (i / config.Size - center) * 1.5f; i++; } var spinQuery = SystemAPI.QueryBuilder().WithAll<Spin>().Build(); if (config.Mode == Mode.VALUE) { state.EntityManager.AddComponent<Spin>(query); } else if (config.Mode == Mode.ENABLEABLE_COMPONENT) { state.EntityManager.AddComponent<Spin>(query); state.EntityManager.SetComponentEnabled<Spin>(spinQuery, false); } } bool ConfigEquals(Config c1, Config c2) { return c1.Size == c2.Size && c1.Radius == c2.Radius && c1.Mode == c2.Mode; } } }