DOTS 정리 (4) – 튜토리얼 실습

https://learn.unity.com/tutorial/660ffc54edbc2a1b0887d45e?uv=6&projectId=660ffcd3edbc2a162b7baa27#66ceb510edbc2a02913174e1

엔티티로 변환

ECS(Entity Component System)와 DOTS(Data-Oriented Technology Stack)를 사용하여 Tank 오브젝트를 정의하고 이를 엔티티로 변환하는 작업

TankAuthoring.cs

using Unity.Entities;
using UnityEngine;

public class TankAuthoring : MonoBehaviour
{
    public GameObject Turret;
    public GameObject Cannon;

    class Baker : Baker<TankAuthoring>
    {
        public override void Bake(TankAuthoring authoring)
        {
            Entity entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
            AddComponent(entity, new Tank
            {
                Turret = GetEntity(authoring.Turret, TransformUsageFlags.Dynamic),
                Cannon = GetEntity(authoring.Cannon, TransformUsageFlags.Dynamic)
            });
        }
    }
}

public struct Tank : IComponentData
{
    public Entity Turret;
    public Entity Cannon;
}

Baker 클래스는 Baker를 상속받아 TankAuthoring MonoBehaviour를 엔티티로 변환하는 로직을 정의

Bake 메서드에서 authoring 객체의 Turret과 Cannon을 엔티티로 변환하고, Tank 컴포넌트를 추가합니다.

GetEntity 메서드는 특정 GameObject를 입력으로 받아 해당 오브젝트를 엔티티로 변환

TransformUsageFlags

Tank 구조체는 IComponentData를 구현하여 엔티티에 부착할 수 있는 컴포넌트입니다.

Turret과 Cannon 필드는 탱크의 포탑과 대포를 나타내는 엔티티입니다.

무작위 곡선 경로를 따라 탱크를 이동하는 시스템

TankMovementSystem.cs

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

public partial struct TankMovementSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // ECS에서는 성능 최적화를 위해 컴포넌트와 시스템이 독립적으로 작동
        // SystemAPI.Time.DeltaTime은 ECS의 업데이트 주기에 맞춰 효율적으로 동작하도록 설계되었음
        float dt = SystemAPI.Time.DeltaTime;

        // LocalTransform 및 Tank 컴포넌트가 있는 각 엔티티에서 
        // LocalTransform 및 엔티티 ID에 액세스합니다.
        foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>().WithAll<Tank>().WithEntityAccess())
        {
            var pos = transform.ValueRO.Position;

            // 이는 탱크의 실제 위치를 수정하는 것이 아니라
            // 3D 노이즈 함수를 샘플링하는 지점만 수정합니다.
            // 이렇게 하면 각 탱크가 서로 다른 슬라이스를 사용하게 되고
            // 각기 다른 무작위 플로 필드를 따라 이동하게 됩니다.
            pos.y = (float)entity.Index;

            var angle = (0.5f + noise.cnoise(pos / 10f)) * 4.0f * math.PI;
            var dir = float3.zero;
            math.sincos(angle, out dir.x, out dir.z);

            // LocalTransform을 업데이트합니다.
            transform.ValueRW.Position += dir * dt * 5.0f;
            transform.ValueRW.Rotation = quaternion.RotateY(angle);

        }
    }
}

TankMovementSystem은 ISystem 인터페이스를 구현하여, ECS 시스템으로 작동

BurstCompile 속성을 사용하여 시스템의 성능을 최적화

OnUpdate 메서드는 매 프레임마다 호출되어 시스템의 주요 로직을 실행
https://docs.unity3d.com/Packages/com.unity.entities@1.3/manual/systems-isystem.html

TankMovementSystem이 모든 프레임을 업데이트합니다.

업데이트에서 시스템은 LocalTransformTank 컴포넌트가 있는 모든 엔티티를 쿼리하고

LocalTransform을 업데이트하여 각 탱크를 무작위 곡선 경로를 따라 이동시킵니다.

무작위 색상으로 다양한 탱크 생성

Tank, Turret, Cannon 게임 오브젝트 모두에 URPMateiralPropertyBaseColorAuthoring 컴포넌트를 추가

URPMateiralPropertyBaseColorAuthoring는 Hybrid Renderer 패키지에서 사용되는 구조체

이 구조체는 머티리얼의 기본 색상 속성을 정의하고, ECS(Entity Component System) 환경에서 머티리얼의 색상 속성을 쉽게 접근하고 조작할 수 있도록 하는 컴퍼넌트
(Authoring – Unity 에디터와 ECS 간의 브리지 역할로 이 컴포넌트가 ECS 엔티티의 데이터를 설정하고 초기화하는 데 사용된다는 것을 명확하게 나타냄)

ConfigAuthoring.cs 작성

using Unity.Entities;
using UnityEngine;

public class ConfigAuthoring : MonoBehaviour
{
    public GameObject TankPrefab;
    public GameObject CannonBallPrefab;
    public int TankCount;

    class Baker : Baker<ConfigAuthoring>
    {
        public override void Bake(ConfigAuthoring authoring)
        {
            Entity entity = GetEntity(authoring, TransformUsageFlags.None);
            AddComponent(entity, new Config
            {
                // 프리팹을 엔티티로 베이크합니다. GetEntity는 프리팹 계층 구조의 
                // 루트 엔티티를 반환합니다.
                TankPrefab = GetEntity(authoring.TankPrefab, TransformUsageFlags.Dynamic),
                CannonBallPrefab = GetEntity(authoring.CannonBallPrefab, TransformUsageFlags.Dynamic),
                TankCount = authoring.TankCount,
            });
        }
    }

}

public struct Config : IComponentData
{
    public Entity TankPrefab;
    public Entity CannonBallPrefab;
    public int TankCount;
}

Baker 클래스는 Baker을 상속받아 ConfigAuthoring MonoBehaviour를 ECS 엔티티로 변환하는 로직을 정의합니다.

Bake 메서드에서 authoring 객체의 TankPrefab과 CannonBallPrefab을 엔티티로 변환하고, Config 컴포넌트를 추가합니다.

탱크를 생성하고 색상을 설정

TankSpawnSystem.cs

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using UnityEngine;
using Random = Unity.Mathematics.Random;

public partial struct TankSpawnSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        // RequireForUpdate는 Config 컴포넌트가 있는 엔티티가 
        // 하나 이상 존재하는 경우에만 시스템이 업데이트됨을 의미합니다.
        // 실제로 이 시스템은 Config가 포함된 하위 씬이 
        // 로드될 때까지 업데이트되지 않습니다.
        state.RequireForUpdate<Config>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 시스템을 비활성화하면 추가 업데이트가 중지됩니다.
        // 첫 번째 업데이트에서 이 시스템을 비활성화하면 실제로 한 번만 업데이트됩니다.
        state.Enabled = false;

        // ‘singleton’은 인스턴스가 하나만 있는 컴포넌트 유형
        // GetSingleton<T>는 T 유형의 컴포넌트를 가진 엔티티가 없거나 2개 이상인 경우 예외 오류가 발생
        // 이 경우 Config 인스턴스는 하나만 있어야 합니다.
        var config = SystemAPI.GetSingleton<Config>();

        // 하드 코딩된 시드의 무작위 숫자
        var random = new Random(123);

        for (int i = 0; i < config.TankCount; i++)
        {
            var tankEntity = state.EntityManager.Instantiate(config.TankPrefab);

            // URPMaterialPropertyBaseColor는 Entities.Graphics 패키지의 컴포넌트이며 
            // 렌더링된 엔티티의 기본 렌더링 색상을 설정할 수 있습니다.
            var color = new URPMaterialPropertyBaseColor { Value = RandomColor(ref random) };

            // 프리팹에서 인스턴스화된 모든 루트 엔티티는 프리팹 계층 구조(루트 포함)를 구성하는
            // 모든 엔티티의 목록인 LinkedEntityGroup 컴포넌트가 있습니다.

            // LinkedEntityGroup은 ‘DynamicBuffer’라고 하는 특별한 유형의 컴포넌트이며
            // 단일 구조체 대신 구조체 값으로 구성된 크기 조절 가능 배열입니다.
            var linkedEntities = state.EntityManager.GetBuffer<LinkedEntityGroup>(tankEntity);
            foreach (var entity in linkedEntities)
            {
                // 여기서는 URPMaterialPropertyBaseColor 컴포넌트를 가진 엔티티에만 해당 컴포넌트를 설정하려 하므로 먼저 확인합니다.
                if (state.EntityManager.HasComponent<URPMaterialPropertyBaseColor>(entity.Value))
                {
                    // 탱크를 구성하는 각 엔티티의 색상을 설정합니다.
                    state.EntityManager.SetComponentData(entity.Value, color);
                }
            }
        }
    }

    // 시각적으로 구별되는 무작위 색상을 반환합니다.
    // (단순한 무작위성은 좁은 범위의 색상을 중심으로 모여 있는 색상 분포를 생성합니다.
    // 참고: https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/)
    static float4 RandomColor(ref Random random)
    {
        // 0.618034005f는 황금 비율의 역수입니다.
        var hue = (random.NextFloat() + 0.618034005f) % 1;
        return (Vector4)Color.HSVToRGB(hue, 1.0f, 1.0f);
    }
}

OnCreate 메서드는 시스템이 생성될 때 한 번 호출되며, RequireForUpdate는 Config 컴포넌트가 존재할 때만 시스템이 업데이트되도록 보장

OnUpdate 메서드는 매 프레임마다 호출되며, 첫 번째 업데이트 후 시스템을 비활성화합니다.

GetSingleton를 사용하여 Config 컴포넌트를 가진 엔티티의 인스턴스를 가져옵니다.

config.TankCount만큼 루프를 돌며, 각 탱크 프리팹을 인스턴스화합니다.

RandomColor 메서드를 사용하여 각 탱크의 색상을 무작위로 설정합니다.

LinkedEntityGroup 버퍼를 통해 프리팹 계층 구조의 모든 엔티티를 가져오고, URPMaterialPropertyBaseColor 컴포넌트를 가진 엔티티에 색상을 설정합니다.

LinkedEntityGroup을 사용하면 프리팹의 모든 엔티티(즉, 계층 구조에 속한 루트와 자식 엔티티)를 참조할 수 있습니다.

이는 특정 프리팹을 인스턴스화할 때 매우 유용합니다.

즉, 프리팹의 모든 엔티티를 탐색하여, URPMaterialPropertyBaseColor 컴포넌트를 가진 엔티티를 찾아서 해당 컴포넌트의 색상을 무작위 색상으로 업데이트함.

ECS(Entity Component System)와 DOTS(Data-Oriented Technology Stack) 환경에서는, 게임 오브젝트와 엔티티가 다르게 렌더링됩니다.

일반적으로, Unity 에디터의 씬 뷰(Scene View)에서는 기본적으로 ECS 엔티티가 보이지 않을 수 있습니다.

이를 해결하기 위해 씬 뷰 모드를 “런타임 데이터(Runtime Data)”로 변경합니다.

Edit > Preferences > Entities > Scene View Mode를 ‘Runtime Data’로 선택합니다.

플레이어 입력으로 하나의 탱크를 움직이고 카메라가 따라가도록 설정

플레이어가 하나의 탱크를 제어하려면 먼저 플레이어의 탱크 엔티티를 다른 탱크와 구별할 방법이 필요합니다.

그럼 이제 새 Player 컴포넌트를 생성한 다음 생성된 탱크 중 하나에 추가해 보겠습니다.

Player.cs

using Unity.Entities;

public struct Player : IComponentData
{

}

TankSpawnSystem.cs 에서 첫 번째 탱크에 Player 컴포넌트를 추가합니다.

TankMovementSystem.cs에서 플레이어 탱크가 다른 탱크처럼 무작위 경로를 따라 이동하는 것을 중지

PlayerSystem.cs 추가

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

public partial struct PlayerSystem : ISystem
{
    // OnUpdate가 관리되는 오브젝트(카메라)에 액세스하기 때문에 이 메서드를 
    // 버스트 컴파일할 수 없으므로 여기서는 [BurstCompile] 속성을 사용하지 않습니다.
    public void OnUpdate(ref SystemState state)
    {
        var movement = new float3(
            Input.GetAxis("Horizontal"),
            0,
            Input.GetAxis("Vertical")
        );
        movement *= SystemAPI.Time.DeltaTime;

        foreach (var playerTransform in SystemAPI.Query<RefRW<LocalTransform>>().WithAll<Player>())
        {
            // 플레이어 탱크를 이동합니다.
            playerTransform.ValueRW.Position += movement;

            // 카메라를 움직여 플레이어를 따라갑니다.
            var cameraTransform = Camera.main.transform;
            cameraTransform.position = playerTransform.ValueRO.Position;
            cameraTransform.position -= 10.0f * (Vector3)playerTransform.ValueRO.Forward();  // 카메라를 플레이어 뒤로 이동합니다.
            cameraTransform.position += new Vector3(0, 5f, 0);  // 카메라를 오프셋만큼 올립니다.
            cameraTransform.LookAt(playerTransform.ValueRO.Position);  // 플레이어를 바라봅니다.
        }
    }
}

포탑 끝에서 포탄 생성

CannonBallAuthoring.cs 생성

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

public class CannonBallAuthoring : MonoBehaviour
{
    class Baker : Baker<CannonBallAuthoring>
    {
        public override void Bake(CannonBallAuthoring authoring)
        {
            var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);

            // 기본적으로 컴포넌트는 초기화되지 않기 때문에
            // CannonBall의 Velocity 필드는 float3.zero가 됩니다.
            AddComponent<CannonBall>(entity);
            AddComponent<URPMaterialPropertyBaseColor>(entity);
        }
    }
}

public struct CannonBall : IComponentData
{
    public float3 Velocity;
}

ShootingSystem.cs 생성

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;

// 이 속성은 업데이트 순서에서 이 시스템을 TransformSystemGroup 앞에 배치합니다.
// ShootingSystem은 포탄의 로컬 트랜스폼만 설정하지만
// TransformSystemGroup의 트랜스폼 시스템은 월드 트랜스폼(LocalToWorld)을 설정합니다.
// ShootingSystem이 프레임에서 TransformSystemGroup 이후에 업데이트되는 경우,
// 생성된 포탄은 단일 프레임의 원점에 렌더링됩니다.
[UpdateBefore(typeof(TransformSystemGroup))]
public partial struct ShootingSystem : ISystem
{
    private float timer;

    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Config>()));
    
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 타이머가 만료된 프레임에서만 발사합니다.
        timer -= SystemAPI.Time.DeltaTime;
        if (timer > 0)
        {
            return;
        }
        timer = 0.3f;   // 타이머를 재설정합니다.

        var config = SystemAPI.GetSingleton<Config>();

        var ballTransform = state.EntityManager.GetComponentData<LocalTransform>(config.CannonBallPrefab);

        // 모든 탱크의 각 포탑에 포탄을 생성하고 초기 속도를 설정합니다.
        foreach (var (tank, transform, color) in SystemAPI.Query<RefRO<Tank>, RefRO<LocalToWorld>, RefRO<URPMaterialPropertyBaseColor>>())
        {
            Entity cannonBallEntity = state.EntityManager.Instantiate(config.CannonBallPrefab);

            // 포탄의 색상이 포탄을 쏜 탱크와 일치하도록 설정합니다.
            state.EntityManager.SetComponentData(cannonBallEntity, color.ValueRO);

            // 월드 공간에서 대포의 트랜스폼이 필요하므로 LocalTransform 대신 LocalToWorld를 사용합니다.
            var cannonTransform = state.EntityManager.GetComponentData<LocalToWorld>(tank.ValueRO.Cannon);
            ballTransform.Position = cannonTransform.Position;

            // 새 포탄의 위치를 생성 지점과 일치하도록 설정합니다.
            state.EntityManager.SetComponentData(cannonBallEntity, ballTransform);

            // 대포에서 발사되는 포탄의 속도를 설정합니다.
            state.EntityManager.SetComponentData(cannonBallEntity, new CannonBall
            {
                Velocity = math.normalize(cannonTransform.Up) * 12.0f
            });
        }
    }
}

포탑 회전

간단하게 TankMovementSystemOnUpdate 메서드 하단에 몇 가지 코드를 추가하여 회전을 구현

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

public partial struct TankMovementSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // ECS에서는 성능 최적화를 위해 컴포넌트와 시스템이 독립적으로 작동
        // SystemAPI.Time.DeltaTime은 ECS의 업데이트 주기에 맞춰 효율적으로 동작하도록 설계되었음
        float dt = SystemAPI.Time.DeltaTime;

        // LocalTransform 및 Tank 컴포넌트가 있는 각 엔티티에서 
        // LocalTransform 및 엔티티 ID에 액세스합니다.
        foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>().WithAll<Tank>().WithEntityAccess().WithNone<Player>())
        {
            var pos = transform.ValueRO.Position;

            // 이는 탱크의 실제 위치를 수정하는 것이 아니라
            // 3D 노이즈 함수를 샘플링하는 지점만 수정합니다.
            // 이렇게 하면 각 탱크가 서로 다른 슬라이스를 사용하게 되고
            // 각기 다른 무작위 플로 필드를 따라 이동하게 됩니다.
            pos.y = (float)entity.Index;

            var angle = (0.5f + noise.cnoise(pos / 10f)) * 4.0f * math.PI;
            var dir = float3.zero;
            math.sincos(angle, out dir.x, out dir.z);

            // LocalTransform을 업데이트합니다.
            transform.ValueRW.Position += dir * dt * 5.0f;
            transform.ValueRW.Rotation = quaternion.RotateY(angle);

        }

        var spin = quaternion.RotateY(SystemAPI.Time.DeltaTime * math.PI);

        foreach (var tank in SystemAPI.Query<RefRW<Tank>>())
        {
            var trans = SystemAPI.GetComponentRW<LocalTransform>(tank.ValueRO.Turret);

            // Y축 주위에 회전을 추가합니다(부모를 기준으로).
            trans.ValueRW.Rotation = math.mul(spin, trans.ValueRO.Rotation);
        }
    }


}

포탄 이동 및 파괴

CannonBallSystem.cs 스크립트 생성

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

public partial struct CannonBallSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // EndSimulationEntityCommandBufferSystem의 싱글톤 인스턴스를 가져옵니다.
        // EndSimulationEntityCommandBufferSystem은 엔티티 명령 버퍼 시스템의 일종으로, 엔티티 명령을 모아서 나중에 한 번에 실행합니다.
        var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

        var cannonBallJob = new CannonBallJob
        {
            // WorldUnmanaged는 현재 시스템의 World 객체를 관리하는 구조체입니다.
            // 이 객체는 시스템이 속해 있는 월드를 나타내며, 명령 버퍼를 생성할 때 필요한 정보를 제공합니다.
            ECB = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged),
            DeltaTime = SystemAPI.Time.DeltaTime
        };

        cannonBallJob.Schedule();
    }
}

// IJobEntity는 소스 생성을 사용하여 
// Execute 메서드의 서명에서 쿼리를 암시적으로 정의합니다.
// 이 경우 암시적 쿼리는 CannonBall 및
// LocalTransform 컴포넌트를 가진 모든 엔티티를 찾습니다. 
[BurstCompile]
public partial struct CannonBallJob : IJobEntity
{
    // ECB는 잡 내에서 엔티티를 안전하게 생성, 삭제 또는 수정하기 위해 사용
    public EntityCommandBuffer ECB;
    public float DeltaTime;

    // Execute는 CannonBall 및 LocalTransform 컴포넌트를 가진 모든 엔티티마다 한 번 호출됩니다.
    void Execute(Entity entity, ref CannonBall cannonBall, ref LocalTransform transform)
    {
        var gravity = new float3(0.0f, -9.82f, 0.0f);

        transform.Position += cannonBall.Velocity * DeltaTime;

        // 땅에 닿는 경우
        if (transform.Position.y <= 0.0f)
        {
            // 포탄 엔티티를 삭제
            ECB.DestroyEntity(entity);
        }

        cannonBall.Velocity += gravity * DeltaTime;
    }
}

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤