Unity DOTS/ECS Sample Projtect

ECS Sample Project #6 SpawnAndRemove

개양반 2022. 7. 30.

선행 학습

[Unity DOTS/Dots Custom Manual] - UNITY DOTS - Entity Command Buffers 에 대해 알아보자.

 

이전 글 보기

[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

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #5 SpawnFromEntity


오늘 알아볼 내용

오늘은 이전에 SampleProject로 배운 것을 활용해서 Cube를 생성하고 일정 시간이 지나면 CommandBuffer를 이용해서 Cube를 삭제하는 것에 대해 배웁니다.

Cube가 생성되고 일정시간이 지나면 삭제된다.

 


새로운 씬을 만들고 이름을 6. SpawnAndRemove 로 변경하고 원하는 폴더에 저장합니다. 6. SpawnAndRemove 폴더를 만들고 자식 폴더로 Component, Authoring, System을 만듭니다.


Cube 생성하기

이전 ECSSampleProject에서 배운 내용을 토대로 GameObject에서 Entity로 변환하고 변환된 Entity에서 Cube Entity를 생성하도록 만들겠습니다. 

1. Spawner_SpawnAndRemove

[6. SpawnAndRemove\Component] 폴더에 우클릭 > Create > ECS > Runtime Component Type 을 클릭하고 이름을 Spawner_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성합니다.

using Unity.Entities;

public struct Spawner_SpawnAndRemove : IComponentData
{
    public int CountX;
    public int CountY;
    public Entity Prefab;
}

 

2. SpawnerAuthoring_SpawnAndRemove

[6. SpawnAndRemove\Authoring] 폴더에 우클릭 > Create > C# 을 누르고 파일 이름을 SpawnerAuthoring_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성합니다. 

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

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

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(Prefab);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var spawnerData = new Spawner_SpawnAndRemove
        {
            Prefab = conversionSystem.GetPrimaryEntity(Prefab),
            CountX = CountX,
            CountY = CountY,
        };

        dstManager.AddComponentData(entity, spawnerData);
    }
}

Entity가 런타임 중에 Entity로 생성할 GameObject 프리팹을 DeclareReferencedPrefabs를 통해 등록하는 코드이다. 

위 코드의 내용이 기억 안 나시는 분은 아래 링크 참고한다.

[Unity DOTS/ECS Sample Projtect] - ECS Sample Project #5 SpawnFromEntity

 

3. SpawnerSystem_SpawnAndRemove

[6. SpawnAndRemove\Authoring] 폴더에 우클릭 > Create >  ECS > System을 클릭한다. 파일이름을 SpawnerSystem_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성한다.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public partial class SpawnerSystem_SpawnAndRemove : SystemBase
{
    BeginInitializationEntityCommandBufferSystem entityCommandBufferSystem;

    protected override void OnCreate()
    {
        // 시스템이 생성되면 OnCreate 호출
        // BeginInitializationEntityCommandBufferSystem을 가져온다. 
        entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();

    }

    protected override void OnUpdate()
    {
        // 병렬 쓰기 작업에서 명령을 예약할 예정이므로 AsParallelWriter();를 설정한다.
        var commandBuffer = entityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter();

        Entities
            .WithName("SpawnerSystem_SpawnAndRemove")
            // 1, 2번 매개변수는 기본값이다. 3번째는 동기식으로 처리한다는 의미이다. 
            // 대충 이런게 있다 정도로만 이해하자.
            .WithBurst(FloatMode.Default, FloatPrecision.Standard, true)
            .ForEach((Entity entity, int entityInQueryIndex, in Spawner_SpawnAndRemove spawner, in LocalToWorld location) =>
            {
                for (var x = 0; x < spawner.CountX; x++)
                {
                    for (var y = 0; y < spawner.CountY; y++)
                    {
                        // entity 생성 예약
                        var instance = commandBuffer.Instantiate(entityInQueryIndex, spawner.Prefab);

                        // 엔티티가 생성될 위치를 결정한다.
                        var position = math.transform(location.Value, 
                            new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));

                        // 생성한 Entity의 위치를 position위치로 변경하는 명령을 예약한다. 
                        commandBuffer
                        .SetComponent(entityInQueryIndex, instance, new Translation { Value = position });
                    }
                }                
                
                // 스폰 Entity를 제거한다. 제거하지 않으면 onUpdate에 의해 Cube가 계속 생성된다.
                commandBuffer.DestroyEntity(entityInQueryIndex, entity);
            }).ScheduleParallel();

        // 위의 작업이 완료되면
        // 예약한 명령을 다음 프레임에서 BeginInitializationEntityCommandBufferSystem 호출될때 처리하라고 등록한다.
        entityCommandBufferSystem.AddJobHandleForProducer(Dependency);
    }
}

#코드 설명

3-1 entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();

BeginInitializationEntityCommandBufferSystem는 System Group이 초기화될때 호출되는 EntityCommandBufferSystem이다. 즉, 예약된 명령을 BeginInitializationEntityCommandBufferSystem가 호출될때 실행되도록 한 것이다. 

4. 테스트 환경 만들기

4-1. Spawner 만들기

Hierarchy뷰에서 빈 게임오브젝트를 만들고 이름을 Spawner로 변경합니다. Spawner에게 위에서 만든 SpawnerAuthoring_SpawnAndRemove를 추가합니다. 

 

4-2 Cube 만들기

Cube 두개를 만들고 부모 자식 관계로 만듭니다. 아래의 이미지처럼 포개 놓습니다. Cube를 프리팹으로 만들고 Spawner의 SpawnerAuthoring_SpawnAndRemove의 Prefab 필드에 연결합니다. Hierarchy에 있는 Cube는 삭제합니다.

 

5. 테스트하기

Spawner의 SpawnerAuthoring_SpawnAndRemove의 CountX, CountY 값을 설정하고 재생버튼을 눌러 Cube가 지그재그로 생성되었는지 확인합니다. 

 


Cube 회전시키기

이번에는 Cube를 회전시키겠습니다. 

1. RotationSpeed_SpawnAndRemove

[6. SpawnAndRemove\Component] 폴더에 우클릭 > Create > ECS > Runtime Component type을 누르고 파일 이름을 RotationSpeed_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성합니다.

using System;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;

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

 

2. RotationSpeedAuthoring_SpawnAndRemove

[6. SpawnAndRemove\Authoring] 폴더에 우클릭 > Create > ECS > Authoring Component type을 누르고 파일 이름을 RotationSpeedAuthoring_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성합니다.

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

[DisallowMultipleComponent]
[ConverterVersion("joe", 1)]
public class RotationSpeedAuthoring_SpawnAndRemove : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new RotationSpeed_SpawnAndRemove { RadiansPerSecond = math.radians(DegreesPerSecond) });
    }
}

#코드 설명

해당 스크립트는 Cube에 추가될 예정이다. GamePrefab인 Cube가 Entity로 생성될때 Convert가 호출된다. 

 

3. RotationSpeedSystem_SpawnAndRemove

이번에는 회전시키는 시스템을 만들 예정이다. 

[6. SpawnAndRemove\System] 폴더에 우클릭  > Create > ECS > System을 누르고 파일 이름을  RotationSpeedSystem_SpawnAndRemove로 변경한 뒤 아래의 코드를 작성한다.

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public partial class RotationSpeedSystem_SpawnAndRemove : SystemBase
{
    protected override void OnUpdate()
    {
        var deltaTime = Time.DeltaTime;

        Entities
            .WithName("RotationSpeedSystem_SpawnAndRemove")
            .ForEach((ref Rotation rotation, in RotationSpeed_SpawnAndRemove rotSpeedSpawnAndRemove) =>
            {
             
                rotation.Value = math.mul(math.normalize(rotation.Value), 
                    quaternion.AxisAngle(math.up(), rotSpeedSpawnAndRemove.RadiansPerSecond * deltaTime));

            }).ScheduleParallel();
    }
}

 

4. 회전 속도 랜덤으로 지정하기

Cube가 회전하는 기본적인 기능은 다 만들었다. 이번에는 Cube가 생성될때 회전 속도를 랜덤으로 갖게 만들어보자. 

SpawnerSystem_SpawnAndRemove을 수정한다.  아래의 Using을 추가한다. Random은 여러가지가 있는데 우리는 Unity.Mathematics에 있는 Random을 사용할 것이다.

using Random = Unity.Mathematics.Random;

 

OnUpdate의 Entity.ForEach에서 for (var x = 0; x < spawner.CountX; x++) 바로 윗 줄에 Ramdom 코드를 추가한다. 매개변수로 랜덤 씨앗값을 전달한다.

var random = new Random(1);

 

다음은 생성된 Entity의 위치 명령을 예약했던 CommandBuffer.SetCompoent 아랫줄에 아래의 코드를 추가한다.

 // 회전 속도
commandBuffer
.SetComponent
(entityInQueryIndex,
  instance,
  new RotationSpeed_SpawnAndRemove
  { RadiansPerSecond = math.radians(random.NextFloat(25.0F, 90.0F)) });

 

5. 테스트 환경 만들기

위에서 만든 RotationSpeedAuthoring_SpawnAndRemove를 Cube 프리팹에 추가하고 재생버튼을 눌러 Cube가 랜덤한 속도로 회전하는지 확인한다.

 


Cube 생명시간 만들기

Cube가 생성되면 랜덤으로 LifeTime을 설정하고 LifeTime이 0이 되면 Cube를 제거하는 기능을 만들겁니다. 

 

1. LifeTimeSystem 만들기

[6. SpawnAndRemove\System] 폴더에 우클릭  > Create > ECS > System을 누르고 파일 이름을  LifeTimeSystem로 변경한 뒤 아래의 코드를 작성한다.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public struct LifeTime : IComponentData
{
    public float Value;
}


public partial class LifeTimeSystem : SystemBase
{
    EntityCommandBufferSystem entityCommandBufferSystem;
    protected override void OnCreate()
    {
        entityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        var commandBuffer = entityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
        var deltaTime = Time.DeltaTime;

        // entityInQueryIndex 는 Query로 조회된 Entities의 식별코드이다.
        Entities.ForEach((Entity entity, int entityInQueryIndex, ref LifeTime lifetime) =>
        {
            lifetime.Value -= deltaTime;

            if (lifetime.Value < 0.0f)
            {
                commandBuffer.DestroyEntity(entityInQueryIndex, entity);
            }
        }).ScheduleParallel();

        // 명령 실행 예약하기
        entityCommandBufferSystem.AddJobHandleForProducer(Dependency);
    }
}

#코드 설명

LifeTime Component도 System파일에 함께 작성했다. 

 

1-1 World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();

Cube를 생성할때 BeginInitializationEntityCommandBufferSystem를 사용했다면 이번에는 Cube를 삭제하는 기능이므로 Simulation이 종료될때 호출되는 EndSimulationEntityCommandBufferSystem를 가져왔습니다. (생성이 되어야 삭제가 될테니 생성되는 CommandBuffer보다 삭제되는 CommandBuffer가 늦게 처리되도록 함)

 

1-2. var deltaTime = Time.DeltaTime;

ForEach에서 조회한 Entities를 처리할때 각 entity마다 DeltaTime이 다르면 안되므로 OnUpdate에서 ForEach가 실행되기 전에 참조시킵니다.

 

2. Cube가 생성될때 LifeTime Component 추가하기

Cube 가 생성될때 LifeTime을 Component로 갖게끔 RotationSpeedAuthoring_SpawnAndRemove의 Convert에서 Entity에 LifteTime을 추가하는 코드를 넣습니다.

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

// ReSharper disable once InconsistentNaming
[AddComponentMenu("DOTS Samples/SpawnAndRemove/Rotation Speed")]
[ConverterVersion("joe", 1)]
public class RotationSpeedAuthoring_SpawnAndRemove : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new RotationSpeed_SpawnAndRemove { RadiansPerSecond = math.radians(DegreesPerSecond) });
        
        // LifeTime Component를 Cube에 추가한다.
        dstManager.AddComponentData(entity, new LifeTime { Value = 0.0F });
    }
}

 

3. LifeTime 랜덤으로 설정하기

Cube가 생성될때 LifeTime 값을 랜덤으로 설정하여 Cube마다 각각 다른 생명 유지시간이 설정되도록 만들거다.

SpawnerSystem_SpawnAndRemove을 수정한다. 생성된 Entity의 위치 명령을 예약했던 CommandBuffer.SetCompoent 아랫줄에 아래의 코드를 추가한다.

// 생존할 시간
commandBuffer
.SetComponent
(entityInQueryIndex, instance, new LifeTime { Value = random.NextFloat(10.0F, 20.0F) });

 

4. 테스트 하기

재생 버튼을 누르고 DOTS Hierarchy뷰에서 Cube를 클릭해서 랜덤 LifteTime 값을 갖는지 확인하고 Lifetime이 0 이하가 되면 Cube가 제거 되는지 확인한다.

댓글

💲 추천 글