게임엔진/ECS(Unity)

ECB + sortKey

tsyang 2025. 11. 30. 10:36

ECB


 

유니티 ECS에는 EntityCommandBuffer(ECB)라는 개념이 있다.

 

엔티티의 생성/파괴, 컴포넌트 추가/제거 등구조변경(Structural Change)및 Sync Point를 만들어서 성능에 악 영향을 준다.

 

특히나 병렬 Job에서는 구조 변경 자체가 불가하다. 

 

 

그래서 여러 Job등에서 구조변경을 유발하는 명령들을 ECB에 모아두고 한번에 Playback한다. 이렇게 하면 병렬 Job에서도 구조 변경이 안전하게 가능해지고, Sync Point를 한 번만 만들어내서 성능에도 좋다.

 

 

ECB는 두 가지 방법으로 사용할 수 있는데, 다음과 같다.

  1. 그때그때 직접 만들기
  2. 이미 있는 걸 쓰기

 

아래와 같이 체력이 0이하인 Entity를 파괴하는 Job이 있다. 이 Job은 병렬로 실행할거라 ECB를 외부에서 받아 사용해야 한다.

[BurstCompile]
public partial struct DestroyDeadJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter Ecb;

    void Execute(
        [EntityIndexInQuery] int sortKey,
        Entity entity,
        in Health health)
    {
        if (health.Value <= 0)
        {
            Ecb.DestroyEntity(sortKey, entity);
        }
    }
}

 

 

1. 그때 그때 ECB 만들어 쓰기

[BurstCompile]
public partial struct DestroyDead_DirectECBSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 1) 이 시스템 안에서 쓸 ECB를 직접 생성
        var ecb = new EntityCommandBuffer(Allocator.TempJob);

        // 2) 병렬 잡에 넘겨서 커맨드 기록
        var job = new DestroyDeadJob
        {
            Ecb = ecb.AsParallelWriter()
        };

        var handle = job.ScheduleParallel(state.Dependency);
        handle.Complete(); // 반드시 완료 후에 Playback

        // 3) 원하는 타이밍에 직접 Playback
        ecb.Playback(state.EntityManager);
        ecb.Dispose();

        state.Dependency = handle;
    }
}

 

만든 ECB의 Playback()과, Dispose()를 꼭 호출해줘야 한다는 점을 명심하자.

 

 

2. 원래 있는 걸 쓰기

[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct DestroyDead_UsingECBSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        // EndSimulation ECB 시스템이 존재할 때만 동작하도록
        state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 1) 이미 존재하는 EndSimulation ECB 싱글턴에서 커맨드 버퍼 생성
        var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
        var ecb          = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();

        // 2) 잡에 ParallelWriter 전달
        var job = new DestroyDeadJob
        {
            Ecb = ecb
        };

        state.Dependency = job.ScheduleParallel(state.Dependency);
        // 의존성 등록은 ECB 시스템이 처리 (버전별 패턴 약간 차이 가능)
    }
}

 

SimulationSystemGroup이 끝날 때 Playback되는 EndSimulationEntityCommandBufferSystem에 접근하여 이것을 사용한다.

 

차이점이 있다면, 직접 만든 것과 달리 Playback이나 Dispose를 호출하지 않는다. (호출하면 에러가 난다.) 

 

Playback을 하면 SyncPoint가 생성되는데, 이 때 모든 병렬 Job들이 SyncPoint를 대기하기 때문에 성능좋지 않다. 따라서 가급적 ECB의 Playback은 하나로 뭉쳐주는 것이 병렬작업 효율 최대화 하기 좋다. 따라서 구조 변경이 즉시 반영되어야 하는 게 아니라면 ECB 하나에 구조 변경을 몰아주는 것이 좋다.

 


직접 만들어 쓸 수도 있다.

using Unity.Entities;

// SimulationGroup 중간 어딘가에서 Playback 하고 싶은 ECB 시스템
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(BeginSimulationEntityCommandBufferSystem))]
public partial class MidSimulationEntityCommandBufferSystem : EntityCommandBufferSystem
{
   
}


[BurstCompile]
public partial struct SomeSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<MidSimulationEntityCommandBufferSystem.Singleton>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var ecbSingleton = SystemAPI.GetSingleton<MidSimulationEntityCommandBufferSystem.Singleton>();
        var ecb          = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();

        var job = new SomeJob { Ecb = ecb };
        state.Dependency = job.ScheduleParallel(state.Dependency);
    }
}

 

 

 

SortKey


 

그럼 SortKey는 무엇인가?

 

다시 위에서 봤던 HP가 0이하인 엔티티를 제거하는 Job을 보자.

[BurstCompile]
public partial struct DestroyDeadJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter Ecb;

    void Execute(
        [EntityIndexInQuery] int sortKey,
        Entity entity,
        in Health health)
    {
        if (health.Value <= 0)
        {
            Ecb.DestroyEntity(sortKey, entity);
        }
    }
}

 

Ecb.DestoryEntity를 할 때 첫 번째 인자로 sortKey를 넘겨주고 있음을 볼 수 있다.

 

 

ECB를 ParallelWriter로 쓰게 되면 Lock을 방지하기 위해 쓰레드 별로 따로 버퍼를 둔다. 그리고 Playback이전에 이것들을 하나로 합쳐서 하나씩 수행하게 된다. (구조 변경은 메인쓰레드에서만 가능하다.)

 

sortKey는 쓰레드별 버퍼를 하나로 합칠 때 어떤 순서로 합칠 것인지에 대한 우선순위이다.

 

딱히 어떻게 되든 상관이 없다면 0으로 넘겨줘도 되고, 작업이 엄격하게 결정적(Deterministic)이어야 한다면 직접 sortKey를 관리해 주는 것이 좋다.

    void Execute(
        [EntityIndexInQuery] int sortKey,

 

위 코드를 보면 [EntityIndexInQuery]를 주고 있는데, 이것은 쿼리 내에서 해당 엔티티가 몇번째인지를 유니티ECS에서 자동으로 넘겨준다고 보면 된다.

 

자매품으로 [EntityIndexInChunk]가 있다. (이름처럼 청크에서 몇번째인지)

 

 

'게임엔진 > ECS(Unity)' 카테고리의 다른 글

Entities - 컴포넌트 구조  (0) 2023.02.25