Skip to content

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 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
// ...
};

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();
}
}

For a single entity, top to bottom.

  1. The engine constructs the instance and fills in Entity, World, and the cached Transform. Editor [Property] values are applied here too.
  2. OnAttach runs, top-down (a parent before its children). The earliest hook.
  3. OnReady runs, 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.
  4. OnUpdate runs every frame while playing, in its update phase (below).
  5. OnDetach runs once, when the entity is destroyed.
  • When a map loads, all of its entities run this together. Every script’s OnAttach first, then every OnReady.
  • When you spawn an entity (or prefab) while the game is running, its OnAttach runs immediately and OnReady right after. Either way, OnReady always runs once the entity is fully set up.

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.

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.

HookWhen
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.

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.

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.

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.

Inside OnUpdate and OnFixedUpdate, a [NoDeps] script may:

  • read and write this entity’s own components, through Registry with Entity or 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.

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.

Put it hereFor
Fields + [Property]Constants, tuning values, and editor-exposed values. Initialized before the entity exists.
OnReadyPer-entity setup that needs the entity, such as caching components, reading the world, finding other entities.
OnUpdatePer-frame behavior and visuals.
OnFixedUpdateForces, impulses, and movement that drive physics.
OnDetachCleanup, disposing subscriptions and timers you created (see Events).
A static memberState 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);
}
}

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.