Files
GhostEngine/doc/docs/ecs-workflows.md
Misaki d8a7b07624 feat(graphics): improve rendering pipeline and docs
- Refactor D3D12 backend and RenderGraph module
- Update graphics RHI and core rendering components
- Add Random.hlsl shader include
- Regenerate API documentation and update user guides
2026-03-27 22:23:44 +09:00

3.0 KiB

ECS Workflows

This document outlines standard daily workflows for working within GhostEngine's ECS.

Creating Entity Queries

To read or modify data, you first build an EntityQuery. You use the QueryBuilder struct to define inclusion and exclusion constraints.

Identifier<EntityQuery> queryId = QueryBuilder.Create()
    .WithAll<Position>()           // Must have Position
    .WithAny<Player, Enemy>()      // Must have Player OR Enemy
    .WithNone<Dead>()              // Must NOT have Dead structurally
    .WithDisabled<Renderable>()    // Must have Renderable structurally, but it must be disabled
    .Build(world);

Queries are hashed and cached internally by the ComponentManager. Building the same query mask twice will yield the same Identifier<EntityQuery>.

Iterating Chunk Views

Once you have a query, use GetChunkIterator() to traverse the unmanaged memory directly. This returns a ChunkView struct.

Because the data is unmanaged, you can extract Span<T> and modify it with extreme speed.

ref var query = ref world.ComponentManager.GetEntityQueryReference(queryId);

foreach (var chunkView in query.GetChunkIterator())
{
    // Get Read-Only Span
    ReadOnlySpan<Velocity> vels = chunkView.GetComponentData<Velocity>();
    
    // Get Read-Write Span
    Span<Position> pos = chunkView.GetComponentDataRW<Position>();

    // For loop for cache-aligned processing
    for (int i = 0; i < chunkView.EntityCount; i++)
    {
        pos[i].Value += vels[i].Value * dt;
    }
}

Note: Accessing data using GetComponentDataRW<T>() will automatically bump the internal version numbers for that component type in that specific chunk, allowing other systems to detect changes efficiently.

Entity Command Buffers (Structural Changes)

In ECS, structural changes (adding/removing components, destroying entities) reorganize chunk memory. Doing this while iterating over chunks invalidates the memory layout.

To safely defer structural changes, use an EntityCommandBuffer.

// Get the world's main ECB
var ecb = world.EntityCommandBuffer;

foreach (var chunk in query.GetChunkIterator())
{
    var entities = chunk.GetEntities();
    var healths = chunk.GetComponentData<Health>();

    for (int i = 0; i < chunk.EntityCount; i++)
    {
        if (healths[i].Value <= 0)
        {
            // Defer the destruction until Playback() is called
            ecb.DestroyEntity(entities[i]);
        }
    }
}

At the end of the frame (or the end of the system group update), you must call:

world.PlaybackEntityCommandBuffers();

Multi-threading ECBs

When using the JobScheduler, you can record deferred commands across multiple worker threads concurrently by requesting thread-local ECBs:

int workerIndex = JobScheduler.CurrentThreadIndex;
var threadEcb = world.GetThreadLocalEntityCommandBuffer(workerIndex);

// Safely record structural changes from parallel jobs
threadEcb.AddComponent<Dead>(entity);