Unity DOTS/ECS Sample Projtect

ECS Sample Project #5 SpawnFromEntity

개양반 2022. 7. 30.
728x90

이전 글 보기

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

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #2 IJobEntityBatch

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #3 Sub Scene

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #4 SpawnFromMonoBehaviour


오늘 알아볼 내용

오늘은 4. SpawnFromMonoBehaviour와  비슷하지만

이번에는 GameObject에서 Entity를 생성하는 것이 아닌 Entity에서 Entity를 생성하는 방법에 대해 다룰 겁니다.

이번에는 런타임 중에 Entity에서 Entity를 생성한다.

 


새로운 씬을 만들고 이름을 5. SpawnFromEntity 만든다음 원하는 폴더에 생성합니다. 해당 폴더의 자식 폴더로 Authoring, System, Component 폴더를 만듭니다.


가. Component 작성

5. SpawnFromEntity\Component 폴더에 우클릭 > Create > ECS >  Runtime Component Type 을 클릭하고 파일 이름을 Spawner_FromEntity로 변경한 뒤, 아래의 코드를 입력합니다.

using Unity.Entities;

// ReSharper disable once InconsistentNaming
public struct Spawner_FromEntity : IComponentData
{
    public int CountX;
    public int CountY;
    public Entity Prefab;
}

#코드 설명

X * Y 만큼 Entity를 생성할 예정입니다. Prefab의 변수타입이 Entity 입니다.


나. Authoring 작성

5. SpawnFromEntity\Authoring 폴더에 C# 스크립트를 생성하고 이름을 SpawnerAuthoring_FromEntity으로 변경합니다. 아래의 코드를 입력합니다.

using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

[ConverterVersion("joe", 1)]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    public GameObject Prefab;
    public int CountX;
    public int CountY;

    // Prefab을 런타임 중에 Entity로 변환해서 생성하려면 등록해야 합니다.
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(Prefab);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var spawnerData = new Spawner_FromEntity
        {
            // DeclareReferencedPrefabs 에서 등록했기에 Spawner_FromEntity의 Prefab으로 전달할 수 있습니다.
            Prefab = conversionSystem.GetPrimaryEntity(Prefab),
            CountX = CountX,
            CountY = CountY
        };

        dstManager.AddComponentData(entity, spawnerData);
    }
}

#코드 설명

게임오브젝트에 추가될 스크립트입니다. 생성할 개수와 생성할 게임오브젝트를 전역변수로 가지고 있습니다. 

 

1. DeclareReferencedPrefabs

게임오브젝트 Prefab을 런타임 중에 Entity로 생성하려면 DeclareReferencedPrefabs를 통해 등록해야 합니다.

 

2. Convert

GameObject가 EnableToEntity옵션에 의해 Entity로 전환될때 호출됩니다. 이때 전환되는 Entity는 해당 스크립트를 컴포넌트로 들고있는 Spawner(GameObject)입니다. Spawner_FromEntity의 Prefab은 Entity이고 SpawnerAuthoring_FromEntity의 Prefab은 GameObject입니다. DeclareReferencedPrefabs에서 Prefab을 등록했기 때문에 conversionSystem.GetPrimaryEntity(Prefab), 코드를 통해 GameObject를 Entity 타입 변수에 전달할 수 있습니다. 

EntityManager.AddComponentData를 통해 위에서 만든 Spawner_From구조체를 Spawner(Entity)의 Component로 추가합니다. 

 


다. System 작성

[5. SpawnFromEntity\System]폴더에 우클릭 > Create > ECS >  System을 클릭하고 파일 이름을 SpawnerSystem_FromEntity로 변경합니다.

 

1. UpdateOrdering 어트리뷰트 추가

어트리뷰트로 [UpdateInGroup(typeof(SimulationSystemGroup))]를 추가합니다.

[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial class SpawnerSystem_FromEntity : SystemBase
{
    protected override void OnUpdate()
    {
    }
}

#코드 설명

UpdateInGroup은 해당 시스템이 어떤 그룹에 속해야 하는지 설정하는 겁니다. 해당 어트리뷰트가 없으면 시스템은 자동으로 SimulationSystemGroup에 속하게 됩니다. 즉, 해당 예제에서는 없어도 되는 어트리뷰트입니다. Update Ordering에는 UpdateInGroup, UpdateBefore, UpdateAfter, DisableAutoCreation 존재합니다. 추후에 자연스럽게 알게 됩니다.

Play모드에서 System창을 열면 시스템 그룹들을 볼 수 있다. 지금 단계에서는 깊게 알 필요가 없다.

 

2. Job 구조체 만들기

OnUpdate() 안에 생성된 Entity의 위치를 변경하는 IJobParalleFor 구조체를 만듭니다. 

    [BurstCompile]
    struct SetSpawnedTranslation : IJobParallelFor
    {
        // 보통은 병렬 작업에서 ComponentDataFromEntity에 쓰기 작업을 할 수 없다.
        // 병렬 쓰기를 가능하게 한다. 즉, IJobParallelFor는 인덱스 값으로 쓰기를 하므로 경쟁 문제가 발생하지 않으므로 사용한다. 
        [NativeDisableParallelForRestriction]
        public ComponentDataFromEntity<Translation> TranslationFromEntity;

        public NativeArray<Entity> Entities;

        // 로컬 좌표
        public float4x4 LocalToWorld;
        public int Stride;

        public void Execute(int index)
        {
            var entity = Entities[index];
            var y = index / Stride;
            var x = index - (y * Stride);

            // Entity의 좌표를 변경한다.
            TranslationFromEntity[entity] = new Translation()
            {
                Value = math.transform(LocalToWorld, new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F))
            };
        }
    }

#코드 설명

2-1 [BurstCompile] :

 IL/.NET 바이트코드를 LLVM을 사용하여 고도로 최적화된 네이티브 코드로 변환합니다. Job구조체는 BurstCompile을 사용할 수 있습니다.

 

2-2  [NativeDisableParallelForRestriction] :

ComponentDataFromEntity<T> 는 모든 Entity를 대상으로 T데이터를 얻어올 수 있습니다. 기본적으로 쓰기 작업이 안됩니다. 하지만 IJobParallelFor은 Excute에서 Job이 수행하는 Entity의 인덱스 값으로 반복을 처리하고 Job이 완료될때까지 대기를 하므로, 쓰기 작업에서 경쟁문제가 발생하지 않다고 확신하기에 [NativeDisableParallelForRestriction] 를 추가하여 쓰기 작업이 가능하도록 만든겁니다. 멀티스레드에서 쓰기 작업 중에 경쟁 문제가 발생하지 않을 확신이 있을때만 해야 합니다.

 

3. Entities.ForEach 작성

OnUpdate() 안에 아래의 코드를 작성합니다.

// WithStructuralChanges 버스트를 비활성하여 함수 내에서 엔티티 데이터를 구조적으로 변경할 수 있게 해준다.
// WithStructuralChanges 보단 EntityCommandBuffer를 사용하는 것이 성능상 더 좋다. 
Entities.WithStructuralChanges().ForEach((Entity entity, int entityInQueryIndex,
    in Spawner_FromEntity spawnerFromEntity, in LocalToWorld spawnerLocalToWorld) =>
{

    // Job 이 끝날때까지 메인쓰레드가 대기한다.
    Dependency.Complete();

    var spawnedCount = spawnerFromEntity.CountX * spawnerFromEntity.CountY;

    // NativeArray<Entity> 초기화
    var spawnedEntities =
        new NativeArray<Entity>(spawnedCount, Allocator.TempJob); 

    // spawnedEntities 크기만큼 Entity를 생성하고 spawnedEntities에 생성한 Entity를 넣습니다.
    EntityManager.Instantiate(spawnerFromEntity.Prefab, spawnedEntities);
    
    // Spawner Entity를 제거합니다. (안 그러면 매프레임마다 Entity를 생성함)
    EntityManager.DestroyEntity(entity);

    var translationFromEntity = GetComponentDataFromEntity<Translation>();
    var setSpawnedTranslationJob = new SetSpawnedTranslation
    {
        TranslationFromEntity = translationFromEntity,
        Entities = spawnedEntities,
        LocalToWorld = spawnerLocalToWorld.Value,
        Stride = spawnerFromEntity.CountX
    };


    // spawnedCount 수행할 반복횟수
    // 두번째 매개변수는 배치 크기이다. 보통 32 또는 64를 사용하며, 매우 큰 Job일 경우 1를 쓰는 것이 좋을 수 있다.
    Dependency = setSpawnedTranslationJob.Schedule(spawnedCount, 64, Dependency);
    Dependency = spawnedEntities.Dispose(Dependency);
}).Run();

#코드 설명

3-1 WithStructuralChanges() : 

Entities.ForEach는 기본적으로 구조 변경(EntityManager로 Entity를 생성 및 제거 등)이 안 되도록 되어 있습니다.

( ForEach에서 람다로 정의한 쓰기 가능 타입은 구조 변경 가능) 그러나, 메인스레드에서만 동작하게 하는 Run() 과 WithStructuralChanges() 으로 구조 변경을 할 수 있게 합니다. 몰론 버스트컴파일을 끄고, 메인스레드에서 동작하므로 성능상 좋지 않습니다. WithStructuralChanges()보다는 EntityCommandBuffer를 사용하는 것이 좋습니다. (*EntityCommandBuffer 에 대해서는 추후에 설명)

 

3-2  int entityInQueryIndex

Entities.ForEach는 람다로 정의된 Query를 만족하는 모든 Entity를 하나씩 실행시키는 함수입니다. entityInQueryIndex는 조회된 Entity의 식별코드입니다.

 

3-3 Dependency.Complete();

이전 프레임에서 실행한 Job이 완료될때까지 대기합니다. Job은 멀티스레드에서 작동하므로 비동기입니다.

 

3-4 new NativeArray<Entity>(spawnedCount, Allocator.TempJob);

NativeArray를 초기화하는 코드입니다. Allocator은 메모리를 할당하는 것입니다. 할당에는 총 3가지 타입이 있습니다.

Allocator.Temp 가장 빠르게 할당됩니다. 수명이 한 프레임 이하인 할당에 사용합니다.
Allocator.TempJob  위의 할당보다는 느리지만 4프레임 내에서 스레드로부터 안전한 할당에 사용합니다. 
Allocator.Persistent 가장 느린 할당이지만 원하는 프레임동안 사용할 수 있습니다.

위 코드에서는 spawnedEntities를 Job에 전달해야 하므로 TempJob을 사용해야 합니다. Temp는 Job에서 동작하지 않습니다.

 

3-5  Dependency = setSpawnedTranslationJob.Schedule(spawnedCount, 64, Dependency);

Job을 예약해서 준비되면 Job이 실행되게 합니다. spawnedCount는 Entity개수이고 두번째 매개변수는 하나의 스레드에서 몇개의 Batch를 만들어서 처리할지 설정합니다. Job이 크면 1, 작으면 64, 보통이면 32를 사용합니다. 테스트 할때, 64, 32, 1 넣고 테스트해보고 차이가 없는거 같으면 64 넣으면 된다. 여기서 Job의 크기란 Entity의 개수가 아니라 하나의 Job이 얼마나 많은 처리를 하는지이다. 예제에서는 위치만 변경하므로 Job이 작다.

 

3-6 Dependency = spawnedEntities.Dispose(Dependency);  NativeArray의 메모리 할당을 해제한다. 


라. 게임오브젝트 세팅

1. Spawner 게임오브젝트 만들기

Hierarchy뷰에서 빈게임오브젝트를 만들고 이름을 Spawner로 만든다. Spawner에 방금 만든 SpawnerAuthoring_FromEntity.cs를 추가한다. 그리고 Spawner의 Inspector창에서 ConvertToEntity를 활성화한다.

 

2. Cube 만들기

Cube를 두개 만들고 부모 자식 관계로 만든다. #1 ForEach에서 만든 RotationSpeed_ForEach를 Component로 추가한다. 

Cube를 Prefab으로 만들고 Hirarchy뷰에서 삭제한다. Cube Prefab을 Spawner의 SpawnerAuthoring_FromEntity의 Prefab변수에 연결한다.


마. 테스트하기

플레이 버튼을 누르고 Hierarchy 뷰를 확인한다. 이번에는 Entity로 변환했기에 Hierarchy 뷰에 Spawner가 없다. 그리고 Cube Entity를 생성하고 삭제되므로 DOTS Hierarchy 뷰에도 없다. 

댓글

💲 추천 글