Unity DOTS/ECS Sample Projtect

ECS Sample Project #2 IJobEntityBatch

개양반 2022. 7. 28.

지난 강좌 보기

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #1 ForEach

 

선행 학습

IJobEntityBatch란?

[Unity DOTS/Dots Custom Manual] - UNITY DOTS - IJobEntityBatch 에 대해 알아보자

 

EntityQuery란?

[Unity DOTS/Dots Custom Manual] - UNITY DOTS - EntityQuery에 대해 알아보자

 


오늘도 #1 ForEach 처럼 도형만 빙글빙글 도는 예제를 진행할 것이다. 차이점은 #1 은 ForEach를 통해 반복문을 실행했다면 오늘은 IJobEntityBatch를 통한 구현이라는 것과 Runtime 중에 Component를 Entity에게 추가하는 것 2가지가 다르다.


새로운 씬을 만들고 이름을 02. IJobEntityBatch 로 원하는 폴더에 저장합니다. Scripts 폴더에 2. IJobEntityBatch 폴더를 만들고 자식 폴더로 Component, System, Authoring 을 추가합니다.


Component 만들기

1. RotationSpeed_IJobEntityBatch

2. IJobEntityBatch\Component 폴더에 우클릭 > Create > ECS > Runtime Component Type 을 눌러 파일을 생성합니다. 파일의 이름은 RotationSpeed_IJobEntityBatch로 만들고 아래의 코드를 작성합니다.

[Serializable]
public struct RotationSpeed_IJobEntityBatch : IComponentData
{
    public float RadiansPerSecond;
}

기존에는 에디터모드에서 게임오브젝트에 추가한 Component가 Runtime이 되어 GameObject가 Entity가되면 자동으로 해당 Entity의 Component가 되었는데

이번에는 런타임 중에 Entity에게 Component를 추가할 것이기에 GenerateAuthoringComponent 어트리뷰트가 없습니다. 


Authoring 만들기

1. RotationSpeedAuthoring_IJobEntityBatch

[2. IJobEntityBatch\Authoring] 폴더에 우클릭 > Create > ECS > Authoring Component Type 을 눌러 파일을 생성합니다. 

파일의 이름은 RotationSpeedAuthoring_IJobEntityBatch 로 만들고 아래의 코드를 작성합니다.

using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;


[ConverterVersion("joe", 1)]
[DisallowMultipleComponent]
public class RotationSpeedAuthoring_IJobEntityBatch : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360.0F;
    
    // this.gameObject가 Entity가 되면 Convert가 호출된다.
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        // Runtime이 되면 Convert가 호출된다.
        // RotationSpeed_IJobEntityBatch 데이터를 만들고
        var data = new RotationSpeed_IJobEntityBatch { RadiansPerSecond = math.radians(DegreesPerSecond) };

        // EntityManager를 위에서 만든 데이터를 해당 Entity에 넣는다.
        dstManager.AddComponentData(entity, data);
    }
}

코드설명

RotationSpeedAuthoring_IJobEntityBatch Script파일은 GameObject에 추가할 파일입니다.  RotationSpeedAuthoring_IJobEntityBatch가 추가된 GameObject는 Runtime이 되면 Convert to Entity 옵션에 의해 Entity가 되고 Convert 함수가 호출됩니다. Convert에서 추가할 데이터를 만들어서 EntityManager를 통해 해당 Entity에게 데이터를 추가하는 코드입니다.

* EntityManager?

World에는 하나의 EntityManager가 존재하며 Entity와 Componet를 관리( 생성, 읽기, 업데이트 및 파괴)합니다. 오직 기본 스레드에서만 작동합니다. 

[DisallowMultipleComponent] : 하나의 GameObject에 RotationSpeedAuthoring_IJobEntityBatch를 두개 이상 중복으로 추가 할 수 없도록 제한합니다.

[ConverterVersion("joe", 1)] : 백그라운드 가져오기는 Authoring 데이터 및 해당 종속성이 변경되면 트리거됩니다. 그러나, 변환 코드 자체는 종속성을 가질 수 없습니다. 이 문제는 코드가 변경될 때만 나타나게 되므로 특히 치명적이지만 변환 코드를 편집할 때 Authoring Component와 Authoring scenes도 함께 편집하는 것이 일반적입니다. 따라서 일반적으로 변경이 이루어진 컴퓨터에서는 제대로 작동하지만 다른 컴퓨터에서는 다시 가져오지 못합니다. 

[ConverterVersion("Fabrice", 2)]
public class SomeAuthoringComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager
        , GameObjectConversionSystem conversionSystem)
    {
        var translation = dstManager.GetComponentData<Translation>(entity);
        translation.Value.y += 1; // Y offset
        dstManager.SetComponentData(entity, translation);
    }
}

위 코드에서 translation.Value.y += 1; 로 변경한 뒤에 ConverVersion 속성 값이 변경되지 않는다면 백그라운드 다시 가져오기를 트리거하지 않습니다. 이러한 문제점을 해결하기 위해 ConverterVersion["작성자", 버전넘버] 속성이 있는 겁니다. 간단하게 말해서 Convert하는 코드가 있으면 ConverterVersion를 추가하는 습관을 들이면 좋다.


System 만들기

1. RotationSpeedSystem_IJobChunk

[2. IJobEntityBatch\System] 폴더에 우클릭 > Create > ECS > System 을 눌러 파일을 생성합니다. 파일 이름은 RotationSpeedSystem_IJobChunk로 만듭니다.

 

1-1 EntityQuery 만들기

RotationSpeedSystem_IJobEntityBatch 스크립트에 아래의 코드를 추가한다.

    EntityQuery m_Query;

    protected override void OnCreate()
    {
        // 시스템이 생성되면 조회할 Entity의 Query를 만듭니다.
        m_Query = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<RotationSpeed_IJobEntityBatch>());
    }

#코드 설명

OnCreate는 기존 Monobehaviour 에서의 Awake와 비슷하게 동작한다. 시스템이 생성되면 OnCreate가 호출되고 OnCreate에서 EntityQuery를 만든다. 읽기만 하는 데이터는 ComponentType.ReadOnly를 추가하여 성능 이점을 최대한 살린다.

 

1-2 IJobEntityBatch 구조체 정의

RotationSpeedSystem_IJobEntityBatch의 OnCreate함수 아래에 아래의 코드를 작성한다.

    // [BurstCompile] 속성을 사용하여 Burst로 Job을 컴파일합니다.상당한 속도 향상을 볼 수 있으므로 시도해 보십시오!
    [BurstCompile]
    struct RotationSpeedJob : IJobEntityBatch
    {
        public float DeltaTime;

        // Handle 필드
        public ComponentTypeHandle<Rotation> RotationTypeHandle;
        [ReadOnly] public ComponentTypeHandle<RotationSpeed_IJobEntityBatch> RotationSpeedTypeHandle;

        public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
        {
            // batchInChunk 에서 아래 Handle과 같은 타입의 데이터를 NativeArray로 얻어온다.(참조)
            var chunkRotations = batchInChunk.GetNativeArray(RotationTypeHandle);
            var chunkRotationSpeeds = batchInChunk.GetNativeArray(RotationSpeedTypeHandle);

            for (int i = 0; i < batchInChunk.Count; i++)
            {
                var rotation = chunkRotations[i];
                var rotationSpeed = chunkRotationSpeeds[i];

                chunkRotations[i] = new Rotation
                {
                    Value = math.mul
                    (
                        math.normalize(rotation.Value), 
                        quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime)
                    )
                };
            }
        }
    }

#코드 설명

정의한 구조체를 시스템에서 생성하고 Job을 예약할 때, 위에서 작성한 EntityQuery를 전달하면 Excute의 ArchtypeChunk에 EntityQuery에서 조회한 batchInChunk그룹이 전달된다. ComponentTypeHandle로 batchInChunk 안의 어떤 데이터를 사용할지 설정할 수 있다. 

 

1-3 OnUpdate 구현

RotationSpeedSystem_IJobEntityBatch의 RotationSpeedJob 구조체 아래에 아래의 코드를 작성한다.

    protected override void OnUpdate()
    {
        // 핸들 얻어오기
        var rotationType = GetComponentTypeHandle<Rotation>();
        var rotationSpeedType = GetComponentTypeHandle<RotationSpeed_IJobEntityBatch>(true);

        var job = new RotationSpeedJob()
        {
            // 필드에 데이터 전달
            RotationTypeHandle = rotationType,
            RotationSpeedTypeHandle = rotationSpeedType,
            DeltaTime = Time.DeltaTime
        };

        // 작업 예약. Dependency(의존성)을 전달하여 여러 스레드에서 쓰고, 읽기로 발생하는 경쟁 문제를 해결한다.
        Dependency = job.ScheduleParallel(m_Query, Dependency);
    }

핸들을 얻어오고 Job 구조체를 만든다면 Job구조체의 필드를 채운다. 구조체를 만들었으면 Job을 예약해서 준비가 완료되면 처리되게 한다. Job을 예약할때 Dependency를 전달하는 이유는 Job이 멀티스레드에서 실행될 경우 읽고, 쓰기에서 경쟁 문제가 발생하는 문제를 해결하기 위함이다. Dependency를 전달하면 해당 Job이 의존하는 모든 Job이 완료되었는지 확인하고 실행하게 된다.


유니티 에디터 작업

#1 ForEach 처럼 큐브 두 개를 자식과 부모 관계로 만들고 아래 사진처럼 포개 놓는다. ConvertToEntity를 활성화하고 위에서 만든 RotationSpeedAuthoring_IJobEntityBatch 파일을 부모오브젝트에 추가한다. Play를 누르고 Dots관련 Editor들을 확인한다. (Archetypes, Hierarchy, Systems)

댓글

💲 추천 글