Skip to content

Core API ​

GodotECS.Core — entities, components, type registry, chunks, entity manager, queries, command buffers, worlds and systems.

Documentation target: GodotECS commit 699338bcefd3edc31ff6b82a054932402f4d2970 (v0.2.x). This page follows the implementation, not the frozen historical docs/API.md snippets.

Component markers ​

csharp
public interface IComponentData { }
public interface IBufferElementData { }
public interface ISharedComponent { }
public interface IEnableableComponent { }
public interface ISingleton { }
public interface IBlobAsset { }

Markers are empty classifier interfaces. IEnableableComponent lets a component column keep its archetype while individual rows are enabled/disabled.

Entity ​

csharp
public readonly struct Entity : System.IEquatable<Entity>
{
    public readonly int Index;
    public readonly int Version;
    public static readonly Entity Null = new Entity(-1, 0);
    public Entity(int index, int version);
    public bool IsNull { get; }
}

Entity is an unmanaged (Index, Version) handle. Destroying an entity bumps its version, so a stale handle does not become valid again when the index is recycled.

Core components ​

csharp
public struct LocalTransform : IComponentData
{
    public Godot.Vector3 Pos;
    public Godot.Quaternion Rot;
    public float Scale;
    public static LocalTransform Identity { get; }
    public Godot.Transform3D ToTransform3D();
}
public struct PrevTransform : IComponentData { public Godot.Vector3 Pos; public Godot.Quaternion Rot; public float Scale; }
public struct Velocity : IComponentData { public Godot.Vector3 Value; }
public struct Health : IComponentData { public int Value; public int Max; }
public struct DestroyTag : IComponentData { }
public struct DisabledTag : IComponentData, IEnableableComponent { }
public struct Parent : IComponentData { public Entity Value; }
public struct NetworkId : IComponentData { public uint Id; }
public struct QualityConfig : ISingleton
{
    public int PhysicsHz;
    public float LodBias;
    public static QualityConfig Default { get; }
}

NetworkId is defined only in GodotECS.Core and is intentionally passive. QualityConfig is installed by the World constructor; the GDScript facade exposes its two values.

Type registry ​

csharp
public enum Allocator { TempJob, Persistent }
public sealed class ComponentVersionAttribute : System.Attribute
{
    public int Version { get; }
    public ComponentVersionAttribute(int version);
}
public readonly struct TypeIndex { public int Value { get; } }
public readonly struct ComponentTypeInfo
{
    public TypeIndex Index;
    public System.Type Type;
    public int Size;
    public int Align;
    public int Version;
    public bool IsShared;
    public bool IsBuffer;
    public bool IsEnableable;
}
public static class TypeRegistry
{
    public static bool TryGetInfo(System.Type type, out ComponentTypeInfo info);
    public static ComponentTypeInfo GetInfo(TypeIndex index);
    public static int Count { get; }
}
public static class TypeRegistry<T> where T : struct
{
    public static readonly TypeIndex Index;
    public static readonly int Size;
    public static readonly int Align;
    public static readonly int ComponentVersion;
    public static readonly bool IsShared;
    public static readonly bool IsEnableable;
    public static readonly bool IsBuffer;
}

Types receive a TypeIndex on first touch. Registration is thread-safe; the type metadata is computed once per component type. TypeIndex values follow runtime registration order and are not stable across independent builds with different registration order.

Archetype and Chunk ​

Chunks use a fixed 16 KiB storage target.

csharp
public sealed class Archetype
{
    public int Id { get; }
    public TypeIndex[] Types { get; }
    public int[] Sizes { get; }
    public int[] Offsets { get; }
    public int Stride { get; }
    public int Capacity { get; }
    public System.Collections.Generic.IReadOnlyList<Chunk> Chunks { get; }
    public bool IsEnableable(int column);
    public bool Has(TypeIndex t);
    public bool TryGetColumn(TypeIndex t, out int column);
    public int GetColumn(TypeIndex t);
    public T GetSharedValue<T>(TypeIndex t) where T : struct;
}

public sealed class Chunk
{
    public const int SizeBytes = 16 * 1024;
    public Archetype Archetype { get; }
    public int Capacity { get; }
    public int Count { get; internal set; }
    public Godot.Vector3 AabbMin;
    public Godot.Vector3 AabbMax;
    public bool HasAabb;
    public System.Span<T> GetSpan<T>(int column) where T : struct;
    public ref T GetRef<T>(int column, int row) where T : struct;
    public Entity GetEntity(int row);
    public System.Span<Entity> EntitySpan { get; }
    public uint GetChangeVersion(int column);
    public void SetChangeVersion(int column, uint version);
    public bool DidChange(int column, uint lastSystemVersion);
    public bool IsEnabled(int column, int row);
    public void SetEnabled(int column, int row, bool enabled);
    public void SetAabb(in Godot.Vector3 min, in Godot.Vector3 max);
    public void InvalidateAabb();
    public void ExpandAabb(in Godot.Vector3 p);
}

public readonly struct ChunkRef
{
    public Chunk Chunk { get; }
    public int Count { get; }
    public Archetype Archetype { get; }
    public System.Span<T> GetSpan<T>(int column) where T : struct;
    public Entity GetEntity(int row);
    public System.Span<Entity> Entities { get; }
}

GetSpan<T> exposes only live rows and performs a size check; a wrong generic type cannot silently alias a column. Chunk AABBs are maintained by the systems that own culling/physics data.

EntityManager ​

csharp
public sealed class EntityManager
{
    public Entity Create();
    public void CreateBatch(System.Span<Entity> outEntities);
    public Entity Instantiate(Entity prefab);
    public void Destroy(Entity e);
    public void DestroyBatch(System.ReadOnlySpan<Entity> entities);
    public int CopyLiveEntities(System.Span<Entity> dst);
    public int LiveCount { get; }
    public bool IsAlive(Entity e);

    public void AddComponent<T>(Entity e) where T : struct, IComponentData;
    public void RemoveComponent<T>(Entity e) where T : struct, IComponentData;
    public void SetEnable<T>(Entity e, bool enable) where T : struct, IEnableableComponent;
    public bool IsEnabled<T>(Entity e) where T : struct, IEnableableComponent;

    public void SetComponentData<T>(Entity e, in T data) where T : struct, IComponentData;
    public T GetComponentData<T>(Entity e) where T : struct, IComponentData;
    public bool HasComponent<T>(Entity e) where T : struct, IComponentData;

    public void SetShared<T>(Entity e, in T data) where T : struct, ISharedComponent;
    public T GetShared<T>(Entity e) where T : struct, ISharedComponent;
    public DynamicBuffer<T> GetBuffer<T>(Entity e) where T : struct, IBufferElementData;

    public EntityQuery CreateQuery(in QueryDesc desc);
    public int Count(EntityQuery query);
    public uint GlobalSystemVersion { get; }
    public void BumpSystemVersion();
}

Structural changes move rows between archetypes. Data writes and structural changes advance GlobalSystemVersion, which backs change-version queries.

Instantiate(Entity prefab) deep-copies components, shared values, enable bits and dynamic buffers. Since v0.2 it also clones the entire Parent subtree rooted at the prefab: cloned parents are created before children, cloned Parent values are remapped, and references to entities outside the subtree remain unchanged.

Destroy uses swap-remove. When a chunk becomes empty it is deregistered from the archetype; one empty chunk is retained to avoid create/destroy thrashing.

There is no EntityManager.Instantiate(EntityScene) overload. Use EntityScene.Instantiate(world, pos) for serialized prefabs.

DynamicBuffer and queries ​

csharp
public struct DynamicBuffer<T> where T : struct, IBufferElementData
{
    public int Length { get; }
    public int Capacity { get; }
    public ref T this[int i] { get; }
    public void Add(in T item);
    public void Clear();
    public System.Span<T> AsSpan();
}

public struct QueryDesc
{
    public System.Type[] All;
    public System.Type[] Any;
    public System.Type[] None;
}

public sealed class EntityQuery
{
    public bool Matches(Entity e);
    public int Count();
}

DynamicBuffer<T> has no raw unsafe pointer API. Use the indexer or AsSpan().

Query matching chunks are cached. A Refresh reuses the existing chunk snapshot when structural state, archetype count and total chunk count have not changed; this avoids rebuilding matching-chunk arrays on every call.

csharp
public static class Query
{
    public static QueryDesc Of<T0>() where T0 : struct;
    public static QueryDesc Of<T0, T1>() where T0 : struct where T1 : struct;
    public static QueryDesc Of<T0, T1, T2>() where T0 : struct where T1 : struct where T2 : struct;
    public static QueryDesc Of<T0, T1, T2, T3>() where T0 : struct where T1 : struct where T2 : struct;
    public static QueryDesc Of<T0, T1, T2, T3, T4>() where T0 : struct where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
}

public static class EntityQueryExt
{
    public static bool DidChangeSince(this EntityQuery q, uint lastSystemVersion);
    public static int CountEnabled<TEnable>(this EntityQuery q)
        where TEnable : struct, IEnableableComponent;
}

EntityCommandBuffer ​

csharp
public struct EntityCommandBuffer : System.IDisposable
{
    public EntityCommandBuffer(Allocator allocator);
    public bool IsCreated { get; }
    public int Count { get; }
    public Entity Create();
    public void Destroy(Entity e);
    public void Add<T>(Entity e) where T : struct, IComponentData;
    public void Remove<T>(Entity e) where T : struct, IComponentData;
    public void Set<T>(Entity e, in T data) where T : struct, IComponentData;
    public void SetEnable<T>(Entity e, bool enable) where T : struct, IEnableableComponent;
    public Entity Instantiate(Entity prefab);
    public void Playback(EntityManager dst);
    public void Clear();
    public void Dispose();
    public ParallelWriter AsParallelWriter();
}

Playback snapshots the recorded command list and sorts commands deterministically by (sortKey, sequence). It does not implicitly clear the buffer; Clear/Dispose reuse the storage.

Systems, scheduling and registration ​

csharp
public interface ISystem
{
    void OnCreate(ref SystemState state);
    void OnUpdate(ref SystemState state);
    void OnDestroy(ref SystemState state);
}

public abstract class SystemBase : ISystem
{
    public bool Enabled { get; set; }
}

public struct SystemState
{
    public World World { get; }
    public EntityManager Entities { get; }
    public EntityCommandBuffer ECB { get; }
    public float FixedDelta { get; }
    public float Delta { get; }
    public double Alpha { get; }
    public uint Tick { get; }
    public uint LastSystemVersion { get; }
    public EcsRng Rng { get; internal set; }

    public EntityQuery Query(in QueryDesc desc);
    public GodotECS.Jobs.JobHandle ScheduleParallel<TJob,T0>(ref TJob job, EntityQuery query, int batchSize = 0)
        where TJob : struct, GodotECS.Jobs.IJobEntityChunk<T0> where T0 : struct;
    // plus the 2-, 3-, 4- and 5-component overloads
}

public sealed class SystemGroup
{
    public string Name { get; }
    public void Add<T>() where T : struct, ISystem;
    public void Add(SystemBase system);
    public void SetEnabled<T>(bool enable);
    public void Update(float delta);
}

Automatic group routing ​

World exposes four real groups: Initialization, FixedStep, Variable, and Presentation.

csharp
[UpdateInGroup(typeof(FixedStepGroup))]
public struct MyPhysicsSystem : ISystem { /* ... */ }

world.RegisterSystem<MyPhysicsSystem>();
world.RegisterSystem(existingSystemBase);

RegisterSystem<T>() resolves [UpdateInGroup] to the corresponding group instance in that World. A system with no UpdateInGroup attribute defaults to Variable. An unknown group marker throws InvalidOperationException. SystemGroup.Add(...) also validates a declared group and rejects a system explicitly declared for another group.

Ordering ​

System order is not merely insertion order. SystemGroup rebuilds a stable topological order from [UpdateBefore(typeof(...))] and [UpdateAfter(typeof(...))] declarations. References to types not present in the group are ignored; a cycle throws. The ordered array is cached, so per-frame scheduling does not re-run the graph construction.

Each group update executes enabled systems, then plays back that group's ECB, then bumps GlobalSystemVersion.

Deterministic RNG and fixed time ​

Every system gets its own RNG fork derived from the group's stream, system position and current tick, so one system's draws do not consume a neighbor's stream.

World.TickFixed(frameDelta) uses FixedConfig to accumulate variable frame time, executes 0..MaxSubsteps fixed updates, drops excess backlog (incrementing SpillCount) to avoid the spiral of death, and updates Tick, LastFixedDelta, Alpha and EcsTime.Elapsed. Elapsed tracks simulated time, not wall time; dropped backlog is not counted.

FixedConfig and EcsTime have typed fast paths inside World to avoid dictionary/boxing overhead on the fixed-step hot path while preserving the public singleton API.

Performance contract ​

Keep EntityQuery instances cached in OnCreate; SystemState.Query(...) is intended for setup, not repeated hot-path construction. ScheduleParallel is chunk-oriented and deterministic; Scheduler.ForceSequential is the deterministic single-thread fallback. ChunkRef exists specifically so jobs can work from a stable chunk snapshot without repeatedly rebuilding query state.

GodotECS docs — version 0.2.0. All rights reserved until a license is added.