짬통

DOTS - 일단 시작2

tsyang 2022. 3. 16. 03:44

2022.03.13 - [유니티/ECS] - Unity ECS - 일단시작 1

 

Unity ECS - 일단시작 1

2021.09.26 - [이론/설계] - ECS (Entity Component System) ECS (Entity Component System) ECS 정의  "A different paradigm of writing code, where we model our programs in a data oriented way" 유니티 EC..

tsyang.tistory.com

 

이전 글에서는 일단 큐브를 만들어서 ECS시스템으로 움직이도록 했다. 

 

또한, 이전 글에서는
[GenerateAuthoringComponent]를 이용하여 Entity에 Component를 추가하고,

Entities.Foreach를 이용하여 System 내에서 적절한 Entity를 찾고 작업을 수행했다.

 

이번에는 Entity에 Component를 추가하고 System에서 Entity를 업데이트하는 또 다른 방법을 다룬다.

 

새로운 방법을 이용해서 이전 글에서 구현한 큐브를 회전도 시켜보자.

 

MonoBehaviour를 Component로

MonoBehaviour가 아래와 같은 IConvertGameObjectToEntity 인터페이스를 구현하도록 하면 ECS Entity에 Component를 추가할 수 있다.

public interface IConvertGameObjectToEntity
{
    void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem);
}

 

먼저 어떤 속도로 큐브를 회전시킬지 정하는 회전 속도에 대한 컴포넌트가 필요하므로 만들어준다.

 

[Serializable]
public struct RotationSpeed : IComponentData
{
    public float RadiansPerSec;
}

 

이전 글의 Velocity 컴포넌트처럼 [GenerateAuthoringComponent] 어트리뷰트를 달아주면 GameObject의 Inspector에 해당 컴포넌트를 추가해 줄 수 있다.

 

GenerateAuthoringComponent를 쓴 Velocity 컴포넌트

 

 

그 대신!!

 

 

public class RotationSpeedAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360.0f;

    public void Convert(
        Entity entity,
        EntityManager dstManager, 
        GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationSpeed{RadiansPerSec = math.radians(DegreesPerSecond)};
        dstManager.AddComponentData(entity, data);
    }
}

위와 같은 코드를 추가하자.

 

당연히 MonoBehaviour이기 때문에 Inspector에서 게임 오브젝트에 스크립트를 추가할 수 있다.

 

구현한 Convert 메서드에서는 해당 게임 오브젝트가 컨버팅 될 때 Entity에 어떤 컴포넌트를 추가해줄지를 정해준다. 위 코드에서는 RotationSpeed 컴포넌트를 추가해주었다.

 

dstManager란 DestinationWorld의 매니저이다.

 

AddComponentData를 통해 해당 오브젝트가 컨버팅 될 Entity에 컴포넌트를 추가한다. 

 

나는 여기에 속도를 랜덤으로 지정해주는 기능을 추가하고자 한다.

public class RotationSpeedAuthoring 
    : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360.0f;
    public bool IsRandom = false;

    private void Awake()
    {
        if (IsRandom)
            DegreesPerSecond = UnityEngine.Random.Range(0, 360);
    }

    public void Convert(
        Entity entity, 
        EntityManager dstManager, 
        GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationSpeed{RadiansPerSec = math.radians(DegreesPerSecond)};
        dstManager.AddComponentData(entity, data);
    }
}

Awake()가 Convert()보다 먼저 호출되므로 저렇게 랜덤 한 값을 넣어줄 수도 있다.

 

여기에 더해서 커스텀 에디터도 작업해주자.

([GenerateAuthoringComponent] 도 CustomEditor를 만들 수 있는지는 모르겠음. 귀찮아서 안 해봄)

 

Random값을 사용한다면 DegreesPerSecond를 감추도록 하겠다.

public class RotationSpeedAuthoringEditor : Editor
{
    public override void OnInspectorGUI()
    {
        var script = target as RotationSpeedAuthoring;

        script.IsRandom = EditorGUILayout.Toggle("Is Random", script.IsRandom);
        if (false == script.IsRandom)
            script.DegreesPerSecond = EditorGUILayout.FloatField
                ("Degrees Per Second", script.DegreesPerSecond);
    }
}

 

커스텀 에디터

 

커스텀 에디터도 잘 동작한다.

 

이제 게임을 실행시키면~!

 

아래 3번째줄의 RotationSpeed가 추가됨

RotationSpeed가 잘 들어가 있는 걸 확인할 수 있다.

 

이처럼 IConvertGameObjectToEntity를 통한 방법은 [GenerateAuthoringComponent]를 통한 방법보다 더 유연하다.

 

 

청크를 받아와 로직 업데이트

이전 글에서는 위와 같은 방법으로 아래 과정을 수행했다.

  1. 어떤 엔티티를 받아올지 정한다.
  2. 엔티티 단위로 작업을 뿌려준다.

 

이번에는 EntityQuery를 이용해 1번을 수행해보자.

public class RotationSpeedSystem : SystemBase
{
    private EntityQuery _query;

    protected override void OnCreate()
    {
        _query = GetEntityQuery(
            ComponentType.ReadWrite<Rotation>(),
            ComponentType.ReadOnly<RotationSpeed>());
    }
}

위와 같은 코드를 작성해주면 _query는 Rotation과 RotationSpeed를 가진 엔티티들을 가져오는 쿼리가 된다.

 

여기에 더해 EntityQueryDesc을 통해 특정 컴포넌트를 가지고 있으면 제외한다거나, 제시한 컴포넌트 중 하나라도 가지고 있으면 된다던가 하는 좀 더 복잡한 쿼리를 지정해줄 수 있다.

 

또한 여러 필터들도 추가해줄 수 있다.

(ex. setChangeUnversionFilter로 변화가 없는 엔티티들이 있는 청크는 건너 뜀)

 

 

그다음에 2번을 위해서 청크 단위로 작업을 해줄 Job 클래스를 만들어준다. Job 클래스는 IJobEntityBatch의 Execute()를 구현해주면 된다.

 

[BurstCompile]  //Job이 BurstCompile되도록 하는 어트리뷰트
struct RotationSpeedJob : IJobEntityBatch
{
    public float DeltaTime;
    public ComponentTypeHandle<Rotation> RotationTypeHandle;
    [Unity.Collections.ReadOnly] 
    public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;

    public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
    {
        NativeArray<Rotation> chunkRotations = 
            batchInChunk.GetNativeArray(RotationTypeHandle);
        NativeArray<RotationSpeed> 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.RadiansPerSec * DeltaTime))
            };
        }
    }
}

Rotation와 RotationSpeed에 접근하기 위해 Handle을 지정해준다. 어떤 Handle을 쓸지는 외부에서 주입해줄 수 있다.

 

protected override void OnUpdate()
{
    var rotationTypeHandle = GetComponentTypeHandle<Rotation>(); //RW
    var rotationSpeedTypeHandle = GetComponentTypeHandle<RotationSpeed>(isReadOnly: true); //R

    var job = new RotationSpeedJob()
    {
        RotationTypeHandle = rotationTypeHandle,
        RotationSpeedTypeHandle = rotationSpeedTypeHandle,
        DeltaTime = Time.DeltaTime * CubeGM.SimulationSpeed
    };

    Dependency = job.ScheduleParallel(_query, batchesPerChunk : 1, Dependency);
}

 

그리고 RotationSpeedSystem의 OnUpdate를 위와 같이 구현해준다.

 

여기서 ScheduleParallel의 batchesPerChunk 옵션을 설명하자면, 이 값이 1일 때 청크 하나를 모두 처리하겠다는 의미가 된다. 따라서 이 batchesPerChunk가 1일 때 효율적이다. 

 

그러나, 청크에 들어가는 엔티티의 숫자가 적고 수행해야 할 작업이 오래 걸린다면 청크를 여러 batch로 쪼개어 작업하는 게 병렬 프로세싱의 이점을 취하는 방법일 수 있다.

 

이제 실행시키면~

큐브의 Rotation과 Position이 잘 변화하는 것을 볼 수 있다.

 

Rotation의 변경은  IConvertGameObjectToEntity​ + IJobEntityBatch를 사용하였고,

Position의 변경은 이전 글에서 작성한 GenerateAuthoringComponent + Entities.Foreach를 사용하였다.

'짬통' 카테고리의 다른 글

DOTS 1.0 - 2 (생성, MonoBehavour 연계)  (0) 2023.02.04
DOTS 1.0 - 기본 (Component,System,Aspect,Job)  (1) 2023.01.29
DOTS 1.0 나온 기념 세팅법  (2) 2023.01.01
일단 시작3 - Instantiate & Destory  (0) 2022.03.23
DOTS - 일단 시작 1  (1) 2022.03.13