Unity DOTS/Dots Custom Manual

UNITY DOTS - IJobEntityBatch 에 대해 알아보자

개양반 2022. 7. 27.
728x90

IJobEntityBatch에서 I는 인터페이스를 뜻하는 것일테고 Job은 특정한 단일 작업을 수행하는 작은 단위라는 것이고 Entity는 그릇이고 그럼 Batch는 무엇일까? 

사전 의미로는 일괄적으로 처리되는 집단[무리] 동사로는 (일괄처리를 위해) 함께 묶다

 

Chunk는 여러 Batch가 존재하게 되는데 IJobEntityBatch는 Batch를 활용해서 데이터를 처리한다.

아키타입이 여럿 청크에 저장되고 청크에는 배치라는 일괄처리 그룹이 존재한다.

 


IJobEntityBatch 구조체 살펴보기

IJobEntityBatch를 상속받아서 구조체를 정의한다.

public struct UpdateTranslationFromVelocityJob : IJobEntityBatch
{
    public ComponentTypeHandle<VelocityVector> velocityTypeHandle;
    public ComponentTypeHandle<Translation> translationTypeHandle;
    public float DeltaTime;

    [BurstCompile]
    public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
    {
        NativeArray<VelocityVector> velocityVectors =
            batchInChunk.GetNativeArray(velocityTypeHandle);
        NativeArray<Translation> translations =
            batchInChunk.GetNativeArray(translationTypeHandle);

        for(int i = 0; i < batchInChunk.Count; i++)
        {
            float3 translation = translations[i].Value;
            float3 velocity = velocityVectors[i].Value;
            float3 newTranslation = translation + velocity * DeltaTime;

            translations[i] = new Translation() { Value = newTranslation };
        }
    }
}

Execute(ArchetypeChunk batchInChunk, int batchIndex) 설명

Execute는 구조체가 만들어지면 자동으로 실행되는 함수이다. 매개변수로 batchInChunk, batchIndex를 받는데 

batchInChunk 는 IJobEntityBatch 구조체를 만들때 Query를 통해 설정한 조건에 맞는 청크의 Batch들이 들어오게 되고 batchIndex는 검색된 Batch들의 식별코드이다. EntityCommand로 병렬 쓰기를 할 때, batchIndex가 sortKey가 될 수 있다.

 

Job이 접근하는 데이터 선언

위 코드에서 ComponentTypeHandle 라는 녀석을 볼 수 있는데 Job의 Excute에서 사용할 수 있는 데이터를 선언한 것이다. Excute에서 사용할 수 있는 필드는 4가지가 존재한다.

ComponentTypeHandle : Excute 함수로 들어온 batchInChunk는 여러 데이터 타입이 들어오게 되는데 ComponentTypeHandle 으로 특정 데이터 핸들을 만들어서 batchInChunk에서 데이터를 가져올때 사용한다. 즉, ComponentTypeHandle 은 batchInChunk 안의 데이터 중에 특정데이터를 가져오는데 사용하는 것이다.

ComponentDataFromEntity, BufferFromEntity : Excute에 들어온 batchInChunk와 상관없이 모든 Entity에 대한 데이터를 조회할 수 있다. 몰론 그만큼 성능상 안 좋은 부분이 있으니 필요한 경우에만 사용해야 한다. 

Other fileds : Job을 실행할 때 다른 정보가 필요한 경우 Job 구조체에서 필드를 정의한 다음 Excute 내부에 있는 필드에 접근할 수 있습니다. Job을 scheduling 할때만 값을 설정할 수 있으며 해당 값은 모든 Batch에 동일하게 유지된다. 예로 Time.delta이 Job에서 필요한 경우 Job 구조체에 float dt를 만들고 Job을 예약할 때 Time.delta 값을 dt에 전달할 수 있다.

Output field : Job에서 쓰기 가능한 Entity Compoent 또는 Buffer를 업데이트하는 것 외에도 Job Struct에 대해 선언된 기본 컨테이너 필드에 쓸 수도 있습니다. 이러한 필드는 NativeArray와 같은 기본 컨테이너여야 합니다. 다른 데이터 유형은 사용할 수 없습니다.

 

NativeArray??

Job의 결과가 각 복사본 내에 격리되어 메인스레드에서 접근할 수가 없다. 그래서 NativeContainer라는 공유 메모리 타입에 저장해서 메인스레드에서도 해당 결과값을 값 복사가 아닌 참조로 사용할 수 있게 한다. 간단하게 말해서 Job에서는 배열 말고 NativeContainer를 사용하면 된다. NativeContainer에는 NativeList, NativeHashMap, NativeMultiHashMap, NativeQueue 이 있다. 

 


Job Scheduing 하기

Job struct를 정의했으면 이번에는 해당 Job을 예약해서 멀티스레드에서 데이터를 처리하게 해보자.

public partial class UpdateTranslationFromVelocitySystem : SystemBase
{
    EntityQuery query;

    protected override void OnCreate()
    {
        // Query 만들기
        var description = new EntityQueryDesc()
        {
            All = new ComponentType[]
                   {ComponentType.ReadWrite<Translation>(),
                    ComponentType.ReadOnly<VelocityVector>()}
        };
        query = this.GetEntityQuery(description);
    }

    protected override void OnUpdate()
    {
        // Job 구조체 생성
        var updateFromVelocityJob
            = new UpdateTranslationFromVelocityJob();

        // Component Handle 만들기
        updateFromVelocityJob.translationTypeHandle
            = this.GetComponentTypeHandle<Translation>(false);
        updateFromVelocityJob.velocityTypeHandle
            = this.GetComponentTypeHandle<VelocityVector>(true);

        // DeltaTime 값을 Job 구조체의 필드로 전달
        updateFromVelocityJob.DeltaTime = World.Time.DeltaTime;

        // Job 예약하기
        this.Dependency
            = updateFromVelocityJob.ScheduleParallel(query, this.Dependency);
    }

진행 순서는 다음과 같습니다.

1. Query를 만들어서 JobBatch에서 작업할 데이터 타입을 설정합니다.

2. IJobBatch 를 생성합니다. 

3. IJobBatch에서 사용할 데이터의 Handle을 만들고 Other fileds(여기서는 Time.Deltatime) 값을 전달합니다.

4. Job 을 예약해서 실행할 준비가 완료되면 실행시킵니다. 

 

GetComponentTypeHandle<T>(false);

여기서 false는 읽기 전용이라는 의미이다. 유니티의 DOTS는 멀티스레드 환경에서 실행되므로 데이터가 읽기만 하는지, 쓰기도 하는지 꼭 정의 해줘야 한다.

 

 

this.Dependency
            = updateFromVelocityJob.ScheduleParallel(query, this.Dependency); 

ScheduleParallel는 멀티스레드에서 병렬로 처리해 달라는 의미이다. 그럼, Dependency가 무엇일까? 종속성이라는 것인데 무엇에 쓰는 물건일까? 일단, Job은 멀티스레드 환경에서 실행될 수 있다는 것이다. 여러 스레드에서 쓰기, 읽기가 처리되니 여러 스레드에서 하나의 데이터에 동시에 접근할 때 경쟁 문제라는 것이 발생할 수 있다. 이러한 경쟁 조건을 방지하기 위해 Job Scheduler는 시스템의 Job이 실행되기 전에 시스템이 의존하는 모든 작업이 완료되었는지를 확인합니다. 이때 사용되는 것이 바로 Dependency이다. 각 Job들이 읽고 쓰는 구성 요소를 기반으로 Dependency를 업데이트하여 각 Job들이 경쟁문제가 발생하지 않도록 한다.

댓글

💲 추천 글