Entity Systems
An entity’s behavior is a C# script (an EntityScript) attached to it: its
per-entity system. Where a world system
is created once per world and iterates many entities, an entity system is one
instance per entity that runs that entity’s logic.
This page is the whole picture of how that script attaches, when each part runs, the physics phase it runs in, and what is per-entity versus shared. Read it before you write much script code, the timing is the difference between code that works and subtle bugs.
An entity and its script
Section titled “An entity and its script”An entity on its own is just an id with components. Adding a C# Script component, with a Script Class, is what gives it behavior. That component holds the running instance and the editor-set property values.
struct SCSharpScriptComponent{ FString ScriptClass; // e.g. "Game.Player", the class to run FScriptPropertyOverrides PropertyOverrides; // [Property] values set in the editor
void* Instance; // this entity's managed instance (a GCHandle) ECSharpBindState BindState; // Unbound -> Attached -> Ready // ...};One instance per entity
Section titled “One instance per entity”Your script is a class. When it attaches to an entity, the engine creates one
instance of that class for that entity. That instance is what you write code
against, namely this. Two consequences, and both trip people up.
public sealed class Turret : EntityScript{ private static int Count; // shared across every Turret in the world
private FVector3 _Start; // per-entity
public override void OnReady() { Count++; // safe here: the instance is fully set up _Start = Transform.GetWorldLocation(); }}The order things run
Section titled “The order things run”For a single entity, top to bottom.
- The engine constructs the instance and fills in
Entity,World, and the cachedTransform. Editor[Property]values are applied here too. OnAttachruns, top-down (a parent before its children). The earliest hook.OnReadyruns, bottom-up (a child before its parent, so it runs up the tree with the root last), once the scene graph is set up. By now every child is ready, so it is safe to look up children and other entities here.OnUpdateruns every frame while playing, in its update phase (below).OnDetachruns once, when the entity is destroyed.
At map load vs at runtime
Section titled “At map load vs at runtime”- When a map loads, all of its entities run this together. Every script’s
OnAttachfirst, then everyOnReady. - When you spawn an entity (or prefab) while the game is running, its
OnAttachruns immediately andOnReadyright after. Either way,OnReadyalways runs once the entity is fully set up.
In the editor
Section titled “In the editor”Scripts run only in play mode (a Game or Simulation world). In the plain editor they stay dormant. Press Play or Simulate to run gameplay.
Update phases
Section titled “Update phases”Like a world system, an entity system runs in a physics phase, and you choose which one. There are two update hooks plus a fixed-step hook, and they run at different points relative to the physics step.
| Hook | When |
|---|---|
OnUpdate(float DeltaTime) | Once per frame, in the entity’s update phase (pre- or post-physics). |
OnFixedUpdate(float FixedDeltaTime) | At the fixed physics rate, 0..N times per frame, before the physics step. |
Pre-physics and post-physics
Section titled “Pre-physics and post-physics”By default OnUpdate runs in the pre-physics phase, before the physics step,
which is where you read input and apply movement intent. Add
[UpdatePhase(EScriptPhase.PostPhysics)] to the class to run its OnUpdate
after physics resolves instead, which is where you read settled results, for
example a follow camera or syncing visuals to a body.
[UpdatePhase(EScriptPhase.PostPhysics)]public sealed class FollowCamera : EntityScript{ public override void OnUpdate(float DeltaTime) { // Bodies have already moved this frame, read their final transforms. }}EScriptPhase has PrePhysics (the default) and PostPhysics. The phase applies
to the whole script’s OnUpdate.
Fixed update for physics
Section titled “Fixed update for physics”OnFixedUpdate(float FixedDeltaTime) runs at the fixed physics timestep
(1 / physics Hz), zero or more times per frame, before each physics step. Its
delta is the fixed step, not the frame delta, so it is framerate-independent. Use
it for anything that drives the simulation: applying forces or impulses, and
character movement.
public override void OnFixedUpdate(float FixedDeltaTime){ Registry.Get<SRigidBodyComponent>(Entity).AddForce(_Move * 1000.0f);}Use OnUpdate for per-frame logic and visuals; use OnFixedUpdate for
physics-affecting forces and movement. They are independent: a script can
override either, both, or neither.
Running in parallel
Section titled “Running in parallel”By default a script’s OnUpdate and OnFixedUpdate run one after another on the
game thread, in an unspecified order. That is always safe, a script can freely
touch other entities, spawn or destroy entities, and add or remove components.
When a script only ever touches its own entity, that serialization is wasted.
Add [NoDeps] to the class to promise the engine that the script’s per-frame
callbacks have no effect on any other entity. The engine then groups every
[NoDeps] script and ticks them across worker threads through the job
system. Scripts without the attribute keep running serially, so it is purely
opt-in.
[NoDeps]public sealed class Spin : EntityScript{ [Property(Units = "deg/s")] public float Rate = 90.0f;
public override void OnUpdate(float DeltaTime) { Transform.AddYaw(Rate * DeltaTime); // touches only this entity }}A spinner, a projectile that only moves itself, a decoration that bobs in place,
anything whose logic begins and ends with this entity is a good fit. On a scene
with thousands of such entities the speedup is large.
The promise
Section titled “The promise”Inside OnUpdate and OnFixedUpdate, a [NoDeps] script may:
- read and write this entity’s own components, through
RegistrywithEntityor a[RequireComponent]field, - move this entity’s
Transform, - do self-contained compute and read values like
DeltaTime,Time, and its own[Property]fields.
It must not:
- read or write another entity’s components or transform,
- make structural changes, spawning or destroying any entity, or adding or removing a component on any entity (its own included),
- broadcast a message or fire a signal that another script handles right away,
- block, await, or hand work to the task system.
Why it is safe
Section titled “Why it is safe”Each script instance owns exactly one entity. When every script in the parallel group touches only its own entity, no two of them ever reach the same data, so they run at once with no locking. Moving your own transform is fine too, those moves are queued and applied together after the parallel pass. The moment a script reaches outside its own entity that guarantee is gone, which is why the attribute is a promise you make rather than the default.
Where to put what
Section titled “Where to put what”| Put it here | For |
|---|---|
Fields + [Property] | Constants, tuning values, and editor-exposed values. Initialized before the entity exists. |
OnReady | Per-entity setup that needs the entity, such as caching components, reading the world, finding other entities. |
OnUpdate | Per-frame behavior and visuals. |
OnFixedUpdate | Forces, impulses, and movement that drive physics. |
OnDetach | Cleanup, disposing subscriptions and timers you created (see Events). |
A static member | State or helpers shared across every entity of the script. |
public sealed class Spinner : EntityScript{ [Property(Units = "deg/s")] public float TurnRate = 90.0f; // editor-exposed, applied before OnReady
public override void OnUpdate(float DeltaTime) { Transform.AddYaw(TurnRate * DeltaTime); }}Hot reload
Section titled “Hot reload”When you save a script while the editor is running, it recompiles in place.
The engine tears down the old managed instances, loads the new assembly,
and rebinds each entity (running OnAttach and OnReady again on the new
version). Your [Property] values set in the editor survive. They are stored on
the component and reconciled against the script’s current fields, so they hold up
even as you add, remove, or rename fields. Ordinary runtime state held in instance
fields is reset. Hot reload is ideal for tuning; it just does not preserve a
script’s accumulated state.