AgentSkillsCN

entity-command-buffer-helper

支持在多线程之间安全地创建EntityCommandBuffer(ECB),以实现结构性变更(添加/移除组件、创建/销毁实体)。当用户需要在任务中修改实体结构、生成/销毁实体,或处理延迟的结构性变更时,可使用此功能。

SKILL.md
--- frontmatter
name: entity-command-buffer-helper
description: Hỗ trợ tạo EntityCommandBuffer (ECB) an toàn giữa các luồng để thực hiện structural changes (add/remove component, create/destroy entity). Dùng khi user cần modify entity structure trong job, spawn/destroy entity, hoặc xử lý deferred structural changes.

Entity Command Buffer Helper

Critical Instructions

  • Never Structural Change in Job Directly: KHÔNG gọi EntityManager.AddComponent() trong job — phải dùng ECB.
  • Choose Right Playback Point: Mỗi ECB system playback tại thời điểm khác nhau. Chọn sai = race condition.
  • Parallel Writer Safety: Dùng .AsParallelWriter() trong parallel jobs, với sortKey = unfilteredChunkIndex hoặc entityInQueryIndex.
  • Project Convention: Pattern ECB chuẩn hóa cho toàn bộ VisualWorld project.

Core Concepts

What is EntityCommandBuffer?

ECB ghi lại ("record") các thao tác structural change, rồi "playback" chúng tại 1 sync point an toàn trên main thread. Điều này cho phép jobs ghi structural changes mà không gây conflict.

Structural Changes (cần ECB)

  • CreateEntity() / DestroyEntity()
  • AddComponent<T>() / RemoveComponent<T>()
  • SetSharedComponent<T>()
  • Instantiate() (clone entity/prefab)
  • AddBuffer<T>() / SetBuffer<T>()

Non-Structural Changes (KHÔNG cần ECB)

  • SetComponent<T>() trên component đã tồn tại → dùng RefRW<T> trực tiếp
  • Write vào NativeArray, DynamicBuffer → trực tiếp trong job

ECB Playback Points

Built-in ECB Systems

code
Frame timeline:
├── BeginInitializationEntityCommandBufferSystem
│     └── ECB playback → structural changes TRƯỚC initialization
├── InitializationSystemGroup
│     └── ... your init systems ...
├── EndInitializationEntityCommandBufferSystem
│
├── BeginSimulationEntityCommandBufferSystem    ← COMMON CHOICE
│     └── ECB playback → structural changes TRƯỚC simulation
├── SimulationSystemGroup
│     ├── ... physics, game logic ...
│     ├── YOUR SYSTEMS HERE
│     └── EndSimulationEntityCommandBufferSystem ← COMMON CHOICE
│           └── ECB playback → structural changes SAU simulation
│
├── BeginPresentationEntityCommandBufferSystem
├── PresentationSystemGroup
│     └── ... rendering ...
└── EndPresentationEntityCommandBufferSystem

Choosing the Right Point

ScenarioRecommended ECB SystemReason
Spawn entity cần xử lý trong frame nàyBeginSimulationECBSystemEntity sẵn sàng cho simulation systems
Destroy entity sau khi xử lý xongEndSimulationECBSystemĐảm bảo tất cả systems đã đọc entity
Spawn entity cho renderingBeginPresentationECBSystemEntity sẵn sàng cho render systems
Init setup (one-time spawn)BeginInitializationECBSystemChạy sớm nhất
Cleanup/despawn sau frameEndSimulationECBSystemCuối frame, an toàn

Usage Patterns

Pattern 1: Single-Threaded ECB (SystemAPI.foreach)

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

/// <summary>
/// Spawn entity từ requests. ECB single-threaded, inline trong System.
/// </summary>
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct SimpleSpawnSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<SpawnRequest>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // Lấy ECB singleton — KHÔNG tạo bằng new EntityCommandBuffer()
        var ecbSingleton = SystemAPI
            .GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
        var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

        foreach (var (request, entity) in
            SystemAPI.Query<RefRO<SpawnRequest>>().WithEntityAccess())
        {
            // Instantiate prefab
            var spawned = ecb.Instantiate(request.ValueRO.Prefab);

            // Set component trên entity MỚI (chưa tồn tại → phải dùng ECB)
            ecb.SetComponent(spawned, LocalTransform.FromPosition(
                request.ValueRO.Position
            ));

            // Add tag
            ecb.AddComponent<ActiveFlag>(spawned);

            // Consume request
            ecb.DestroyEntity(entity);
        }
        // ECB tự playback tại BeginSimulationECBSystem sync point
    }
}

Pattern 2: Parallel ECB (IJobEntity)

csharp
/// <summary>
/// Job xử lý song song với ECB.ParallelWriter.
/// QUAN TRỌNG: sortKey phải unique per entity để đảm bảo deterministic.
/// </summary>
[BurstCompile]
public partial struct ParallelDestroyJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter ECB;

    void Execute(
        [ChunkIndexInQuery] int sortKey,    // ← sortKey cho determinism
        Entity entity,
        in HealthComponent health)
    {
        if (health.Current <= 0)
        {
            ECB.DestroyEntity(sortKey, entity);

            // Spawn debris
            var debris = ECB.CreateEntity(sortKey);
            ECB.AddComponent(sortKey, debris, new DebrisData
            {
                Position = /* ... */,
                Velocity = /* ... */
            });
        }
    }
}

// Trong System:
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct DeathSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var ecbSingleton = SystemAPI
            .GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
        var ecb = ecbSingleton
            .CreateCommandBuffer(state.WorldUnmanaged)
            .AsParallelWriter();  // ← PARALLEL WRITER

        state.Dependency = new ParallelDestroyJob
        {
            ECB = ecb
        }.ScheduleParallel(state.Dependency);
    }
}

Pattern 3: ECB with DynamicBuffer

csharp
/// <summary>
/// Thêm buffer elements vào entity mới qua ECB.
/// </summary>
[BurstCompile]
public void SpawnWithBuffer(EntityCommandBuffer ecb, float3 position)
{
    var entity = ecb.CreateEntity();
    ecb.AddComponent(entity, LocalTransform.FromPosition(position));

    // Add buffer và populate
    var buffer = ecb.AddBuffer<NeighborElement>(entity);
    buffer.Add(new NeighborElement { Value = Entity.Null });
}

Pattern 4: Deferred Companion (Tag + Process)

csharp
// Bước 1: Job đánh tag (nhanh, parallel)
[BurstCompile]
public partial struct TagForProcessingJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter ECB;

    void Execute([ChunkIndexInQuery] int sortKey, Entity e, in SomeCondition cond)
    {
        if (cond.ShouldProcess)
            ECB.AddComponent<NeedsProcessingTag>(sortKey, e);
    }
}

// Bước 2: System xử lý tagged entities (frame sau)
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(TaggingSystem))]
public partial struct ProcessTaggedSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<NeedsProcessingTag>();
    }

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

        foreach (var (data, entity) in
            SystemAPI.Query<RefRW<SomeData>>()
                .WithAll<NeedsProcessingTag>()
                .WithEntityAccess())
        {
            // Process...
            data.ValueRW.Processed = true;

            // Remove tag khi xong
            ecb.RemoveComponent<NeedsProcessingTag>(entity);
        }
    }
}

Project-Specific Patterns (VisualWorld)

Voxel Destruction → Debris Spawn

csharp
/// <summary>
/// Khi voxel bị phá hủy (structural collapse), spawn Physics Entity debris.
/// Gom voxels bị phá thành clusters, mỗi cluster = 1 debris entity.
/// </summary>
[BurstCompile]
public partial struct DebrisSpawnJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter ECB;
    public Entity DebrisPrefab;

    void Execute(
        [ChunkIndexInQuery] int sortKey,
        Entity chunkEntity,
        in ChunkCoordinate coord,
        in DirtyChunkTag dirty)
    {
        // Scan voxels có Stress > MaxLoad
        // Gom thành clusters
        // Mỗi cluster → ECB.Instantiate(DebrisPrefab)
        // Set position, mass, velocity
    }
}

Phase Change (Solid → Liquid)

csharp
/// <summary>
/// Khi voxel đạt MeltingPoint → đổi State từ Solid sang Liquid.
/// Cần ECB vì thay đổi SharedComponent (material group có thể đổi).
/// </summary>
// Dùng EndSimulation ECB vì thermodynamics chạy trong Simulation

Anti-Patterns (TRÁNH)

csharp
// ❌ SAI: Tạo ECB bằng constructor → phải tự Dispose, dễ quên
var ecb = new EntityCommandBuffer(Allocator.TempJob);
ecb.Playback(entityManager);
ecb.Dispose(); // Nếu quên → memory leak

// ✅ ĐÚNG: Dùng Singleton pattern → tự động playback & dispose
var ecb = SystemAPI.GetSingleton<BeginSimulationECBSystem.Singleton>()
    .CreateCommandBuffer(state.WorldUnmanaged);

// ❌ SAI: Dùng EntityManager trực tiếp trong Job → crash
void Execute(int i) {
    entityManager.CreateEntity(); // KHÔNG ĐƯỢC!
}

// ❌ SAI: sortKey = 0 cho tất cả → non-deterministic
ECB.DestroyEntity(0, entity); // Luôn dùng chunkIndex hoặc entityInQueryIndex

// ❌ SAI: Structural change trên main thread giữa 2 jobs → sync point ngầm
state.EntityManager.AddComponent<Tag>(entity); // Forces sync → kills perf

Validation Checklist

  • ECB lấy từ Singleton, không new EntityCommandBuffer()
  • Parallel jobs dùng .AsParallelWriter() với sortKey
  • sortKey = [ChunkIndexInQuery] hoặc tương đương, không hardcode 0
  • Playback point phù hợp (Begin/End + Init/Sim/Present)
  • Không gọi EntityManager structural methods trong jobs
  • Không gọi EntityManager structural methods giữa job scheduling
  • ECB commands referencing newly created entities sử dụng returned Entity handle
  • System có [UpdateInGroup] và ordering đúng relative to ECB system