샘플 프로젝트 공략하기 마지막 포스팅이다.
이후에는 NSprite의 라이브러리를 쪼개고자 하지만... 내 실력에 가능할련지...
남은 코드는 아래 Squads 관련 System 코드들이다.
우선 쉬어가볼까?
SpawnSoldierSystem
더 보기를 누르면 전체 코드를 확인 할 수 있습니다.
using Unity.Burst;
using Unity.Entities;
using UnityEngine;
[BurstCompile]
public partial struct SpawnSoliderSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
if (!Input.GetKey(KeyCode.A))
return;
if (!SystemAPI.TryGetSingleton<SquadDefaultSettings>(out var squadSettings))
return;
_ = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged).Instantiate(squadSettings.soldierPrefab);
}
}
정말 간단한 System 코드이다.
A를 누르면 동작하는 코드이며
_ = SystemAPI.
GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged)
.Instantiate(squadSettings.soldierPrefab);
무식하게 길게 느껴지는
코드 한줄만을 위한 System 코드이다.
이 코드를 통해서 ECS의 위엄을 또 한번 느낄 수 있는데
A를 연타하는 만큼 미친듯이 Soldier들이 생성된다.
ECB를 생성하여 SoldierPrefab을 생성하는 코드를 ECB에 기록 한 뒤
업데이트가 끝나며 생성 시키기만 하는 코드이다. 끝!
SquadSpawnSystem
더 보기를 누르면 전체 코드를 확인 할 수 있습니다.
개발자가 나 이런 방식으로도 동작 시킬 수 있다.라고 자랑하는건지
코드들이 참 다양한 방식으로 구현해놓은 것 같다.
using NSprites;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using UnityEngine;
[WorldSystemFilter(WorldSystemFilterFlags.Default)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[BurstCompile]
public partial struct SquadSpawnSystem : ISystem
{
private struct SystemData : IComponentData
{
public EntityArchetype SquadArchetype;
}
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<SquadDefaultSettings>();
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
var typeArray = new NativeArray<ComponentType>(4, Allocator.Temp);
typeArray[0] = ComponentType.ReadOnly<WorldPosition2D>();
typeArray[1] = ComponentType.ReadOnly<PrevWorldPosition2D>();
typeArray[2] = ComponentType.ReadOnly<SoldierLink>();
typeArray[3] = ComponentType.ReadOnly<RequireSoldier>();
var systemData = new SystemData{ SquadArchetype = state.EntityManager.CreateArchetype(typeArray) };
_ = state.EntityManager.AddComponentData(state.SystemHandle, systemData);
typeArray.Dispose();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
if (!Input.GetKeyDown(KeyCode.S))
return;
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
var squadSettings = SystemAPI.GetSingleton<SquadDefaultSettings>();
var soldierCount = squadSettings.SoldierCount;
var squadEntity = ecb.CreateEntity(systemData.SquadArchetype);
ecb.SetComponent(squadEntity, new RequireSoldier { count = soldierCount });
}
}
(1)
[UpdateInGroup(typeof(SimulationSystemGroup))]
이제는 익숙해졌다.
해당 System은 SimulationSystemGroup에서 동작 시키겠다는 의미이다.
(2)
OnCreate를 보자.
기존 SystemData를 설계하던 방식과 다르게 이번엔 ArchType를 사용하는 SystemData 컴포넌트를 구현해놓았다.
var typeArray = new NativeArray<ComponentType>(4, Allocator.Temp);
typeArray[0] = ComponentType.ReadOnly<WorldPosition2D>();
typeArray[1] = ComponentType.ReadOnly<PrevWorldPosition2D>();
typeArray[2] = ComponentType.ReadOnly<SoldierLink>();
typeArray[3] = ComponentType.ReadOnly<RequireSoldier>();
4개의 Component를 가지고 있는 NativeArray를 생성한 뒤에,
var systemData = new SystemData{ SquadArchetype = state.EntityManager.CreateArchetype(typeArray) };
systemData struct를 생성하면서 새로운 ArcheType으로써 생성해주고 있다.
굳이 이렇게 하는 이유가 있는지 궁금하지만..
이번에는 Squad Authroing 이 가지고 있던 컴포넌트들을 타겟팅으로 하고 있다.
스쿼드는 초기에 17개로 시작하는데
스쿼드 생성하는 코드 역시 Soldier처럼 Hierachy에는 반영이 안되도록 하고 있나보다.
이번에는 S 키를 누를때마다 Squad가 생성된다. 딱히 표시는 안되지만..
(3)
OnUpdate 문을 보자
의외로 정석적인 ECS System 스크립트이다.
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
if (!Input.GetKeyDown(KeyCode.S))
return;
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
var squadSettings = SystemAPI.GetSingleton<SquadDefaultSettings>();
var soldierCount = squadSettings.SoldierCount;
var squadEntity = ecb.CreateEntity(systemData.SquadArchetype);
ecb.SetComponent(squadEntity, new RequireSoldier { count = soldierCount });
}
SystemData로부터 ArchType을 꺼내올 준비를 하고
ECB 생성
SquadDefaultSetting 꺼낸 뒤에
스쿼드 안에 담을 SoldierCount 설정
ArchType대로 설정된 Entity를 생성 한뒤에 RequireSoldier 컴포넌트를 세팅해주고 끝난다.
진짜 간단하다..
이 정도면 Hello Cube 보다 도움 되는 예제라고 생각이 들정도
의외로 천천히 리뷰하면 간단한 System 코드들이 많다.
SoldierDistributionSystem 처럼 괴악한 스크립트도 존재하지만... (이것도 뭐 까보면 별거 아닐수도 있다.)
SpawnNewSquad
더 보기를 누르면 전체 코드를 확인 할 수 있습니다.
using NSprites;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
[BurstCompile]
public partial struct SpawnNewSquadsSystem : ISystem
{
private struct SystemData : IComponentData
{
public EntityQuery SoldierRequireQuery;
public EntityArchetype SquadArchetype;
public Random Rand;
}
public void OnCreate(ref SystemState state)
{
var systemData = new SystemData
{
SoldierRequireQuery = state.GetEntityQuery(typeof(RequireSoldier)),
SquadArchetype = state.EntityManager.CreateArchetype
(
typeof(SoldierLink),
typeof(RequireSoldier),
typeof(SquadSettings),
typeof(WorldPosition2D),
typeof(PrevWorldPosition2D)
),
Rand = new Random(1u)
};
_ = state.EntityManager.AddComponentData(state.SystemHandle, systemData);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
if (systemData.SoldierRequireQuery.CalculateChunkCount() != 0
|| !SystemAPI.TryGetSingleton<MapSettings>(out var mapSettings)
|| !SystemAPI.TryGetSingleton<SquadDefaultSettings>(out var squadDefaultSettings))
return;
var pos = systemData.Rand.NextFloat2(mapSettings.size.c0, mapSettings.size.c1);
var resolution = systemData.Rand.NextInt2(new int2(5), new int2(20));
var soldierCount = resolution.x * resolution.y;
var squadEntity = state.EntityManager.CreateEntity(systemData.SquadArchetype);
state.EntityManager.GetBuffer<SoldierLink>(squadEntity).EnsureCapacity(soldierCount);
state.EntityManager.SetComponentData(squadEntity, new SquadSettings
{
squadResolution = resolution,
soldierMargin = squadDefaultSettings.defaultSettings.soldierMargin
});
state.EntityManager.SetComponentData(squadEntity, new RequireSoldier { count = soldierCount });
state.EntityManager.SetComponentData(squadEntity, new WorldPosition2D { Value = pos });
state.EntityManager.SetComponentData(squadEntity, new PrevWorldPosition2D { value = pos });
SystemAPI.SetComponent(state.SystemHandle, systemData);
}
}
이번에도 정석적인 System 스크립트이다. 너무 좋다.
(1)
SystemData라는 내부 컴포넌트 Struct를 보자.
private struct SystemData : IComponentData
{
public EntityQuery SoldierRequireQuery;
public EntityArchetype SquadArchetype;
public Random Rand;
}
이번에는 좀 알차게 들어있다.
필터링할 EntityQuery
아마도 생성에 사용될 EntityArcheType
그리고 랜덤 시드값
(2)
빠르게 OnCreate로 들어가자.
public void OnCreate(ref SystemState state)
{
var systemData = new SystemData
{
SoldierRequireQuery = state.GetEntityQuery(typeof(RequireSoldier)),
SquadArchetype = state.EntityManager.CreateArchetype
(
typeof(SoldierLink),
typeof(RequireSoldier),
typeof(SquadSettings),
typeof(WorldPosition2D),
typeof(PrevWorldPosition2D)
),
Rand = new Random(1u)
};
_ = state.EntityManager.AddComponentData(state.SystemHandle, systemData);
}
SystemData 컴포넌트를 마찬가지로 생성
public struct RequireSoldier : IComponentData
{
public int count;
}
RequireSoldier를 필터링용으로 사용하고 있다.
ArcheType은 이전 코드와 동일하게 Squad를 생성하기 위한 ArcheType 인 것 같다.
이런 저런 방식이 있다는 것을 설명해주는 코드인 듯..
곧바로 AddComponent 해주고 있다.
(3)
다음 OnUpdate 코드.
확실히 리뷰를 계속 하다보니까 속도가 붙는 느낌이다.
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
if (systemData.SoldierRequireQuery.CalculateChunkCount() != 0
|| !SystemAPI.TryGetSingleton<MapSettings>(out var mapSettings)
|| !SystemAPI.TryGetSingleton<SquadDefaultSettings>(out var squadDefaultSettings))
return;
var pos = systemData.Rand.NextFloat2(mapSettings.size.c0, mapSettings.size.c1);
var resolution = systemData.Rand.NextInt2(new int2(5), new int2(20));
var soldierCount = resolution.x * resolution.y;
var squadEntity = state.EntityManager.CreateEntity(systemData.SquadArchetype);
state.EntityManager.GetBuffer<SoldierLink>(squadEntity).EnsureCapacity(soldierCount);
state.EntityManager.SetComponentData(squadEntity, new SquadSettings
{
squadResolution = resolution,
soldierMargin = squadDefaultSettings.defaultSettings.soldierMargin
});
state.EntityManager.SetComponentData(squadEntity, new RequireSoldier { count = soldierCount });
state.EntityManager.SetComponentData(squadEntity, new WorldPosition2D { Value = pos });
state.EntityManager.SetComponentData(squadEntity, new PrevWorldPosition2D { value = pos });
SystemAPI.SetComponent(state.SystemHandle, systemData);
}
시작하자마자 SystemData 컴포넌트를 꺼내주고
이런 저런 조건을 통해 해당 System을 실행 할 지 결정해주고 있다.
CalculateChunkCount()
Calculates the number of chunks that match this EntityQuery, taking into account all active query filters and enabled components.
해당 청크의 카운트가 0인지 체크하는 조건이다.
어라? 해당 청크의 카운트를 늘려주는건, 이전 코드에서 S 키를 누를때마다였다.
단순히 생성 하는 Squad 스크립트가 처리 되기전에, 이 System 코드를 멈추게 했다는 걸 짐작 할 수 있다.
다른 필터링 조건은 뭐 반 필수적인 세팅들이라 CunkCount만 신경쓰면 된다.
var pos => 스쿼드 랜덤 생성 위치
resolution은 랜덤으로
해당 스쿼드에 할당할 병사들의 수는 soldierCount 로
아주 쉽다!
그러면 앞서서 SystemData의 ArcheType을 엔티티 생성 해주고.
SoldierLink 라는 DynamicBuffer를 설정한다.
(SoldierLink는 SquadAuthoring에서 AddBuffer를 해주고 있다.)
EnsureCapacity 는 DynamicBuffer의 용량을 적어도 이 정도는 사용하겠다고 마크하는 함수이다.
DynamicBuffer의 특징을 고려하면 필요한 함수라도 생각한다.
생성할 Squad 엔티티에 SetComponent를 통해 필요한 값을 넣어준다.
여기서는 EntityManager를 활용하고 있음을 알 수 있다.
코드는 이렇게 끝이 나는데
이번에도 다양한 방법을 시도하는 걸 보여주는 코드인 것 같다.
이걸 보니 개발자에게 고마움이 느껴진다.
SquadMoveSystem
더 보기를 누르면 전체 코드를 확인 할 수 있습니다.
이제 2개 남았다.
이 코드는 샘플 프로젝트의 최대 난관이다.
해당 코드도 욕부터 나오는 구조를 보여주지만.. 천천히 해체해보자.
SquadMoveSystem의 사용 주체 엔티티는 SquadSettings라는 이름의 엔티티이다.
Squad 매니저라고 생각하자.
using NSprites;
using System.Runtime.CompilerServices;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
[BurstCompile]
public partial struct SquadMoveSystem : ISystem
{
[BurstCompile]
private struct MoveJob : IJobChunk
{
public float2 SoldierSize;
[ReadOnly] public BufferTypeHandle<SoldierLink> SoldierLink_BTH_RO;
[ReadOnly] public ComponentTypeHandle<WorldPosition2D> WorldPosition2D_RO;
[ReadOnly] public ComponentTypeHandle<SquadSettings> SquadSettings_CTH_RO;
public ComponentTypeHandle<PrevWorldPosition2D> PrevPos_CTH_RW;
[NativeDisableParallelForRestriction][WriteOnly] public ComponentLookup<Destination> Destination_CL_WO;
public uint LastSystemVersion;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MoveSoldiers(in SquadSettings squadSettings, in float2 soldierSize, in DynamicBuffer<SoldierLink> soldiersBuffer, in float2 pos, ref ComponentLookup<Destination> destination_CL_WO)
{
var perSoldierOffset = (2 * squadSettings.soldierMargin + 1f) * soldierSize;
for (var soldierIndex = 0; soldierIndex < soldiersBuffer.Length; soldierIndex++)
destination_CL_WO[soldiersBuffer[soldierIndex].entity] = new Destination { Value = pos + (perSoldierOffset * new float2(soldierIndex % squadSettings.squadResolution.x + .5f, soldierIndex / squadSettings.squadResolution.x)) };
}
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
{
var worldPositions = chunk.GetNativeArray(ref WorldPosition2D_RO);
var soldierBufferAccessor = chunk.GetBufferAccessor(ref SoldierLink_BTH_RO);
if (chunk.DidChange(ref WorldPosition2D_RO, LastSystemVersion))
{
var prevPositions = chunk.GetNativeArray(ref PrevPos_CTH_RW);
var squadSettingsArray = chunk.GetNativeArray(ref SquadSettings_CTH_RO);
for (int squadIndex = 0; squadIndex < worldPositions.Length; squadIndex++)
{
var pos2D = worldPositions[squadIndex].Value;
if (math.any(pos2D != prevPositions[squadIndex].value))
{
MoveSoldiers(squadSettingsArray[squadIndex], SoldierSize, soldierBufferAccessor[squadIndex], pos2D, ref Destination_CL_WO);
prevPositions[squadIndex] = new PrevWorldPosition2D { value = pos2D };
}
}
}
else if (chunk.DidChange(ref SoldierLink_BTH_RO, LastSystemVersion))
{
var squadSettingsArray = chunk.GetNativeArray(ref SquadSettings_CTH_RO);
for (int squadIndex = 0; squadIndex < worldPositions.Length; squadIndex++)
MoveSoldiers(squadSettingsArray[squadIndex], SoldierSize, soldierBufferAccessor[squadIndex], worldPositions[squadIndex].Value, ref Destination_CL_WO);
}
}
}
[BurstCompile]
private partial struct MoveOnChangeGlobalSettingsJob : IJobEntity
{
public float2 SoldierSize;
[NativeDisableParallelForRestriction] public ComponentLookup<Destination> Destination_CL;
private void Execute(in LocalToWorld ltw, in DynamicBuffer<SoldierLink> soldiersBuffer, in SquadSettings squadSettings)
{
MoveJob.MoveSoldiers(squadSettings, SoldierSize, soldiersBuffer, ltw.Position.xy, ref Destination_CL);
}
}
private struct SystemData : IComponentData
{
public SquadDefaultSettings PrevSquadSettings;
public EntityQuery SquadQuery;
}
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var queryBuilder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<SoldierLink, WorldPosition2D, PrevWorldPosition2D, SquadSettings>();
_ = state.EntityManager.AddComponentData(state.SystemHandle, new SystemData { SquadQuery = state.GetEntityQuery(queryBuilder) });
queryBuilder.Dispose();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
if (!SystemAPI.TryGetSingleton<SquadDefaultSettings>(out var squadDefaultSettings))
return;
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var soldierSize = SystemAPI.GetComponent<Scale2D>(squadDefaultSettings.soldierPrefab).value;
if (systemData.PrevSquadSettings != squadDefaultSettings)
{
systemData.PrevSquadSettings = squadDefaultSettings;
SystemAPI.SetSingleton(systemData);
var moveOnSettingChangeJob = new MoveOnChangeGlobalSettingsJob
{
Destination_CL = SystemAPI.GetComponentLookup<Destination>(false),
SoldierSize = soldierSize
};
state.Dependency = moveOnSettingChangeJob.ScheduleParallelByRef(state.Dependency);
}
else
{
var moveJob = new MoveJob
{
LastSystemVersion = state.LastSystemVersion,
SoldierSize = soldierSize,
SquadSettings_CTH_RO = SystemAPI.GetComponentTypeHandle<SquadSettings>(true),
Destination_CL_WO = SystemAPI.GetComponentLookup<Destination>(false),
PrevPos_CTH_RW = SystemAPI.GetComponentTypeHandle<PrevWorldPosition2D>(false),
SoldierLink_BTH_RO = SystemAPI.GetBufferTypeHandle<SoldierLink>(true),
WorldPosition2D_RO = SystemAPI.GetComponentTypeHandle<WorldPosition2D>(true)
};
state.Dependency = moveJob.ScheduleParallelByRef(systemData.SquadQuery, state.Dependency);
}
}
}
(1)
SystemData Component를 보자
private struct SystemData : IComponentData
{
public SquadDefaultSettings PrevSquadSettings;
public EntityQuery SquadQuery;
}
SuqadDefaultSettings 컴포넌트와 EntityQuery를 가지고 있다.
public struct SquadDefaultSettings : IComponentData, IEquatable<SquadDefaultSettings>
{
public Entity soldierPrefab;
public SquadSettings defaultSettings;
public float2 SoldierMargin => defaultSettings.soldierMargin;
public int2 SquadResolution => defaultSettings.squadResolution;
public int SoldierCount => SquadResolution.x * SquadResolution.y;
...
PrevSquadSettings 라는 변수명을 보아하니
세팅을 저장해두고, 업데이트 하는 식으로 하는 걸까 짐작이 된다.
(2)
OnCreate
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var queryBuilder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<SoldierLink, WorldPosition2D, PrevWorldPosition2D, SquadSettings>();
_ = state.EntityManager.AddComponentData(state.SystemHandle, new SystemData { SquadQuery = state.GetEntityQuery(queryBuilder) });
queryBuilder.Dispose();
}
WithAll 에 나열된 Component를 보아하니, Squad 엔티티에 대한 Query임을 알 수 있다.
SystemData를 만들었지만, 별거 없으니 넘어가자
(3)
OnUpdate
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
if (!SystemAPI.TryGetSingleton<SquadDefaultSettings>(out var squadDefaultSettings))
return;
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var soldierSize = SystemAPI.GetComponent<Scale2D>(squadDefaultSettings.soldierPrefab).value;
if (systemData.PrevSquadSettings != squadDefaultSettings)
{
systemData.PrevSquadSettings = squadDefaultSettings;
SystemAPI.SetSingleton(systemData);
var moveOnSettingChangeJob = new MoveOnChangeGlobalSettingsJob
{
Destination_CL = SystemAPI.GetComponentLookup<Destination>(false),
SoldierSize = soldierSize
};
state.Dependency = moveOnSettingChangeJob.ScheduleParallelByRef(state.Dependency);
}
else
{
var moveJob = new MoveJob
{
LastSystemVersion = state.LastSystemVersion,
SoldierSize = soldierSize,
SquadSettings_CTH_RO = SystemAPI.GetComponentTypeHandle<SquadSettings>(true),
Destination_CL_WO = SystemAPI.GetComponentLookup<Destination>(false),
PrevPos_CTH_RW = SystemAPI.GetComponentTypeHandle<PrevWorldPosition2D>(false),
SoldierLink_BTH_RO = SystemAPI.GetBufferTypeHandle<SoldierLink>(true),
WorldPosition2D_RO = SystemAPI.GetComponentTypeHandle<WorldPosition2D>(true)
};
state.Dependency = moveJob.ScheduleParallelByRef(systemData.SquadQuery, state.Dependency);
}
}
If문을 통해 분기를 주고 있다.
우선, 처음부터 살펴보자.
SoldierSize의 Scale2D 는 NSprite의 Animation Sprite Authoring을 통해 얻을 수 있는 Component임을 잊지 말자.
IF문을 보면,
PrevSquadSettings랑 squadDefaultSettings를 비교하고 있다.
둘은 Struct인데 어떻게 비교하는것인가?
이제서야 DefaultSettings 컴포넌트 코드의 IEquatable 인터페이스의 사용 목적이 보인다.
public override bool Equals(object obj) => base.Equals(obj);
systemData.PrevSquadSettings = squadDefaultSettings;
SystemAPI.SetSingleton(systemData);
var moveOnSettingChangeJob = new MoveOnChangeGlobalSettingsJob
{
Destination_CL = SystemAPI.GetComponentLookup<Destination>(false),
SoldierSize = soldierSize
};
state.Dependency = moveOnSettingChangeJob.ScheduleParallelByRef(state.Dependency);
같지 않을 때, PrevSettings를 자기 자신으로 바꿔주고,
MoveOnChangeGlobalSettings Job 을 생성하여 실행 시켜주고 있다.
var moveJob = new MoveJob
{
LastSystemVersion = state.LastSystemVersion,
SoldierSize = soldierSize,
SquadSettings_CTH_RO = SystemAPI.GetComponentTypeHandle<SquadSettings>(true),
Destination_CL_WO = SystemAPI.GetComponentLookup<Destination>(false),
PrevPos_CTH_RW = SystemAPI.GetComponentTypeHandle<PrevWorldPosition2D>(false),
SoldierLink_BTH_RO = SystemAPI.GetBufferTypeHandle<SoldierLink>(true),
WorldPosition2D_RO = SystemAPI.GetComponentTypeHandle<WorldPosition2D>(true)
};
state.Dependency = moveJob.ScheduleParallelByRef(systemData.SquadQuery, state.Dependency);
같다면,
Move Job을 생성하여 실행한다.
MoveOnChangeGlobalSettings Job부터 부셔보자.
Destination ( public float2 Value;) 컴포넌트와 SoliderSize ( Scale Value) 컴포넌트를 받아서 실행해주고 있다.
(4)
MoveOnChangeGlobalSettings Job
[BurstCompile]
private partial struct MoveOnChangeGlobalSettingsJob : IJobEntity
{
public float2 SoldierSize;
[NativeDisableParallelForRestriction] public ComponentLookup<Destination> Destination_CL;
private void Execute(in LocalToWorld ltw, in DynamicBuffer<SoldierLink> soldiersBuffer, in SquadSettings squadSettings)
{
MoveJob.MoveSoldiers(squadSettings, SoldierSize, soldiersBuffer, ltw.Position.xy, ref Destination_CL);
}
}
의외로 별거 없다.
Native를 마음껏 쓰게 해주는 Destination_CL 컴포넌트 값들을 가져오고
위치 값과 DynamicBuffer, SquadSettings을 받아서 실행 해준다.
근데?? 실행 하는건 MoveJob 쪽이다.
그 의미는
SquadSettings이 같아지면, 새로운 MoveJob을 부여받으며, 같지 않아지고
같지 않은 동안 MoveSoliders 함수를 실행 시켜준다.
(5)
MoveJob 코드이다. 머리가 띵하네..
Soldier 코드 리뷰가 없었으면 참 애먹을만한 코드이다.
이번 코드에는 Job 내부에 인라인 함수가 포함되어 있다. (MoveSoliders 함수)
[BurstCompile]
private struct MoveJob : IJobChunk
{
public float2 SoldierSize;
[ReadOnly] public BufferTypeHandle<SoldierLink> SoldierLink_BTH_RO;
[ReadOnly] public ComponentTypeHandle<WorldPosition2D> WorldPosition2D_RO;
[ReadOnly] public ComponentTypeHandle<SquadSettings> SquadSettings_CTH_RO;
public ComponentTypeHandle<PrevWorldPosition2D> PrevPos_CTH_RW;
[NativeDisableParallelForRestriction][WriteOnly] public ComponentLookup<Destination> Destination_CL_WO;
public uint LastSystemVersion;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MoveSoldiers(in SquadSettings squadSettings, in float2 soldierSize, in DynamicBuffer<SoldierLink> soldiersBuffer, in float2 pos, ref ComponentLookup<Destination> destination_CL_WO)
{
var perSoldierOffset = (2 * squadSettings.soldierMargin + 1f) * soldierSize;
for (var soldierIndex = 0; soldierIndex < soldiersBuffer.Length; soldierIndex++)
destination_CL_WO[soldiersBuffer[soldierIndex].entity] = new Destination { Value = pos + (perSoldierOffset * new float2(soldierIndex % squadSettings.squadResolution.x + .5f, soldierIndex / squadSettings.squadResolution.x)) };
}
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
{
var worldPositions = chunk.GetNativeArray(ref WorldPosition2D_RO);
var soldierBufferAccessor = chunk.GetBufferAccessor(ref SoldierLink_BTH_RO);
if (chunk.DidChange(ref WorldPosition2D_RO, LastSystemVersion))
{
var prevPositions = chunk.GetNativeArray(ref PrevPos_CTH_RW);
var squadSettingsArray = chunk.GetNativeArray(ref SquadSettings_CTH_RO);
for (int squadIndex = 0; squadIndex < worldPositions.Length; squadIndex++)
{
var pos2D = worldPositions[squadIndex].Value;
if (math.any(pos2D != prevPositions[squadIndex].value))
{
MoveSoldiers(squadSettingsArray[squadIndex], SoldierSize, soldierBufferAccessor[squadIndex], pos2D, ref Destination_CL_WO);
prevPositions[squadIndex] = new PrevWorldPosition2D { value = pos2D };
}
}
}
else if (chunk.DidChange(ref SoldierLink_BTH_RO, LastSystemVersion))
{
var squadSettingsArray = chunk.GetNativeArray(ref SquadSettings_CTH_RO);
for (int squadIndex = 0; squadIndex < worldPositions.Length; squadIndex++)
MoveSoldiers(squadSettingsArray[squadIndex], SoldierSize, soldierBufferAccessor[squadIndex], worldPositions[squadIndex].Value, ref Destination_CL_WO);
}
}
}
코드 리뷰 전에 숙지하자. 해당 코드의 주체는 SquadSettings 라는 엔티티이며, Squad 들을 관리한다. Squad 들은 Soldier 사이즈와 Margin 값을 기준으로 각자 사이즈를 갖고 있으며, 할당된 Soldier들을 버퍼로 갖고 있다. Soldier들은 각자 Sprite에 관련된 컴포넌트들을 가지고 있으며, Destination을 할당 받아 움직여야 한다. |
Squad에 세울 Soldier들은 Margin과 함께 배치되도록 한다.
해당 Squad에 Link 되어 있는 (DynamicBuffer) 버퍼의 Soldier들의 Destination을
할당 해주는 역할이 MoveSoldiers 이다.
Job 실행 부를 보자.
IJobChunk의 기본적인 Execute 부분을 지나서..
우선,
Squad들의 WorldPosition들
SoldierBuffer를 갖고온다.
다음 If 문에서는 2가지를 분기로 둔다.
(1) 현재 Squad의 WorldPosition이 변경되었는가?
변경된 Squad들에 대하여 Soldier들의 Destination들을 할당 해준다.
(2) 현재 Squad의 Soldier Buffer가 변경되었는가?
Soldier들을 Squad들에 맞게 이동한다.
코드가 많이 난해하다.
졸려서 집중이 안되는 것일수도..
Distribution 쪽에서 작업하는게 있는걸까....
SoldierDistributionSystem
더 보기를 누르면 전체 코드를 확인 할 수 있습니다.
마지막 코드이자 최대 난관 코드 중 하나이다.
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public partial struct SoldierDistributionSystem : ISystem
{
[BurstCompile]
private struct DistributeJob : IJob
{
[ReadOnly] public NativeList<Entity> SquadEntities;
[ReadOnly] public NativeList<RequireSoldier> RequireSoldierData;
[ReadOnly] public NativeList<Entity> SoldierEntities;
public EntityCommandBuffer ECB;
public BufferLookup<SoldierLink> SoldierLink_BFE;
[WriteOnly] public ComponentLookup<RequireSoldier> RequireSoldier_CDFE_WO;
public void Execute()
{
if (SoldierEntities.Length == 0 || SquadEntities.Length == 0)
return;
var soldierIndex = 0;
var prevSquadIndex = -1;
var squadIndex = 0;
DynamicBuffer<SoldierLink> soldierLinkBuffer = default;
while (soldierIndex < SoldierEntities.Length && squadIndex < SquadEntities.Length)
{
if (squadIndex != prevSquadIndex)
{
soldierLinkBuffer = SoldierLink_BFE[SquadEntities[squadIndex]];
prevSquadIndex = squadIndex;
}
var requireSoldier = RequireSoldierData[squadIndex];
var distributionCount = math.min(SoldierEntities.Length - soldierIndex, requireSoldier.count);
soldierLinkBuffer.Capacity += distributionCount;
for (int i = soldierIndex; i < distributionCount; i++)
{
var soldierEntity = SoldierEntities[i];
_ = soldierLinkBuffer.Add(new SoldierLink { entity = soldierEntity });
ECB.AddComponent<InSquadSoldierTag>(soldierEntity);
}
soldierIndex += distributionCount;
requireSoldier.count -= distributionCount;
// means squad is full so we can just remove comp
if (requireSoldier.count == 0)
ECB.RemoveComponent<RequireSoldier>(SquadEntities[squadIndex++]);
// means squad isn't full AND there is no more soldiers so we should update comp
else if (soldierIndex >= SoldierEntities.Length)
RequireSoldier_CDFE_WO[SquadEntities[squadIndex]] = requireSoldier;
}
}
}
private struct SystemData : IComponentData
{
public EntityQuery SoldierLessSquadQuery;
public EntityQuery FreeSoldiersQuery;
}
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var systemData = new SystemData();
var queryBuilder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<RequireSoldier>();
systemData.SoldierLessSquadQuery = state.GetEntityQuery(queryBuilder);
queryBuilder.Reset();
_ = queryBuilder
.WithAll<SoldierTag>()
.WithNone<InSquadSoldierTag>();
systemData.FreeSoldiersQuery = state.GetEntityQuery(queryBuilder);
_ = state.EntityManager.AddComponentData(state.SystemHandle, systemData);
queryBuilder.Dispose();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var squadEntities = systemData.SoldierLessSquadQuery.ToEntityListAsync(Allocator.TempJob, out var squadEntitiesGatherHandle);
var requireSoldierData = systemData.SoldierLessSquadQuery.ToComponentDataListAsync<RequireSoldier>(Allocator.TempJob, state.Dependency, out var requireSoldier_GatherHandle);
var soldierEntities = systemData.FreeSoldiersQuery.ToEntityListAsync(Allocator.TempJob, out var soldierEntitiesGatherHandle);
var distributeJob = new DistributeJob
{
SquadEntities = squadEntities,
RequireSoldierData = requireSoldierData,
SoldierEntities = soldierEntities,
ECB = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged),
SoldierLink_BFE = SystemAPI.GetBufferLookup<SoldierLink>(false),
RequireSoldier_CDFE_WO = SystemAPI.GetComponentLookup<RequireSoldier>(false)
};
var inputHandles = new NativeArray<JobHandle>(4, Allocator.Temp);
inputHandles[0] = squadEntitiesGatherHandle;
inputHandles[1] = requireSoldier_GatherHandle;
inputHandles[2] = soldierEntitiesGatherHandle;
inputHandles[3] = state.Dependency;
state.Dependency = distributeJob.ScheduleByRef(JobHandle.CombineDependencies(inputHandles));
_ = squadEntities.Dispose(state.Dependency);
_ = requireSoldierData.Dispose(state.Dependency);
_ = soldierEntities.Dispose(state.Dependency);
}
}
(1)
어김없이, Component들부터 보자
private struct SystemData : IComponentData
{
public EntityQuery SoldierLessSquadQuery;
public EntityQuery FreeSoldiersQuery;
}
이번엔 필터링할 쿼리가 2개가 있다.
SoldierLessSquadQuery
FreeSoldierQuery
이름만 봐도 무엇 할 지 짐작이 간다.
(2)
OnCreate
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var systemData = new SystemData();
var queryBuilder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<RequireSoldier>();
systemData.SoldierLessSquadQuery = state.GetEntityQuery(queryBuilder);
queryBuilder.Reset();
_ = queryBuilder
.WithAll<SoldierTag>()
.WithNone<InSquadSoldierTag>();
systemData.FreeSoldiersQuery = state.GetEntityQuery(queryBuilder);
_ = state.EntityManager.AddComponentData(state.SystemHandle, systemData);
queryBuilder.Dispose();
}
RequireSoldier 컴포넌트가 있는 엔티티들을 가져오는 쿼리
SoldierTag는 있지만, InSquadSoldierTag가 없는 Soldier 엔티티들을 가져오는 쿼리 2개를 SystemData에 넣어준다.
(3)
OnUpdate
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var systemData = SystemAPI.GetComponent<SystemData>(state.SystemHandle);
var squadEntities = systemData.SoldierLessSquadQuery.ToEntityListAsync(Allocator.TempJob, out var squadEntitiesGatherHandle);
var requireSoldierData = systemData.SoldierLessSquadQuery.ToComponentDataListAsync<RequireSoldier>(Allocator.TempJob, state.Dependency, out var requireSoldier_GatherHandle);
var soldierEntities = systemData.FreeSoldiersQuery.ToEntityListAsync(Allocator.TempJob, out var soldierEntitiesGatherHandle);
var distributeJob = new DistributeJob
{
SquadEntities = squadEntities,
RequireSoldierData = requireSoldierData,
SoldierEntities = soldierEntities,
ECB = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged),
SoldierLink_BFE = SystemAPI.GetBufferLookup<SoldierLink>(false),
RequireSoldier_CDFE_WO = SystemAPI.GetComponentLookup<RequireSoldier>(false)
};
var inputHandles = new NativeArray<JobHandle>(4, Allocator.Temp);
inputHandles[0] = squadEntitiesGatherHandle;
inputHandles[1] = requireSoldier_GatherHandle;
inputHandles[2] = soldierEntitiesGatherHandle;
inputHandles[3] = state.Dependency;
state.Dependency = distributeJob.ScheduleByRef(JobHandle.CombineDependencies(inputHandles));
_ = squadEntities.Dispose(state.Dependency);
_ = requireSoldierData.Dispose(state.Dependency);
_ = soldierEntities.Dispose(state.Dependency);
}
systemData 준비,
RequireSoldier 컴포넌트가 있는 Squad 엔티티들 준비
할당 되지 못한 RequireSoldiers 엔티티들 준비
Job을 생성하고,
Job Handle을 가진 NativeArray를 생성한다.
여기서는 특별히 CombineDependencies를 사용하고 있다.
(4)
DistributeJob : IJob
이번엔 IJob 인터페이스를 사용하고 있다.
[BurstCompile]
private struct DistributeJob : IJob
{
[ReadOnly] public NativeList<Entity> SquadEntities;
[ReadOnly] public NativeList<RequireSoldier> RequireSoldierData;
[ReadOnly] public NativeList<Entity> SoldierEntities;
public EntityCommandBuffer ECB;
public BufferLookup<SoldierLink> SoldierLink_BFE;
[WriteOnly] public ComponentLookup<RequireSoldier> RequireSoldier_CDFE_WO;
public void Execute()
{
if (SoldierEntities.Length == 0 || SquadEntities.Length == 0)
return;
var soldierIndex = 0;
var prevSquadIndex = -1;
var squadIndex = 0;
DynamicBuffer<SoldierLink> soldierLinkBuffer = default;
while (soldierIndex < SoldierEntities.Length && squadIndex < SquadEntities.Length)
{
if (squadIndex != prevSquadIndex)
{
soldierLinkBuffer = SoldierLink_BFE[SquadEntities[squadIndex]];
prevSquadIndex = squadIndex;
}
var requireSoldier = RequireSoldierData[squadIndex];
var distributionCount = math.min(SoldierEntities.Length - soldierIndex, requireSoldier.count);
soldierLinkBuffer.Capacity += distributionCount;
for (int i = soldierIndex; i < distributionCount; i++)
{
var soldierEntity = SoldierEntities[i];
_ = soldierLinkBuffer.Add(new SoldierLink { entity = soldierEntity });
ECB.AddComponent<InSquadSoldierTag>(soldierEntity);
}
soldierIndex += distributionCount;
requireSoldier.count -= distributionCount;
// means squad is full so we can just remove comp
if (requireSoldier.count == 0)
ECB.RemoveComponent<RequireSoldier>(SquadEntities[squadIndex++]);
// means squad isn't full AND there is no more soldiers so we should update comp
else if (soldierIndex >= SoldierEntities.Length)
RequireSoldier_CDFE_WO[SquadEntities[squadIndex]] = requireSoldier;
}
}
}
텅빈 SoldierLinkBuffer를 두고,
Soldier Index / Squad Index가 넘치지 않을 떄까지 While문을 돌리고 있다.
Squad는 순서는 모르지만, 앞에서부터 카운트 업 하는 방식으로 반복문이 진행되는 것 같다.
앞에 텅빈 SoldierLinkBuffer를 현재 Squad의 Buffer로 변경해놓고,
커서를 해당 스쿼드로 설정한다. (prevSquadIndex)
requireSoldier 란 이름으로 특정 Squad의 RequiredSoldierData를 가져오고
남은 Soldier 수 혹은 RequireSoldierData의 수 중에서 작은 값을 Count로 설정
SoldierLinkBuffer의 카파시티를 설정해준다.
카파시티 만큼 Soldier 엔티티들을 버퍼에 넣어주면서
해당 솔져에 InSquadSoldierTag를 넣어줘서 할당 된 Soldier 꼬리표를 달아준다.
SoldierIndex를 버퍼에 넣어준 수만큼 올려줘서 중복되지 않도록 하며
RequireSoldier의 값을 감소시켜준다.
보기엔 복잡해보이지만
간단하게 그냥 Count 값을 이용해 조절 해나가면서
할당 해주는 코드이다.
SoldierLink는 도대체 어떻게 채워지는 지를 해당 코드를 통해 알 수 있었다.
이 코드를 먼저 보고 SquadMoveSystem 코드를 보는게 좋았을 것 같다.
드디어 코드리뷰가 끝났다.
여전히 어렵지만... 그래도 이해도가 많이 올라간 느낌이다.
다음엔 어떤 걸 리뷰하면서 성장 할지 기대된다.
'Game Dev > Unity ECS & DOTS' 카테고리의 다른 글
6. 원하는 위치에 프리팹 생성 해보기- ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (0) | 2024.11.20 |
---|---|
5. 뭐가 다른데? IJobEntity, IJobChunk, IASpect - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (4) | 2024.11.19 |
[ECS&DOTS] NSprite 머리 쪼개기 (2) 우선, 샘플 프로젝트부터 공략하자. (2) (8) | 2024.11.11 |
[ECS&DOTS] NSprite 머리 쪼개기 (1) 우선, 샘플 프로젝트부터 공략하자. (1) | 2024.11.08 |
4. ECS 워크플로 이해하기- ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트 (4) | 2024.11.06 |