5. 뭐가 다른데? IJobEntity, IJobChunk, IASpect - ECS & DOTS 완전 정복하기. Feat. 안 반가운 큐브 프로젝트
이번 포스팅에서는
Hello Cube 샘플 프로젝트에서 2번, 3번, 5번에 대해 다뤄볼려고 합니다.
왜 애네를 묶느냐? 하면
뭐가 바뀌는지 눈으로 보여주는게 없는.. 성의 없는 샘플 예제 3대장,
이기 때문..
근데 정말 알아둬야할 중요한 친구들이다.
아 몰라! Docs부터 읽어봅시다.
우선, IJobEntity부터..
IJobEntity Any type which implements this interface and also contains an Execute() method (with any number of parameters) will trigger source generation of a corresponding IJobChunk or IJobEntity type. The generated job in turn invokes the Execute() method on the IJobEntity type with the appropriate arguments. Remarks If any SharedComponent, or ManagedComponent is part of the query, __EntityManager is generated. It's needed to access the components from the batch. This also means, that type of job has to run in main thread. |
IJobEntity 여러 파라미터를 가질 수 있는 Execute() 메소드를 포함한 이 인터페이스를 구현한 모든 타입. 이 타입은 IJobChunk 혹은 IJobEntity 타입의 Source Generation을 유발한다. job이 생성되면서 적절한 파라미터를 가진 Execute() 메소드를 실행 시킨다. Remakrs, 만약 SharedComponent, ManagedComponent가 쿼리의 일부라면, EntityManager가 생성되며, EntityManager는 배치 단에서 컴포넌트들에 접근하는데 필요하다. 다시 말해서 이런 타입의 IJobEntity는 Main Thread에서 동작해야 한다. |
정리해보자.
(1) Interface로서, Execute() 메소드를 포함하여 구현해야 한다.
(2) Source Generation을 유발한다. (Compile 단계에서 Source를 생성하여 동작하도록 함.)
(3) IJobChunk와 연관이 있다. (IJobChunk로도 Source Generation이 될 수 있다.)
(4) 해당 Job을 생성하면, Execute() 메소드를 Invoke한다. (스케줄링 할 때, 실행된다.)
(5) 특정 컴포넌트들이 포함된다면, EntityManager가 생성된다.
(6) 특정 컴포넌트들이 포함된다면, Main Thread에서 동작되어야 한다. (EntityManager에 의해 관리되어야 한다.)
꽤 많은 정보를 얻을 수 있지만.. 숨겨진 내용도 있다.
예제의 주석문을 살펴보자.
// In source generation, a query is created from the parameters of Execute(). // Here, the query will match all entities having a LocalTransform, PostTransformMatrix, and RotationSpeed component. // (In the scene, the root cube has a non-uniform scale, so it is given a PostTransformMatrix component in baking.) |
SourceGeneration 할 때, Query가 파라미터대로 생성되어진다. 예제에 나온대로 LocalTransform/PostTransformMatrix/RotationSpeed 컴포넌트를 가진 모든 엔티티들을 매칭할 것이다. (Scene에서는, RootCube가 uniform하지 않은 스케일을 가지고 있기 때문에 PostTransformMatrix 컴포넌트가 베이킹된다.) |
이건 NSprite 코드 리뷰를 하면서 해당 사실을 몰라서 갸우뚱 했던 부분인데
Execute() 메소드에 들어갈 파라미터들을 정의하면, 실제로는 Query 역할을 하게 된다.
즉, Execute(SharedComponent _shard) 인 Job이 있다면, _shard를 쿼리하며, EntityManager가 생성된다.
정리한 내용들에 대해서는 자세한 내용은 후술하기로 하며, 다른 2가지 Docs부터 읽어보도록 하자.
IJobChunk is a type of IJob that iterates over a set of chunks. For each chunk the job runs on, the job code receives an ArchetypeChunk instance representing the full chunk, plus a bitmask indicating which entities in the chunk should be processed. Remarks Schedule or run an IJobChunk job inside the OnUpdate() function of a SystemBase implementation. When the system schedules or runs an IJobChunk job, it uses the specified EntityQuery to select a set of chunks. The entities in each chunk are examined to determine which have the necessary components enabled, according to the EntityQuery provided at schedule time. The job struct's Execute function is called for each chunk, along with a bitmask indicating which entities in the chunk should be processed. To pass data to your Execute function (beyond the Execute parameters), add public fields to the IJobChunk struct declaration and set those fields immediately before scheduling the job. You must always pass the component type information for any components that the job reads or writes using a field of type, ComponentTypeHandle<T>. Get this type information by calling the appropriate GetComponentTypeHandle<T>(bool) function for the type of component. |
IJobChuk는 Chunks 단위로 반복되어지는 IJob의 타입이다. 각 청크가 Job이 실행되는 동안, Job 코드는 모든 Chunk를 나타내는 ArchetypeChunk 인스턴스와 더불어서, Chunk안에서 어떤 엔티티들이 작업되어야 하는지 알려주는 bitmask가 존재한다. Remarks, SystemBase 구현체의 OnUpdate() 함수안에서 IJobChunk를 실행하거나 스케듈링한다. System이 IJobChunk를 스케듈링하거나 실행할 때, Chunk 모음을 선택해주는 특정한 EntityQuery를 사용한다. 해당 Schedule Time에서 제공되어진 EntityQuery에 따라서, 각 Chunk에서 엔티티들은 필요한 컴포넌트들을 사용할 수있는지 조사되어진다. 어떤 엔티티들이 해당 청크안에서 처리되어야 할 지 알려주는 bitmask를 따라서 Job 구조체의 Execute 함수가 각 청크에서 실행된다. 미리 정의되어있는 파라미터들 (ArchetypeChunk / bool / in v128) 외에 원하는 데이터를 이용하려면, IJobChunk 구조체 정의단에서 public field로서 추가하고, Job을 Scheduling 할 때, 즉시 set 되어진다. 사용자는 반드시 컴포넌트 타입 정보를 읽고 쓰기 형태인 ComponentTypeHandle<T> 형태로 받아와야 한다. 그럼으로써, ComponentType의 정보를 가져올 수 있게 된다. |
정말 어렵다. 용도를 정확히 모르니 한번에 탕! 하고 이해되는 사람이 있을까..
예제만 봐도 어렵고, Docs만 봐도 어렵다..
정리해보면..
(1) IJobChunk는 IJob형태이면서 Chunk 단위로 반복되어지는 타입이다.
(2) IJobChunk는 전체 chunk를 대표하는 ArchetypeChunk 인스턴스와 어떤 엔티티들을 사용 할 지 알려주는 bitmask가 존재한다.
(3) System의 OnUpdate() 메소드안에서 사용된다.
(4) IJobChunk를 사용 할 때, 어떤 엔티티들을 사용할지 알려주는 EntityQuery를 입력으로 넣어준다.
(5) IJobChunk가 사용되는 시점에서, EntityQuery를 통해 매칭한 Entity들이 사용 가능한지를 체크한다.
(6) bitmask에 의해, 해당 Chunk에서 매칭되는 엔티티들만 IJobEntity를 실행한다.
(7) 필요한 파라미터 외에, 사용할 컴포넌트들은 IJobChunk를 정의단에다가 구현해야하며, ComponentTypeHandle<T> 형태로 정의되어야 한다.
정리를 해보니 약간의 사용 방법이 보일랑 말랑한다.
해당하는 모든 엔티티들을 불러와서 일괄처리 (IJobEntity)하지 않고,
처리할 엔티티와 해당 Job이 불필요한 엔티티들을 구분하여 Job을 처리하겠다는 의미로 받아들이면 될 것 같다.
특이한 점으로는 EntityQuery를 수동으로 줘야 한다. (IJobEntity는 필요한 Query를 설정해서 실행시켰다.)
엔티티 필터 역할도 하는걸까..
https://discussions.unity.com/t/difference-between-ijobentity-ijobchunk/896931
IJobEntity와 IJobChunk의 비교에 관한 글
(결론은 거의 같으니 필요에 의해 구분해서 쓰자.)
IAspect
엄밀히 말하자면, 이 친구를 위 2개와 동일한 포스팅에서 설명하는 건 잘못되었다..
Base interface for Aspects, a higher level combination for components. Remarks Implement IAspect on a struct with any number of RefRW<T> fields. A RefRW<T> field may use these attributes: [OptionalAttribute] Make the component optional. Field IsValid will be true if the component is present on the current entity. [ReadOnlyAttribute] Make the component read-only when building an entity query that uses the aspect. The field ValueRW will break the safety checks. Use ValueRO instead. |
컴포넌트들의 더 상위 단계의 조합인, 양상들을 위한 인터페이스이다. IAspect는 구조체 위에 RefRW<T> 필드들을 가지고 구현되어진다. RefRW<T> 필드는 아래 Attribute들을 사용하여 정의할 수 있다. [OptionalAttribute] : 해당 Attribute를 가진 컴포넌트는 선택 사항이다. IsValid에 의해 현재 엔티티가 해당 컴포넌트에 대해서 사용 가능한지 알 수 있다. [ReadOnlyAttribute] : 해당 컴포넌트를 ReadOnly로 바꿔준다. ValueRW는 Safety Check를 깨트릴 수 있으니 되도록, ValueRO로 대신 사용해야한다. |
보자마자 드는 생각은 일종의 클래스 아닐까?
정리부터 하면,
(1) 컴포넌트들의 조합으로 이루어진 상위 단계의 인터페이스이다.
(2) 가져올 컴포넌트들은 RefRW<T> 필드로 구현된다.
(3) 2가지 Attribute가 제공된다.
정도이다.
예제를 봐야, 뭐가 다른지 이해가 될텐데..
구현된 예제를 보면, Query도 따로 할 필요없다.
Aspect안에서 다 진행하는 걸로 볼 수 있다.
public partial struct RotationSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteAspects>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
double elapsedTime = SystemAPI.Time.ElapsedTime;
// Rotate the cube directly without using the aspect.
// The query matches all entities having the LocalTransform and RotationSpeed components.
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
{
transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
}
// Rotate the cube using the aspect.
// The query will include all components of VerticalMovementAspect.
// Note that, unlike components, aspect type params of SystemAPI.Query are not wrapped in a RefRW or RefRO.
foreach (var movement in
SystemAPI.Query<VerticalMovementAspect>())
{
movement.Move(elapsedTime);
}
}
}
// An instance of this aspect wraps the LocalTransform and RotationSpeed components of a single entity.
// (This trivial example is arguably not a worthwhile use case for aspects, but larger examples better demonstrate their utility.)
readonly partial struct VerticalMovementAspect : IAspect
{
readonly RefRW<LocalTransform> m_Transform;
readonly RefRO<RotationSpeed> m_Speed;
public void Move(double elapsedTime)
{
m_Transform.ValueRW.Position.y = (float)math.sin(elapsedTime * m_Speed.ValueRO.RadiansPerSecond);
}
}
OnUpdate에서 첫번째 Foreach는 IAspect를 사용하지 않았을 때이고,
두번째 Foreach는 IAspect를 그대로 실행할 뿐인 코드이다.
임의로 정해놓은 컴포넌트와 그 결과를 보여주고 있다.
이건 참 쉽다..
있어도 그만, 없어도 그만인 기능이라 그런가?
이유는 잘 모르지만, 유니티 6 이후로 Deprecated 가능성이 있다한다.
반대하는 사람도 있는 걸 보니.. 그냥 없애진 않겠지..
IAspect 쩌리는 넘어가고,
IJobEntity와 IJobChunk의 예제 코드만 보고 포스팅을 마치려한다.
코드 자체는 위의 내용을 보고 리뷰해보면 어렵지 않다.
IJobEntity
using PlasticPipe.PlasticProtocol.Messages;
using System.Numerics;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace HelloCube.JobEntity
{
public partial struct RotationSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteIJobEntity>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var job = new RotateAndScaleJob
{
deltaTime = SystemAPI.Time.DeltaTime,
elapsedTime = (float)SystemAPI.Time.ElapsedTime
};
job.Schedule();
}
}
[BurstCompile]
partial struct RotateAndScaleJob : IJobEntity
{
public float deltaTime;
public float elapsedTime;
// In source generation, a query is created from the parameters of Execute().
// Here, the query will match all entities having a LocalTransform, PostTransformMatrix, and RotationSpeed component.
// (In the scene, the root cube has a non-uniform scale, so it is given a PostTransformMatrix component in baking.)
void Execute(ref LocalTransform transform, ref PostTransformMatrix postTransform, in RotationSpeed speed)
{
transform = transform.RotateY(speed.RadiansPerSecond * deltaTime);
postTransform.Value = float4x4.Scale(1, math.sin(elapsedTime), 1);
}
}
}
IJobEntity 구현부를 보면,
Execute 안에서 필요한 Component들을 나열해서 받으려고 하고 있다.
그리고 OnUpdate에서 어딜봐도 Job에 해당 컴포넌트들을 넣어주는 곳이 없다.
즉, Compoennt들을 자동으로 쿼리해서 계산에 사용한다는 뜻이다.!
IJobChunk
using System.Diagnostics;
using Unity.Assertions;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
namespace HelloCube.JobChunk
{
public partial struct RotationSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExecuteIJobChunk>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed, LocalTransform>().Build();
var job = new RotationJob
{
TransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(),
RotationSpeedTypeHandle = SystemAPI.GetComponentTypeHandle<RotationSpeed>(true),
DeltaTime = SystemAPI.Time.DeltaTime
};
// Unlike an IJobEntity, an IJobChunk must be manually passed a query.
// Furthermore, IJobChunk does not pass and assign the state.Dependency JobHandle implicitly.
// (This pattern of passing and assigning state.Dependency ensures that the entity jobs scheduled
// in different systems will depend upon each other as needed.)
state.Dependency = job.Schedule(spinningCubesQuery, state.Dependency);
}
}
[BurstCompile]
struct RotationJob : IJobChunk
{
public ComponentTypeHandle<LocalTransform> TransformTypeHandle;
[ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
public float DeltaTime;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
in v128 chunkEnabledMask)
{
// The useEnableMask parameter is true when one or more entities in
// the chunk have components of the query that are disabled.
// If none of the query component types implement IEnableableComponent,
// we can assume that useEnabledMask will always be false.
// However, it's good practice to add this guard check just in case
// someone later changes the query or component types.
Assert.IsFalse(useEnabledMask);
var transforms = chunk.GetNativeArray(ref TransformTypeHandle);
var rotationSpeeds = chunk.GetNativeArray(ref RotationSpeedTypeHandle);
for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)
{
transforms[i] = transforms[i].RotateY(rotationSpeeds[i].RadiansPerSecond * DeltaTime);
}
}
}
}
IJobChunk의 구현부를 보자.
ComponentTypeHandle을 통해서 원하는 컴포넌트들을 잔뜩 가져오고 있는걸 알 수 있다.
대신에 Execute에서는 미리 정의된 파라미터들만 넣어주고 있다.
주석문에 나온대로 사용할 수 없는 엔티티 쿼리를 넣으면, bool은 False가 뜬다.
수동으로 쿼리는 넣어주고 있으며, 작업이 끝나면 state.Dependency를 남기고 있다.
일괄적으로 Entity를 다루는 IJobEntity와 다르게, 필터를 거치는 특정 엔티티들만을 사용 할 수 도 있으므로
하나 이상의 System들에 의해 처리되어지는 엔티티들에게 종속성을 부여하는게 필요한 것으로 보인다.
이상,
샘플 프로젝트 (2), (3), (5)번에 대해 알아보았다.
아는만큼 보인다고, 집중적으로 공부하면 할수록 조금씩 눈에 틔는 기분이 들것이다.
처음 ECS를 접하면, 정글속에 들어온 기분이고 자료도 도움도 없이 더듬더듬 빠져나가는 과정인 것 같다.
참고 자료,
https://docs.unity3d.com/Packages/com.unity.entities@1.3/api/index.html
Entity Component System API reference | Entities | 1.3.5
Entity Component System API reference This page contains an overview of some key APIs that make up Unity's Entity Component System (ECS). Attributes Description UpdateInGroup Defines the ComponentSystemGroup that a system should be added to. UpdateBefore S
docs.unity3d.com
https://github.com/Unity-Technologies/EntityComponentSystemSamples
GitHub - Unity-Technologies/EntityComponentSystemSamples
Contribute to Unity-Technologies/EntityComponentSystemSamples development by creating an account on GitHub.
github.com