Nukecs is a Burst-compiled ECS framework for Unity. It uses unsafe code and raw pointers throughout for maximum performance. A source generator at ../../SourseGen/NUKECSGEN/ generates system runners and component type registrations.
- Runtime: .NET Framework 4.7.1 (Unity legacy)
- Dependencies: Unity.Burst, Unity.Collections, Unity.Jobs, Unity.Mathematics
- Assembly defs:
Nukecs.asmdef(runtime),Nukecs.Tests.asmdef(tests),AllocatorEditor.asmdef(debug) [BurstCompile]used on hot paths
World → Archetype[] → Entity (int ID)
→ Queries[] → QueryEnumerator / Query<T1..TN, TOption> / Chunk<T1..T8>
→ Systems → OnUpdate() dispatch (onStart/onUpdate/onFixedUpdate/onDestroy)
→ ECB → deferred add/remove/destroy
→ Events<T> → thread-safe event buffers (spinlock-protected)
→ Res<T> → singleton resources (unmanaged + managed)
→ Reactive<T> → change detection via memcmp + Changed<T> tags
→ HotReload → Roslyn-based runtime system recompilation
- World — central container: entity storage, archetype management, pools, queries (
src/World/World.cssafe wrapper,src/World/World.Unsafe.cscore). Static management viaWorld.Static.cs(SharedStaticworld list,ALLOCATORstruct). Disposal inWorld.Free.cs. - Archetype — stores entities with same component mask; SoA data layout (
src/Archetype.cs) - Entity — lightweight ID (
int); location stored inWorld.entityLocations(archetypeIndex + row) - Query — matches entities by component mask; iterates via
QueryEnumeratoror genericQuery<T1..TN, TOption>(src/Query.cs,src/Systems/FnSystems/Query.cs) - Systems — functions with
[System]attribute; source-gen creates runners; 3 thread modes: Main, Single, Parallel (src/Systems/Systems.cs). Lifecycle lists:onStart,onUpdate,onFixedUpdate,onDestroy.ISystemRunnerinterface for struct/class systems. - EntityCommandBuffer (ECB) — deferred add/remove/destroy; flushed on
world.Update()(src/Entity/EntityCommandBuffer.cs) - Component Storage — two modes: inline (packed in archetype data array) and Pool (separate
GenericPool<T>with SparseSet) (src/Components/GenericPool.cs) - Allocator — custom
MemAllocatorwithptr<T>wrapper,MemoryArray<T>,MemoryList<T>(src/Allocator/) - ISystemParam — unified interface for system parameters: Query, Res, State, Events, Single, Local, Chunk. Source generator recognizes these and wires them up.
- Events —
Events<TEvent>thread-safe event buffer;AddParfor parallel writes (spinlock);EventsParallelReader<TEvent>for parallel reads;EventsStoragecentral registry (src/Systems/FnSystems/Events.cs) - Resources —
Res<T>/ResManaged<T>singleton resource accessors;IResinterface withOnCreate/OnUpdate;ResStorageunmanaged storage (src/Systems/FnSystems/Res.cs,ResManaged.cs,ResStorage.cs) - Chunk Iteration —
Chunk<T1..T8>archetype chunk iterators;IChunkinterface; direct pointer iteration over archetype component arrays (src/Systems/FnSystems/Chunk.cs) - Reactive —
IReactivemarker +Reactive<T>stores old value +Changed<T>tag;ReactiveCheckSystem<T>detects changes via memcmp;ReactAndClearSystem<T>clears tags and fires callbacks (src/Reactive/) - Hot Reload —
HotReloadSystemswrapsSystems; file watching + Roslyn compilation + runner swapping at runtime (src/Systems/HotReload/,src/Unity/Editor/HotReload/) - DynamicBuffer —
DynamicBuffer<T>Unity-style dynamic buffer component (src/Components/DynamicBuffer.cs) - IEntityJobSystem — per-entity job system interface;
EntityJobSystemRunner<T>dispatches (src/Systems/EntityJobSystem.cs)
| File | Description |
|---|---|
src/Archetype.cs |
ArchetypeUnsafe: entity CRUD, batch ops, archetype edges/transitions, SoA data layout |
src/World/World.Unsafe.cs |
WorldUnsafe: entity create/destroy, archetype registry, query management, batch ops |
src/World/World.cs |
Safe World wrapper |
src/World/World.Entities.cs |
(stub — entity helpers moved) |
src/World/World.Components.cs |
AspectType definitions |
src/World/World.Allocation.cs |
World memory allocation helpers |
src/World/World.Static.cs |
Static world creation/management, ALLOCATOR struct, SharedStatic world list |
src/World/World.Free.cs |
World/WorldUnsafe disposal logic |
src/World/World.StoryLog.cs |
Debug ring-buffer for component changes (#if NUKECS_DEBUG) |
src/World/nukecs.cs |
Version info struct (NukEcs { version, name, author }) |
src/Query.cs |
QueryUnsafe: archetype matching, entity tracking, QueryEnumerator |
src/Entity/Entity.cs |
Entity struct + extension methods (Get, Set, Add, Remove, Destroy, Copy) |
src/Entity/EntityCommandBuffer.cs |
Deferred operations buffer |
src/Entity/EntityAspectExtensions.cs |
Aspect-related entity extension methods |
src/Entity/EntityArrayExtensions.cs |
Array operation entity extensions |
src/Entity/EntityChildrenExtensions.cs |
Parent/child entity hierarchy extensions |
| File | Description |
|---|---|
src/Components/ComponentTypeMap.cs |
Static type → index registry |
src/Components/ComponentTypeData.cs |
ComponentTypeData struct (size, storageType, etc.) |
src/Components/ComponentType.cs |
Per-type SharedStatic<ComponentTypeData> with lazy registration |
src/Components/Component.cs |
Core interfaces: IComponent, IArrayComponent, IPoolComponent, IReactive, Changed<T>, Reactive<T>, Name, DestroyEntity, EntityCreated, IsPrefab, ChildOf |
src/Components/GenericPool.cs |
GenericPool for Pool-stored components (SparseSet-based) |
src/Components/DynamicBuffer.cs |
DynamicBuffer<T> — Unity-style dynamic buffer component |
src/Components/ComponentArray.cs |
ComponentArray<T> — array-as-component type |
src/Components/ComponentData.cs |
Serialization helper (byte[] representation of components) |
src/Components/UnsafeStatic.cs |
UnsafeStatic utility (memcpy, as_ref, etc.) |
src/Components/DisposeRegistryStatic.cs |
Dispose tracking for unmanaged resources |
src/Components/TypeExtensions.cs |
Type reflection extensions |
src/Components/GeneratedComponentList.cs |
Auto-generated component registration list |
| File | Description |
|---|---|
src/Systems/Systems.cs |
Systems container; lifecycle lists (onStart/onUpdate/onFixedUpdate/onDestroy); ISystemRunner support; Add<T>() overloads |
src/Systems/State.cs |
State struct (World, TimeData, Dependencies) — system execution context, implements ISystemParam |
src/Systems/WorldSystems.cs |
Static registry mapping world IDs to Systems instances |
src/Systems/_systems_internal.cs |
Internal helper for routing system runners to correct lifecycle list |
src/Systems/Marker.cs |
Marker struct wrapping Unity ProfilerMarker |
src/Systems/TimeData.cs |
TimeData struct (DeltaTime, Time, etc.) |
src/Systems/EntityJobSystem.cs |
IEntityJobSystem interface + EntityJobSystemRunner<T> runner |
src/Systems/EntityDestroySystem.cs |
Built-in entity destruction system |
src/Systems/ECBJob.cs |
ECB processing job |
src/Systems/StartFixedECBSystem.cs |
Start/fixed-update ECB processing |
src/Systems/JobSystem.cs |
Job system base |
src/Systems/IQueryJobSystem.cs |
Query-based job system interface |
src/Systems/BuiltInSystems.cs |
Built-in system registrations |
| File | Description |
|---|---|
src/Systems/FnSystems/Query.cs |
Generic query iterators Query<T1..T5, TOption> with Query<Entity, ...> for entity+components |
src/Systems/FnSystems/QueryGeneric.cs |
Generic query job system runners |
src/Systems/FnSystems/QueryIterators.cs |
Query iterator implementations |
src/Systems/FnSystems/QueryIteratorsParallel.cs |
Parallel query iterator implementations |
src/Systems/FnSystems/Chunk.cs |
Chunk<T1..T8> archetype chunk iterators + IChunk interface |
src/Systems/FnSystems/Events.cs |
Events<TEvent> system param, EventsParallelReader<TEvent>, EventsStorage |
src/Systems/FnSystems/Res.cs |
Res<TRes> (resource param), SaveRes<TRes>, TimeRes |
src/Systems/FnSystems/ResManaged.cs |
ResManaged<TRes> for class-type resources |
src/Systems/FnSystems/ResStorage.cs |
ResStorage unmanaged resource storage |
src/Systems/FnSystems/Single.cs |
Single<T1> (singleton entity accessor), MutRes<TRes>, Local<TData>, IRes, IResourceGetSet |
src/Systems/FnSystems/ManagedResRef.cs |
ManagedResRef<T> (GCHandle-like managed reference wrapper) |
| File | Description |
|---|---|
src/Systems/FnSystems/Tuples/RefTuple.cs |
Ref-based query tuples |
src/Systems/FnSystems/Tuples/PtrTuple.cs |
Pointer-based query tuples |
src/Systems/FnSystems/Tuples/EntityRefTuple.cs |
Entity+ref query tuples |
src/Systems/FnSystems/Tuples/EntityPtrTuple.cs |
Entity+ptr query tuples |
src/Systems/FnSystems/Tuples/ObjectTuple.cs |
Object-based query tuples |
| File | Description |
|---|---|
src/Systems/Runners/SystemMainThreadRunnerStruct.cs |
Runner for struct ISystem (main thread, Burst-friendly) |
src/Systems/Runners/SystemMainThreadRunnerClass.cs |
Runner for class ISystem (managed) |
src/Systems/Runners/SystemDestroyer.cs |
SystemDestroyer<T> for IOnDestroy unmanaged systems |
src/Systems/Runners/SystemClassDestroyer.cs |
Destroyer for class-type systems |
| File | Description |
|---|---|
src/Systems/HotReload/HotReloadSystems.cs |
HotReloadSystems class: wraps Systems, tracks source files, swaps runners on recompile |
| File | Description |
|---|---|
src/Reactive/ReactiveCheckSystem.cs |
ReactiveCheckSystem<T> detects component changes via memcmp; AddReactive<T>() extension |
src/Reactive/ReactAndClearSystem.cs |
Clears Changed<T> tags and fires callbacks |
src/Reactive/ComponentChangeEvent.cs |
Static event system for component change notifications |
src/Reactive/ReactDelegate.cs |
ReactDelegate<T> delegate type |
| File | Description |
|---|---|
src/Collections/MemoryArray.cs |
MemoryArray<T> — unmanaged resizable array via allocator |
src/Collections/MemoryList.cs |
MemoryList<T> — unmanaged list via allocator |
src/Collections/DynamicBitmask.cs |
Bitmask for archetype component matching |
src/Collections/HashMap.cs |
Custom hash map |
src/Collections/AliveEntitiesSet.cs |
Sparse-set based alive entity tracking |
src/Collections/Bitmask1024.cs |
Bitmask for 1024 elements |
src/Collections/Bitmask4096.cs |
Bitmask for 4096 elements |
src/Collections/BitMap1024.cs |
Fast hashmap for 1024 elements |
src/Collections/MultiArray.cs |
MultiArray collection |
| File | Description |
|---|---|
src/Allocator/Allocator.cs |
MemAllocator — arena allocator |
src/Allocator/ptr.cs |
ptr<T> — safe pointer wrapper |
src/Allocator/Serialization.cs |
Allocator serialization support |
src/Allocator/Spinner.cs |
Spinlock implementation (copy of Unity internal Spinner) |
| File | Description |
|---|---|
src/dbug.cs |
Debug logging utility |
src/NUnsafe.cs |
Additional unsafe utilities |
src/Singleton.cs |
StructSingleton<T> and Singleton<T> implementations |
src/SparseSet.cs |
Sparse set data structure |
src/StaticAllocations.cs |
Static allocation helpers |
src/SystemsGroup.cs |
SystemsGroup — named group of system runners |
src/rng.cs |
Random number generation utilities |
src/EntityFilterBuffer.cs |
Entity filtering buffer |
src/QueryFilter.cs |
Query filtering logic |
src/Usings.cs |
Global using directives |
| File | Description |
|---|---|
src/Unity/WorldInstaller.cs |
World lifecycle management MonoBehaviour |
src/Unity/WorldBaker.cs |
Baker for sub-scene conversion |
src/Unity/EntityBaker.cs |
Entity prefab baking |
src/Unity/EntityPrefabMap.cs |
Prefab → entity mapping |
src/Unity/SyncTransformsSystem.cs |
Transform synchronization system |
src/Unity/Transforms/Transform.cs |
Transform component |
src/Unity/Transforms/LocalTransform.cs |
Local transform component |
src/Unity/Transforms/TransformChildSystem.cs |
Child transform system |
src/Unity/Transforms/TransformsGroup.cs |
Transforms system group |
src/Unity/Transforms/UpdateTransformOnAddChildSystem.cs |
Transform update on child add |
src/Unity/Utils/Reflect.cs |
Reflection utilities |
src/Unity/Resoursers/EntityBlueprintEditor.cs |
Entity blueprint editor (UIElements) |
- Archetype data: SoA layout —
componentOffsets[i]= byte offset to start of componenti's row array; each row =componentSize * capacityapart - Entity location:
World.entityLocations[entityID] = { archetypeIndex, row } - Packed entities:
ArchetypeUnsafe.packedEntities[row] = entityID - Query iteration:
QueryEnumeratorwalksmatchingArchetypes[]→ archetype →packedEntities[row] - Component access:
data.Ptr + componentOffset + row * componentSize - Two component storage types:
StorageType.Default(inline in archetype) andStorageType.Pool(separate pool) TOptionin queries: can beNone<T>,Any<T>,With<T>, or a regular component- Component type index:
ComponentType<T>.Index— per-typeSharedStatic<ComponentTypeData>with lazy registration - Alive entity set:
AliveEntitiesSet(SparseSet-based) tracks living entity IDs - Events range:
Events<TEvent>.Rangeset per-thread for parallel iteration;RangeEnumeratorwalksstart..end
BatchCreateEntitymust callEnsureCapacity, fillpackedEntities, setentityLocations.row, memclear component data, incrementcountTOptIsComponentstatic field inQuery<T1, TOption>was stale — useQueryParamInfo<TOption>.IsComponentinsteadQueryEnumeratorneeds_lastArch < 0guard on firstMoveNextto avoid null derefSetupTNmethods needli < 0guard before.SetArchetype()callsMoveNextin generic queries needs_archIdx >= matchingArchetypes.lengthbounds checkUpdate()in allQuery<T1..TN, TOption>and.WithEntityvariants: the archetype row loop must use the snapshot ofcounttaken before iteration (not re-read from archetype each tick), otherwise entities added during iteration cause OOB access
- Unity Edit mode tests in
UnitTests/ - Key test files:
SystemChainTests.cs,WorldTests.cs,AdvancedTests.cs,EventsTests.cs,SerializationTests.cs,PrefabSpawnTests.cs,ResizeTests.cs,AllocatorTests.cs,AddObjectTests.cs,SimpleTest.cs - Tests run via Unity Editor (not
dotnet test) — results inUnitTests/TestResults_*.xml - SystemChainTests: 14 tests covering system chains (3+ systems in sequence), thread modes, batch creation
- EventsTests: tests for
Events<TEvent>add/read/clear and parallel safety
- Located at
../../SourseGen/NUKECSGEN/(note:ComponentTypeGenerator .cshas a space before.cs) - Generates system runners (
ISystemRunner), query system job runners, and component type registrations - Generated output in
NUKECSGEN/directory at solution root [System]attribute marks static methods as ECS systems; source gen creates runner classes
For each [System] method, the generator produces:
IXxxSystemJobinterface withOnUpdate+OnUpdateBatchedIXxxSystemJobExtensionsstatic class withQuerySystemJobWrapper<TJob>struct (Execute dispatch)IXxxQuerySystemJobRunner<TJob>runner class (Schedule/Run for Main/Single/Parallel modes)XxxJobstruct implementingIXxxSystemJob(copies user's method body)
OnUpdateBatched is always void and always has ref State state in its parameters (added automatically if the user's OnUpdate doesn't have it). This guarantees state.World.UnsafeWorld is available for the batch archetype loop path.
Fallback is internal — call sites (Execute Single, Runner Main) just call OnUpdateBatched(...) directly, no bool checks.
Three generated body variants:
- Batchable + hasQuery — storage check; if not all archetypes, fallback to
Update+OnUpdate+return; else archetype pointer loop (rewritten foreach) - Non-batchable + hasQuery —
Update+OnUpdate(regular foreach path) - No query —
OnUpdateonly
hasStateParam— whether user'sOnUpdatehas aStateparameteronUpdateCallArgs— args for callingOnUpdatefrom insideOnUpdateBatched(e.g.,ref q, ref cp) — nostateprefix, nofullData.prefixonUpdateBatchedParams— method signature params forOnUpdateBatched; appends, ref State stateif!hasStateParamonUpdateBatchedCallExecute/onUpdateBatchedCallRunner— call-site args that always include state
When a system's body is a single foreach (var (a, b) in query) with all components inline (StorageType.Archetype), the generator rewrites it to a direct archetype pointer loop bypassing QueryEnumerator. ForeachBodyRewriter replaces a.Val / a.Get with _p0[_i] array access.
All system parameters implement ISystemParam with Init(ref ptr<World.WorldUnsafe>), Update(ref World, IntPtr), and MetaType property. The source generator auto-recognizes these types in [System] method signatures.
| Param | MetaType | Description |
|---|---|---|
State |
State |
Execution context: World, TimeData, Dependencies |
Query<T1..TN, TOption> |
Query |
Component query with foreach iteration |
Res<TRes> |
Resource |
Read/write access to unmanaged singleton resource |
ResManaged<TRes> |
Resource |
Read/write access to managed (class) singleton resource |
Events<TEvent> |
Events |
Thread-safe event buffer; AddPar for parallel writes |
Single<T1> |
Single |
Access singleton entity with component T1 |
Local<TData> |
Local |
Per-system local data |
Chunk<T1..T8> |
— | Direct archetype chunk pointer iteration |
Events<TEvent>—ISystemParambacked byMemoryList<TEvent>viaMemAllocator- Thread-safe writes:
AddPar(in TEvent)acquiresSpinnerspinlock before list push; auto-resizes under lock - Parallel reads:
ReadPar()returnsEventsParallelReader<TEvent>(readonly ptr + length); per-threadRangeset byUpdate(ref World, IntPtr data)wheredatapoints to aRangestruct - Iteration:
RangeEnumeratorwalksRange.start..Range.endof the event list - Storage:
EventsStorage—HashMap<int, ptr>keyed by type hash;Get<TEvents>(ref ptr<World.WorldUnsafe>)lazily creates on first access;ClearAll()iterates all event types
Res<TRes>whereTRes : struct, IRes— wrapsStructSingleton<TRes>(static storage);IReshasOnCreate(ref ptr<World>)andOnUpdate(ref World)ResManaged<TRes>— for class-type resources; usesManagedResRef<T>(GCHandle-like wrapper)IResourceGetSet— boxing/unboxing interface for reflection-based access (used by debug tools)ResStorage— unmanaged storage registry for resourcesSaveRes<TRes>— save-only resource accessor (no auto-update)
Chunk<T1..T8>— direct archetype chunk iterators implementingIChunkSetData(ref ArchetypeUnsafe)— resolves component pointers viaGetComponentLocalIndex+GetComponentOffset- Iterator pattern:
_remainingcountdown; eachMoveNext()decrements and advances all component pointers - Access via
C0..C7properties (ref returns) orGet()for single-type chunks CopyTo<TU>(TU* dest, int len)— selective memcpy by matching type index
IReactive— marker interface on components that should be tracked for changesReactive<T>— storesoldValueof type T; added alongside reactive componentChanged<T>— tag component added byReactiveCheckSystem<T>when value differs from stored old valueReactiveCheckSystem<T> : IEntityJobSystem— per-entity memcmp between current andReactive<T>.oldValue; addsChanged<T>on mismatchReactAndClearSystem<T>— clearsChanged<T>tags and fires registered callbacks- Registration:
systems.AddReactive<T>()adds both check and clear systems
HotReloadSystems— wrapsSystems; tracks source files of registered system runners- Flow:
StartTracking()→TrackRunnerList()resolves[System]methods →HotReloadWatcher.Watch(path)monitors files → on changeHotReloadCompiler(Roslyn) recompiles →OnSystemsCompiledcallback swaps runners in-place SystemsHotReloadExtensions.AddHotReload(this Systems)— convenience extension; registersonWorldDisposecleanup- Editor-only (
#if UNITY_EDITOR);HotReloadCompiler.PrewarmCache()called in constructor - Source files tracked in
SystemEntrystruct (filePath, methodName, declaringTypeName, threadMode, runnerIndex)
- World creation:
World.Static.cs—ALLOCATORstruct provides domain/per-world allocators viaSharedStatic; world instances stored inSharedStaticarray - Lifecycle lists (in
Systems):onStart,onUpdate,onFixedUpdate,onDestroy—List<ISystemRunner>per phase - Disposal:
World.Free.cs—WorldUnsafe.Free()releases all allocator memory;SystemsonWorldDisposecallback fires - StoryLog:
World.StoryLog.cs— debug ring-buffer recording component add/remove/set operations (behind#if NUKECS_DEBUG) - Deserialization hook:
IOnWorldDeserializeinterface for post-deserialization fixup
- Uses
[BurstCompile]on hot paths — avoid managed allocations in Burst-compiled code MemAllocatoris a custom arena allocator, not Unity'sAllocator- Jobs use
IJobParallelForandIJobfrom Unity.Jobs UnityAllocatorHandler.cs/UnityAllocatorWrapper.csbridge to Unity's allocator for specific use casesWorld.SerializeAndSave.cshandles world serializationWorld.Aspects.csprovides aspect (group-of-components) supportEntityFilterBuffer.csandQueryFilter.cshandle entity filteringsrc/Unity/Transforms/— Transform hierarchy:Transform,LocalTransformcomponents + child/parent systemssrc/Unity/Utils/Reflect.cs— Reflection utilities for editor toolingsrc/Unity/Editor/HotReload/— Editor-side hot reload:HotReloadCompiler,HotReloadRoslynCompiler,HotReloadWatchersrc/Unity/Editor/EcsDebugV2/— Cyberpunk-styled data-driven debugger (seeecs-debug-v2skill)src/Unity/Editor/EcsDashboard/— Dashboard debugger (seeecs-dashboardskill)src/Unity/Editor/World/— Legacy debug windows (ECSDebugWindow, ECSMemoryProfilerWindow, etc.)src/Unity/Editor/Allocator/— Allocator debug windows (separateAllocatorEditor.asmdef)Demos/— Example projects:RotateCubeDemo,BoidsDemo,CubeSculptureDemo
using Wargon.Nukecs;
using Wargon.Nukecs.Transforms;
using Wargon.Nukecs.HotReload;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Collections;
using UnityEngine;// Data component
public struct Health : IComponent
{
public float Value;
}
// Tag component (no data)
public struct EnemyTag : IComponent { }
// Component with IDisposable (cleanup on entity destroy)
public struct GameObjectView : IComponent, IDisposable
{
public ObjectRef<GameObject> val;
public void Dispose()
{
if (val.IsValid() && val != null)
{
Object.Destroy(val.Value);
val.Dispose();
}
}
}
// Built-in tag components: DestroyEntity, EntityCreated, IsPrefab, ChildOf, Name
// Interfaces: IComponent, IArrayComponent, IPoolComponent, IReactive// Unmanaged resource (struct IRes)
public struct ConfigData : IRes
{
public int TargetCount;
public float CubeScale;
public void OnCreate(ref World world) { }
public void OnUpdate(ref World world) { }
}
// Managed resource (class IRes)
public class MeshData : IRes
{
public Mesh Mesh;
public Material Material;
public void OnCreate(ref World world) { }
public void OnUpdate(ref World world) { }
}// Thread modes: Main, MainRun, Single, Parallel (default)
public class GameSystems
{
// Query + State
[System, BurstCompile]
public static void MovementSystem(
ref Query<Position, Velocity> query,
ref State state)
{
var dt = state.Time.DeltaTime;
foreach (var (pos, vel) in query)
{
pos.Get.X += vel.Read.X * dt;
pos.Get.Y += vel.Read.Y * dt;
}
}
// Unsafe pointer iteration (for Burst)
[System, BurstCompile]
public static unsafe void FastMovement(
ref Query<Position, Velocity, With<EnemyTag>> query,
ref State state)
{
var dt = state.Time.DeltaTime;
foreach (var (pos, vel) in query.iter_unsafe())
{
pos->X += vel->X * dt;
pos->Y += vel->Y * dt;
}
}
// Parallel unsafe iteration
[System, BurstCompile]
public static unsafe void ParallelPhysics(
ref Query<Position, Velocity> query,
ref State state)
{
var dt = state.Time.DeltaTime;
foreach (var (pos, vel) in query.par_iter_unsafe())
{
pos->X += vel->X * dt;
}
}
// Query + Res + State
[System, BurstCompile]
public static void SpawnSystem(
ref State state,
ref Res<ConfigData> config)
{
var count = config.Ref.TargetCount;
config.Ref.timer -= state.Time.DeltaTime;
}
// Query + multiple Res + State
[System, BurstCompile]
public static void AISystem(
ref Query<Position, EnemyTag> query,
ref State state,
ref Res<ConfigData> config,
ref Res<GameWorldData> worldData)
{
}
// With Entity (get entity + components) — Entity is always first type param
[System]
public static void DamageSystem(
ref Query<Entity, Health> query,
ref State state,
ref Events<DamageEvent> events)
{
foreach (var (e, hp) in query)
{
if (hp.Read.Value <= 0)
e.Destroy();
}
}
// None<T> filter (exclude entities with component) + Entity
[System]
public static void AddVelocitySystem(
ref Query<Entity, Position, None<Velocity>> query)
{
foreach (var (e, _) in query)
{
e.Add(new Velocity { X = 0, Y = 0 });
}
}
// State only (no query)
[System, BurstCompile]
public static void TimerSystem(ref State state)
{
}
// ResManaged<T> for class resources
[System]
public static unsafe void RenderSystem(
ref Query<LocalTransform, EnemyTag> query,
ref ResManaged<MeshData> meshData)
{
var param = new RenderParams(meshData.Val.Material);
}
// 5 components + TOption
[System, BurstCompile]
public static void ComplexSystem(
ref Query<Position, Velocity, Health, EnemyTag, Weapon> query,
ref State state)
{
}
}[BurstCompile]
public struct RotateSystem : IEntityJobSystem
{
public Threads Mode => Threads.Parallel;
public Query GetQuery(ref World world)
{
return world.Query().With<Transform>().With<RotationSpeed>();
}
public void OnUpdate(ref Entity entity, ref State state)
{
ref var transform = ref entity.Get<Transform>();
ref var speed = ref entity.Get<RotationSpeed>();
transform.Rotation = math.mul(
transform.Rotation,
quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * state.Time.DeltaTime)
);
}
}public struct CustomSystem : ISystem, IOnCreate
{
private Query query;
public void OnCreate(ref World world)
{
query = world.Query().With<Position>().With<Velocity>();
}
public void OnUpdate(ref State state)
{
foreach (ref var e in query)
{
ref var pos = ref e.Get<Position>();
ref var vel = ref e.Get<Velocity>();
pos.X += vel.X * state.Time.DeltaTime;
}
}
}// Create world
var world = World.Create(WorldConfig.Default256);
// Create systems
var systems = new Systems(ref world)
.AddDefaults()
.Add(GameSystems.MovementSystem, Threads.Main)
.Add(GameSystems.FastMovement)
.Add(GameSystems.SpawnSystem, Threads.MainRun)
.Add<RotateSystem>() // IEntityJobSystem struct
.AddGroup(new TransformsGroup()); // ISystemsGroup
// Register resources
world.AddRes(new ConfigData { TargetCount = 100 });
world.AddResManaged(new MeshData { Mesh = myMesh, Material = myMat });
// Game loop
void Update()
{
systems.OnUpdate(Time.deltaTime, Time.time);
}
// Cleanup
void OnDestroy()
{
world.Dispose();
}WorldConfig.Default16 // 16 entities
WorldConfig.Default // 64 entities
WorldConfig.Default256 // 256 entities
WorldConfig.Default1024 // 1024 entities
WorldConfig.Default6144
WorldConfig.Default16384
WorldConfig.Default65536
WorldConfig.Default163840
WorldConfig.Default256000
WorldConfig.Default_1_000_000// Empty entity
ref var entity = ref world.Entity();
// With components
var e = world.Entity(new Health { Value = 100 }, new Position { X = 0, Y = 0 });
// Create then add (deferred via ECB)
ref var e = ref world.Entity();
e.Add(new Health { Value = 100 });
e.Add<EnemyTag>();
e.Add(new Name("Player"));
// Batch creation
var entities = world.BatchCreateEntity(count);
for (int i = 0; i < entities.Length; i++)
{
ref var e = ref entities[i];
e.Add(new Position { X = i * 1.5f });
e.Add<Velocity>();
}
// From archetype
var arch = world.GetArchetype(typeof(Position), typeof(Velocity));
var e = arch.CreateEntity();
e.Get<Position>().X = 5;
// Batch from archetype
var entities = arch.BatchCreateEntity(count);ref var hp = ref entity.Get<Health>(); // Read
entity.Set(new Health { Value = 75 }); // Write
bool hasHp = entity.Has<Health>(); // Check
entity.Add(new Health { Value = 10 }); // Add (deferred via ECB)
entity.Add<TagComponent>(); // Add tag (deferred)
entity.Remove<Health>(); // Remove (deferred via ECB)
ref var pos = ref entity.TryGet<Position>(out bool exist); // TryGet
entity.Destroy(); // Deferred destroy
entity.DestroyNow(); // Immediate destroy// Build queries via fluent API
var q1 = world.Query().With<Health>();
var q2 = world.Query().With<Health>().With<Velocity>();
var q3 = world.Query().With<Health>().None<EnemyTag>();
int count = q1.Count;
bool empty = q1.IsEmpty;
Entity first = q1.First();
// Iterate non-generic query
foreach (ref var entity in q1)
{
ref var hp = ref entity.Get<Health>();
}// Ref-based (safe)
foreach (var (pos, vel) in query)
{
pos.Get.X += vel.Read.X;
}
// Pointer-based (unsafe, Burst-friendly)
foreach (var (pos, vel) in query.iter_unsafe())
{
pos->X += vel->X;
}
// Parallel ref
foreach (var (pos, vel) in query.par_iter())
{
pos.Get.X += vel.Read.X;
}
// Parallel pointer
foreach (var (pos, vel) in query.par_iter_unsafe())
{
pos->X += vel->X;
}// Define event (plain struct, no interface)
public struct DamageEvent
{
public int EntityId;
public float Amount;
}
// Produce events (single-thread)
[System]
public static void ProduceDamage(
ref Query<Entity, Health> query,
ref Events<DamageEvent> events)
{
foreach (var (e, hp) in query)
{
events.Add(new DamageEvent { EntityId = e.id, Amount = 10 });
}
}
// Produce events (parallel-safe)
[System]
public static void ProduceDamageParallel(
ref Query<Entity, Health> query,
ref Events<DamageEvent> events)
{
foreach (var (e, hp) in query)
{
events.AddPar(new DamageEvent { EntityId = e.id, Amount = 10 });
}
}
// Consume events
[System]
public static void ApplyDamage(
ref State state,
ref Events<DamageEvent> events)
{
foreach (ref var ev in events)
{
var e = state.World.GetEntity(ev.EntityId);
ref var hp = ref e.Get<Health>();
hp.Value -= ev.Amount;
}
}
// Consume events via parallel reader
var reader = events.ReadPar();
for (int i = 0; i < reader.Length; i++)
{
ref var ev = ref reader[i];
}[BurstCompile]
public class GameSystemsGroup : ISystemsGroup
{
public void Build(Systems systems, ref World world)
{
systems
.Add(MovementSystem, Threads.Main)
.Add(FastPhysics)
.Add(SpawnSystem, Threads.MainRun)
.Add(RenderSystem, Threads.Main);
}
[System, BurstCompile]
public static void MovementSystem(/* ... */) { }
[System, BurstCompile]
public static void FastPhysics(/* ... */) { }
}
// Registration:
systems.AddGroup(new GameSystemsGroup());public class GameBootstrap : WorldInstaller
{
[SerializeField] int entityCount = 100;
protected override WorldConfig GetConfig() => WorldConfig.Default256;
protected override void OnWorldCreated(ref World world)
{
world.AddRes(new ConfigData { TargetCount = entityCount });
Systems.AddGroup(new GameSystemsGroup());
}
protected override void CreateEntities(ref World world)
{
var entities = world.BatchCreateEntity(entityCount);
for (int i = 0; i < entities.Length; i++)
{
entities[i].Add(new Position { X = i });
entities[i].Add<Velocity>();
}
}
void Update()
{
Systems.OnUpdate(Time.deltaTime, Time.time);
}
}// Function-based (source-generated runner)
systems.Add(ClassName.MethodName, Threads.Main);
systems.Add(ClassName.MethodName); // Default = Parallel
// IEntityJobSystem struct
systems.Add<MyJobSystem>();
systems.Add<MyJobSystem>(Threads.Parallel);
// ISystem struct
systems.Add<MyStructSystem>();
// ISystem class (managed)
systems.Add<MyClassSystem>();
// ISystemsGroup
systems.AddGroup(new MySystemsGroup());
// Batch with tuples
systems.AddSystems(SystemPath.Update,
Method1, // default Parallel
(Method2, Threads.Main),
(Method3, Threads.MainRun));
// Built-in defaults (entity destroy, clear events)
systems.AddDefaults();// Component changes via Add/Remove/Destroy are DEFERRED (queued in ECB)
var e = world.Entity();
e.Add(new Health { Value = 10 });
e.Has<Health>(); // FALSE — not visible yet
query.Count; // 0 — query doesn't match yet
world.Update(); // <-- ECB playback happens here
e.Has<Health>(); // TRUE
query.Count; // 1Entity API (Get, Set, Add, Remove, Has, Destroy, DestroyNow) works in ALL thread modes including Threads.Parallel and Burst-compiled systems. Only Unity managed API (GameObject, Camera, Debug.Log, UnityEngine.Object.Destroy, UnityEngine.Transform) requires Threads.Main.
| Mode | Entity API | Unity API | Burst |
|---|---|---|---|
Threads.Main |
Yes | Yes | No |
Threads.MainRun |
Yes | No | Yes |
Threads.Parallel |
Yes | No | Yes |
Threads.Single |
Yes | No | Yes |
A [System] method can accept multiple Query<> parameters. This is useful for cross-query lookups (e.g., collision: projectiles vs enemies).
[System]
public static void CollisionSystem(
ref Query<Entity, Transform, Projectile, With<ProjectileTag>> projectiles,
ref Query<Entity, Transform, Health, With<EnemyTag>> enemies,
ref State state,
ref Events<DamageEvent> damageEvents)| Operation | Timing | Visible same frame? |
|---|---|---|
entity.Get<T>() / entity.Set() |
Immediate | Yes |
Events<T>.Add() / .Clear() |
Immediate | Yes |
Res<T>.Ref |
Immediate (static) | Yes |
entity.Add<T>() |
Deferred (ECB) | No — next frame |
entity.Remove<T>() |
Deferred (ECB) | No — next frame |
entity.Destroy() |
Deferred (ECB) | No — next frame |
Pattern for same-frame death events: When an entity dies, use sentinel values instead of relying on deferred DeadTag:
// DON'T: relies on deferred DeadTag being visible same frame
if (hp.Current <= 0)
target.Add(new DeadTag()); // deferred — won't be visible until next frame
// DO: use sentinel + immediate event
if (hp.Current <= 0)
{
deathEvents.Add(new DeathEvent { XPReward = xp }); // immediate
hp.Current = -999f; // sentinel — visible immediately
target.Add(new DeadTag()); // for next-frame cleanup
}Events<T> are immediate buffers that persist across frames until explicitly cleared. The consuming system MUST call .Clear(). Otherwise events accumulate and are re-processed every frame.
[System]
public static void XPSystem(
ref Events<DeathEvent> deathEvents,
...)
{
foreach (ref var ev in deathEvents) { /* process */ }
deathEvents.Clear(); // REQUIRED — or events accumulate forever
}Guard pattern for systems that reset state — check Count before resetting:
// DON'T: unconditionally resets every frame
upgradeState.Ref.SelectionPending = false;
gameState.Ref.Value = GameStateType.Playing;
// DO: only reset when events are present
if (upgradeEvents.Count == 0) return;
// ... process events ...
upgradeState.Ref.SelectionPending = false;[BurstCompile]on a class +using UnityEngine;+ any managed API call = silent compilation failure. The source generator won't produce runners, and ALL systems in that class disappear with a cryptic "does not contain a definition" error.- Fix: Remove
[BurstCompile]from the class, or remove all managed API usage. - C# version is 9.0 — no struct field initializers, no parameterless struct constructors.
Res<T>whereT : struct, IRes— unmanaged, static storage. Access vianew Res<T>().Refworks anywhere.ResManaged<T>whereT : class, IRes— for resources containing managed types (arrays, GameObjects, etc.). Registered viaworld.AddResManaged().- Resources with managed types (arrays, lists) must be classes registered with
AddManaged. Res<T>/ResManaged<T>should only be accessed inside[System]methods or fromWorldcontext. Don't access from arbitrary MonoBehaviours — use static fields on the system class instead.
world.Query().With<T>() returns a Query struct that is internally cached by component mask. Store the query once in OnCreate / Start and reuse:
private Query enemies;
void Start()
{
enemies = world.Query().With<Transform>().With<Health>().With<EnemyTag>();
}Creating the same query repeatedly each frame is wasteful. The world.Query().With<T>() chain registers the query in the world's internal storage by component mask, so repeated calls with the same mask return the same registered query — but still incurs lookup overhead.
To pass data from ECS systems to Unity MonoBehaviour components (UI rendering, etc.), use static fields on the system class:
public class HealthBarSystems
{
public static int Count;
public static readonly HealthBarEntry[] Entries = new HealthBarEntry[64];
[System]
public static void CollectHealthBars(ref Query<...> query)
{
Count = 0;
// fill Entries...
}
}
// MonoBehaviour reads static data
public class HealthBarRenderer : MonoBehaviour
{
void OnGUI()
{
for (int i = 0; i < HealthBarSystems.Count; i++) { /* draw */ }
}
}In WorldInstaller.Update(), call Systems.OnUpdate() before reading world data for UI:
void Update()
{
Systems.OnUpdate(Time.deltaTime, Time.time); // systems run first
ReadWorldData(); // then read updated state
UpdateUI(); // display current data
}To pause, gate Systems.OnUpdate() on game state:
void Update()
{
if (new Res<GameState>().Ref.Value == GameStateType.Playing)
Systems.OnUpdate(Time.deltaTime, Time.time);
}For clickable UI buttons (Canvas-based), the scene needs:
EventSystemcomponentInputSystemUIInputModule(if using Unity Input System)GraphicRaycasteron the Canvas
Without these, buttons render but don't respond to clicks.