Skip to main content

Entities and components

Under the editor, every scene is an ECS world. An entity is only an id, components are plain data attached to it, and systems are the code that runs over many entities at once. This page explains how the world stores that data, how you read and change it from scripts and systems, and how to declare components of your own.

Entities, components and the world​

A World owns the entities of one scene. Each scene gets its own world, created in the scene's service scope, so nothing carries over from one scene to the next.

ConceptWhat it is in code
EntityEntity, a record struct of an Id and a Version. It holds no data. When an entity is destroyed its id is reused with a higher version, so an old handle never points at the new entity. Entity.Null never refers to anything.
ComponentAny struct (or class) stored on an entity: Transform, Sprite, Collider2D, your own Health. An entity has at most one component of each type.
WorldThe container: Create, Destroy, Set, Get, Has, Remove, IsAlive and Query. A world is not thread-safe; it belongs to the game thread.
ArchetypeThe set of component types an entity has. All entities with exactly the same set share one archetype.

A script reaches its scene's world through World, and its own entity through Entity. The entity you select in the hierarchy is the same kind of id; its name is a Name component and its place in the tree is a Parent component.

How the world stores components​

Entities with the same component types are stored together. Each archetype is a table: one row per entity and one contiguous array per component type. Adding or removing a component moves the entity's row to another table.

This layout is why the ECS is fast for many similar entities. A query visits only the archetypes that match, and within one archetype it walks arrays in order, so the CPU reads memory sequentially and nothing is looked up per entity. It is also why changing the shape of an entity costs more than changing a value: moving a row copies every component of that entity.

Reading and changing components​

World.Get<T>(entity) and a script's GetComponent<T>() return a reference into the archetype's array. Change fields through a ref local and the stored component changes. Assign the result to a plain var and you get a copy, and changes to the copy are lost.

ref var health = ref GetComponent<Health>();
health.Current -= 1; // changes the stored component

var copy = GetComponent<Health>();
copy.Current = 0; // changes only the local copy
CallUse it for
World.Get<T>(entity)A reference to an existing component; throws when the entity has none
World.TryGet<T>(entity, out var value)A copy, when the component may be missing
World.TryGetRef<T>(entity, out var exists)A reference that may be null; check exists first
World.Has<T>(entity)Whether the entity has the component
World.Set(entity, value)Adds the component, or replaces it
World.Remove<T>(entity)Removes the component; returns false when there was none
World.Create(c1, c2, …)A new entity with up to four components, placed straight into its final archetype
World.Destroy(entity), world.DestroyWithChildren(entity)Removes an entity, or an entity and all its descendants

Do not keep a ref across a structural change. Adding or removing any component of that entity moves its row, and the reference then points at stale memory. Take the reference again after Set or Remove.

Struct and class components​

Most components are structs, so they live inline in the archetype's arrays. A few are classes: ParticleEmitter, TileMapComponent, MapObjectComponent, TriggerArea and ScriptComponent. For those the array holds a reference to the object, so GetComponent<ParticleEmitter>().Emit(20) calls a method on the shared object and any copy of the reference sees the same emitter. Use a class only for data that is large, shared or has its own behavior. Every entity needs its own instance; never put the same class instance on two entities.

Queries​

World.Query<T1, …, T4>() returns a Query for entities that have all of those components. Queries are cached by their description and pick up new archetypes automatically, so calling Query<Transform, Health>() every frame costs a dictionary lookup, not a scan. For "with this but without that", build a QueryDescription:

var alive = World.Query(QueryDescription.With<Health>().Without<Inactive>());
var drawn = QueryDescription.With<Transform>().WithAny<Sprite>().WithAny<ParticleEmitter>();

There are three ways to walk a query, from most convenient to fastest:

MethodHow it worksWhen to use it
query.ForEach((Entity e, ref Transform t, ref Health h) => …)Calls a delegate per entity with references to its componentsShort loops where clarity matters more than speed
foreach (var archetype in query) with archetype.GetSpan<T>()You loop over the spans of each archetype yourselfMost systems; no delegate call per entity and no allocation
query.Run<TJob, T1, …>(ref job) with a struct implementing IForEach<…>The JIT inlines the job's Execute into the loopHot loops over thousands of entities

A lambda that captures variables allocates a closure each time it is created, which is why systems that run every frame prefer spans or struct jobs. See Systems for each form in a complete system.

query.Count, query.IsEmpty and query.TryGetSingle(out var entity) answer the common questions without a loop. TryGetSingle is how systems find "the player" or "the camera" when there is exactly one.

Structural changes and the command buffer​

Creating or destroying entities and adding or removing components are structural changes. They are not allowed while a query is iterating, because they would move rows inside the arrays being walked. World.IsIterating tells you whether a query is running.

Inside a system, record changes in context.Commands, a CommandBuffer. It plays back right after your system returns. Entities created through it are reserved at once, so you can store the handle and give it components in the same frame:

assets/scripts/Health.cs
namespace MyGame;

/// <summary>Hit points of anything that can be hurt.</summary>
[Component(Category = "Gameplay", Icon = "heart")]
public struct Health
{
[Range(0, 100)]
public int Current;

[Range(1, 100)]
public int Maximum;

[Tooltip("Hit points regained per second.")]
public float Regeneration;

[Transient]
public float Pending;
}

/// <summary>Regenerates health and removes entities whose health ran out.</summary>
[UpdateIn(SystemPhase.Update)]
public sealed class HealthSystem : ISystem
{
public void Update(in SystemContext context)
{
var delta = context.Time.DeltaTime;
foreach (var archetype in context.World.Query<Health>())
{
var healths = archetype.GetSpan<Health>();
var entities = archetype.Entities;
for (var i = 0; i < healths.Length; i++)
{
ref var health = ref healths[i];
health.Pending += health.Regeneration * delta;
if (health.Pending >= 1)
{
health.Current = Math.Min(health.Maximum, health.Current + 1);
health.Pending -= 1;
}

if (health.Current <= 0)
context.Commands.Destroy(entities[i]);
}
}
}
}

Scripts usually do not need the buffer. Script updates run outside any query, so AddComponent, RemoveComponent, CreateEntity and Destroy apply immediately. The exception is Destroy: called while a query iterates, for example from a ForEach callback in a script, it waits until the start of the next script update phase.

Declaring your own components​

A component is a struct with public fields. Mark it [Component] so scenes, prefabs and the inspector know it, as Health above does. Its fields follow the same rules and attributes as script fields: public fields and settable properties are saved, [SerializeField] saves a private field, [Transient] keeps runtime state out of the file, and [Range], [Tooltip], [Header] and the rest shape the inspector. See Fields and the inspector for the full list.

Where you declare itHow it is registered
A script file in assets/scriptsAutomatically, when the game starts: every [Component] type in the scripts is registered
A pluginservices.AddComponent<Health>() in the plugin's Configure

Scenes save a component under its full type name, such as MyGame.Health. The ECS keeps component types for the life of the process, and that has costs for components declared in scripts: any script change then needs play mode to restart instead of hot reloading, and an assembly that declares components is never unloaded. In the editor, the scene view rebuilds its game in the background after such a change, so script components appear under Add component and in the inspector like any other. See Compiling and hot reload.

Scripts, components or systems​

All three end up in the same world, so the choice is about how the code is organized and how many entities it runs for.

UseWhenExample
A scriptBehavior of one entity or a handful: it has state, reacts to its own collisions, runs a sequence of waitsA player controller, a door, a boss, the shrine in Lantern Grove
A component and a systemThe same logic over many entities, or data that other systems readHealth regeneration for every enemy, parallax layers, bullets
A pluginCode shared between games, editor tools, game UI in Avalonia, code scenes, servicesA cutscene system, a HUD overlay, an inventory service

Scripts are dispatched per type from flat lists without allocating, but each one is still a separate object and a method call per frame. For thousands of similar entities, a system over a component wins: one call per frame instead of one per entity, and data laid out for the CPU cache. Many games mix them. Lantern Grove has scripts for the player, the lanterns and the shrine, and a Parallax component with a ParallaxSystem for its backdrop layers, all in its assets/scripts folder.