ECS System이 데이터 즉, Component에 접근하는 다양한 방법을 정리해봤다. 여기서 System은 범용적인 의미의 시스템이 아니라 ECS의 3대 구성요소 Entity, Component, System의 그 System이다. 아래 유니티 매뉴얼 페이지를 많이 참고했다.
https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/entity_iteration_job.html
데이터 접근 방법은 멀티스레드 사용 여부에 따라 다음과 같이 크게 둘로 나뉜다.
- JobComponentSystem 상속 (멀티쓰레드 사용)
- IJobForEach 인터페이스 구현
- IJobForEachWithEntity 인터페이스 구현
- IJobChunk 인터페이스 구현
- 수동으로 반복(Manual iteration) 처리
- ComponentSystem 상속 (메인쓰레드만 사용)
- ForEach delegate를 이용한 반복 처리
먼저 IJobForEach 인터페이스를 구현하는 방법을 살펴보자. 다섯 가지 방법 중 가장 간단하면서 효율적인 방법으로 개별 Enitity 단위로 데이터(이하 Component)를 처리한다. 프로그램이 실행되면 ECS 프레임웍이 조건에 맞는 모든 Component를 찾는다. 그리고 Enitity 각각에 대해 Job 구조체의 Excute() 메서드를 호출한다. 다음은 IJobForeach를 사용해서 간단한 System을 구현한 예제 코드다.
public class RotationSpeedSystem : JobComponentSystem
{
// [BurstCompile] attribute를 사용하면 job 코드 컴파일 시 Burst 컴파일러가 사용된다.
[BurstCompile]
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
{
public float DeltaTime;
// [ReadOnly] attribute를 사용해서 job이 RotationSpeed를 읽기만 하고 쓰지 않는다고 job 스케쥴러에게 알려준다.
public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed)
{
// RotationSpeed를 속도 값으로 취해서 up 벡터 방향으로 enitity를 회전시키는 로직이다.
rotationQuaternion.Value = math.mul(math.normalize(rotationQuaternion.Value), quaternion.AxisAngle(math.up(), rotSpeed.RadiansPerSecond * DeltaTime));
}
}
// OnUpdate는 메인 쓰레드에서 실행된다.
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new RotationSpeedJob()
{
DeltaTime = Time.deltaTime
};
return job.Schedule(this, inputDependencies);
}
}
다음으로 IJobForEachWithEntity 인터페이스를 구현해서 Component에 접근하는 방법이다. IJobForEach 보다 조금 복잡하다. 대신 Component뿐만 아니라 Entity 오브젝트에 대한 접근 권한과 Entity의 배열 index를 얻을 수 있다. 따라서 Entity에 Component를 추가하거나 제거하는 등의 작업을 할 수 있다. 나아가 Entity를 생성하거나 제거할 수도 있다. 다만 이런 작업들은 경쟁 상태(Race Condition)을 피하기 위해 Job 안에서 이뤄지지 않는다. EntityCommandBuffer라는 것에 의해 메인쓰레드에서 처리된다. 이 부분은 추후 다른 포스트에서 자세히 다루겠다.
다음은 IJobChunk 인터페이스를 구현해서 Component에 접근하는 방법이다. 개별 Entity가 아닌 Chunk라는 메모리 블록 단위로 데이터를 반복 처리한다. 최대한의 성능을 보장하면서 보다 복잡한 조건을 처리할 수 있다. 다음 예제 코드와 같이 Excute() 메서드 내부에서 for 루프를 통해 Chunk의 각 원소에 접근할 수 있다.
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
for (var i = 0; i < chunk.Count; i++)
{
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
// 해당 Entity를 up 벡터 방향으로 RotationSpeed 속도로 회전시킨다.
chunkRotations[i] = new RotationQuaternion
{
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
}
}
Job을 사용하는 경우 중 마지막 방법인 '수동으로 반복(Manual iteration) 처리'하는 방법은 따로 설명하지 않겠다. 포스트 앞 부분에 소개한 링크의 문서를 참고하기 바란다.
이제 Job을 사용하지 않는 경우를 살펴보자. JobComponentSystem이 아닌 ComponentSystem을 상속받아서 System 클래스를 구현한다. 구현 방법은 ForEach 루프를 돌면서 반복 처리하는 방법 하나뿐이다. Job을 사용하는 경우 다양한 구현 방법이 제공되는 것과 비교된다. 다음 예제와 같이 ComponentSystem의 Entities.ForEach 델리게이트 함수를 사용해서 데이터를 반복 처리한다.
public class RotationSpeedSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach( (ref RotationSpeed rotationSpeed, ref RotationQuaternion rotation) =>
{
var deltaTime = Time.deltaTime;
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
});
}
// .. 이하 생략..
}
Job을 사용하지 않기 때문에 당연히 메인쓰레드에서만 실행된다. 구현이 간단한 대신 ECS의 성능 향상 효과를 제대로 볼 수 없다. 하지만 메인 스레드에서만 작동하는 작업을 구현할 때는 이 방법을 사용할 수밖에 없다.
이상으로 ECS System에서 데이터, 즉 Componet에 접근하는 다양한 방법을 간단하게나마 정리해봤다. 끝.
'개발 > Unity' 카테고리의 다른 글
[Unity] 콘솔 창 Error Pause 옵션 (408) | 2020.03.23 |
---|---|
[Unity][DOTS] Unity.Mathematics 라이브러리 사용하기 (6) | 2020.01.28 |
[Unity][DOTS] ECS System에서 기존(Non-ECS) Component 제어하기 (0) | 2019.09.17 |
[Unity] NGUI + Spine 연동 (2) | 2019.08.30 |
[Unity][DOTS] 계층 구조의 GameObject를 ECS Entity로 변환하기 (0) | 2019.08.23 |
댓글