Skip to content

Systems

Systems contain the logic that operates on entities. Every system implements ISystem and its Execute() method is called once per frame at the appropriate update phase.

Defining a System

public partial class SpinnerSystem : ISystem
{
    readonly float _rotationSpeed;

    public SpinnerSystem(float rotationSpeed)
    {
        _rotationSpeed = rotationSpeed;
    }

    [ForEachEntity(MatchByComponents = true)]
    void Execute(ref Rotation rotation)
    {
        float angle = World.DeltaTime * _rotationSpeed;
        rotation.Value = math.mul(rotation.Value, quaternion.RotateY(angle));
    }
}

Key points:

  • Systems are partial class (source generation fills in boilerplate)
  • Systems are not created by Trecs. Instantiate them however you like and register with the world builder.
  • World is a source-generated property providing the WorldAccessor

The Execute Method

Every system must define exactly one method named Execute. This is the system's entry point, called once per frame. There are several forms it can take:

  • [ForEachEntity] method — Source-generated iteration over matching entities. This is the most common form.
  • public void Execute() — A manual entry point where you write your own logic, queries, and iteration. Required when you have multiple [ForEachEntity] methods and need to call them explicitly.
  • [WrapAsJob] static method — A [ForEachEntity] method that runs as a Burst-compiled parallel job instead of on the main thread. See Jobs & Burst.
// Option 1: ForEachEntity (most common)
public partial class MovementSystem : ISystem
{
    [ForEachEntity(Tag = typeof(GameTags.Player))]
    void Execute(in PlayerView player)
    {
        player.Position += player.Velocity * World.DeltaTime;
    }
}

// Option 2: Manual Execute
public partial class DamageSystem : ISystem
{
    public void Execute()
    {
        foreach (var enemy in EnemyView.Query(World).WithTags<GameTags.Enemy>())
        {
            if (enemy.Health <= 0)
                World.RemoveEntity(enemy.EntityIndex);
        }
    }

    partial struct EnemyView : IAspect, IRead<Health>, IAspectEntityIndex { }
}

// Option 3: WrapAsJob (parallel, Burst-compiled)
public partial class ParticleMoveSystem : ISystem
{
    [ForEachEntity(Tag = typeof(SampleTags.Particle))]
    [WrapAsJob]
    static void Execute(in Velocity velocity, ref Position position, in NativeWorldAccessor world)
    {
        position.Value += world.DeltaTime * velocity.Value;
    }
}

ForEachEntity

The [ForEachEntity] attribute marks a method for source-generated entity iteration. The generator creates the query and loop code automatically.

Scoping by Tags

// Single tag
[ForEachEntity(Tag = typeof(SampleTags.Spinner))]
void Execute(ref Rotation rotation) { ... }

// Multiple tags
[ForEachEntity(Tags = new[] { typeof(BallTags.Ball), typeof(BallTags.Active) })]
void Execute(in ActiveBall ball) { ... }

Scoping by Components

When you don't want to target specific tags, use MatchByComponents to iterate all entities that have the required components:

[ForEachEntity(MatchByComponents = true)]
void Execute(ref Position position, in Velocity velocity)
{
    position.Value += velocity.Value * World.DeltaTime;
}

Scoping by Set

[ForEachEntity(Set = typeof(SampleSets.HighlightedParticle))]
void Execute(in ParticleView particle) { ... }

See Sets for more on defining and using sets.

Multiple ForEachEntity Methods

A system can have multiple iteration methods for different entity groups:

[VariableUpdate]
public partial class BallRendererSystem : ISystem
{
    [ForEachEntity(Tags = new[] { typeof(BallTags.Ball), typeof(BallTags.Active) })]
    void RenderActive(in ActiveBallView ball)
    {
        // Render active balls as red
    }

    [ForEachEntity(Tags = new[] { typeof(BallTags.Ball), typeof(BallTags.Resting) })]
    void RenderResting(in RestingBallView ball)
    {
        // Render resting balls as gray
    }

    public void Execute()
    {
        RenderActive();
        RenderResting();
    }

    partial struct ActiveBallView : IAspect, IRead<Position, GameObjectId> { }
    partial struct RestingBallView : IAspect, IRead<Position, GameObjectId> { }
}

When you have multiple [ForEachEntity] methods, you must provide an explicit Execute() that calls them.

Parameters

[ForEachEntity] methods accept the following parameter types. The source generator wires them automatically.

Entity data (choose one style per method — cannot be mixed):

  • Component refsref T (read-write) or in T (read-only) for IEntityComponent types. Multiple components can be listed.
  • Aspectin MyAspect for bundled component access (see Aspects). Only one aspect per method.

Additional parameters (can be combined with either style above):

  • EntityIndex — the current entity's transient index
  • WorldAccessor — the system's world accessor (main-thread methods only)
  • NativeWorldAccessor — job-safe world access ([WrapAsJob] methods only). See Jobs & Burst.
  • [PassThroughArgument] — custom values passed in by the caller. See below.

PassThroughArgument

Mark a parameter with [PassThroughArgument] to pass custom values into a [ForEachEntity] method. The generated method will include matching parameters that the caller must provide:

public partial class ParticleBoundSystem : ISystem
{
    readonly float _halfSize;

    [ForEachEntity(Tag = typeof(SampleTags.Particle))]
    [WrapAsJob]
    static void ExecuteAsJob(
        ref Velocity velocity,
        ref Position position,
        [PassThroughArgument] float halfSize
    )
    {
        // halfSize is passed in by the caller, not looked up from the world
        if (position.Value.x > halfSize || position.Value.x < -halfSize)
            velocity.Value.x = -velocity.Value.x;
    }

    public void Execute()
    {
        // Pass _halfSize to the generated method
        ExecuteAsJob(_halfSize);
    }
}

This is useful for passing configuration, precomputed values, or other data that isn't a component on the iterated entities. [PassThroughArgument] works with both main-thread and [WrapAsJob] methods, but the value must be an unmanaged type when used with jobs.

SingleEntity

Use [SingleEntity] for operations on a singleton entity (asserts exactly one entity matches):

[SingleEntity(Tag = typeof(GlobalTag))]
void Execute(ref Score score)
{
    score.Value += 1;
}

Update Phases

Systems run in one of four phases, controlled by attributes:

Phase Attribute Typical Use
Input [InputSystem] Reading player input
Fixed Update (default) Simulation, physics, game logic
Variable Update [VariableUpdate] Rendering, visual updates
Late Variable Update [LateVariableUpdate] Final frame cleanup
// Fixed update (default — no attribute needed)
public partial class PhysicsSystem : ISystem { ... }

// Variable update
[VariableUpdate]
public partial class RenderSystem : ISystem { ... }

// Input phase
[InputSystem]
public partial class InputSystem : ISystem { ... }

Fixed update runs at a fixed timestep (default 1/60s) and may run multiple times per frame to catch up (or zero times at fast variable frame rates). Variable update runs once per frame at the actual frame rate. See Input System for details on the input phase.

Trecs does not hook into Unity's update loop automatically — you drive it by calling these methods on the world each frame:

  • world.Tick() — Runs variable-update systems and some number of fixed update ticks
  • world.LateTick() — Runs late-variable-update systems.

Typically these are called from a MonoBehaviour like this:

public class GameLoop : MonoBehaviour
{
    World _world;

    void Start()
    {
        _world = new WorldBuilder()
            .AddEntityType(PlayerEntity.Template)
            .AddSystem(new MovementSystem())
            .BuildAndInitialize();
    }

    void Update()
    {
        _world.Tick();
    }

    void LateUpdate()
    {
        _world.LateTick();
    }

    void OnDestroy()
    {
        _world.Dispose();
    }
}

All outstanding jobs are completed at the boundary between phases. See Dependency Tracking.

System Ordering

Control execution order within a phase using [ExecutesAfter] and [ExecutesBefore]:

[ExecutesAfter(typeof(SpawnSystem))]
public partial class LifetimeSystem : ISystem { ... }

[ExecutesBefore(typeof(RenderSystem))]
public partial class PhysicsSystem : ISystem { ... }

Order constraints can also be declared at the builder level:

new WorldBuilder()
    .AddSystemOrderConstraint(typeof(SpawnSystem), typeof(LifetimeSystem))
    // ...

ExecutePriority

Use [ExecutePriority] to influence ordering when no explicit constraints apply. The default priority is 0. Lower values run earlier, higher values run later:

[ExecutePriority(-10)]  // Runs before systems with default priority
public partial class EarlySystem : ISystem { ... }

[ExecutePriority(10)]   // Runs after systems with default priority
public partial class LateSystem : ISystem { ... }

[ExecutesAfter] and [ExecutesBefore] constraints always take precedence over priority — priority only breaks ties among systems with no ordering constraints between them.

Entity Operations in Systems

Systems access the world via the source-generated World property:

public partial class SpawnSystem : ISystem
{
    public void Execute()
    {
        // Create entities
        World.AddEntity<SampleTags.Sphere>()
            .Set(new Position(float3.zero))
            .Set(new Lifetime(5f));

        // Remove entities
        World.RemoveEntity(entityIndex);

        // Partition transitions
        World.MoveTo<BallTags.Ball, BallTags.Resting>(ball.EntityIndex);

        // Access time and RNG
        float dt = World.DeltaTime;
        float random = World.Rng.Next();
    }
}

Registering Systems

Systems are registered with the world builder:

new WorldBuilder()
    .AddSystem(new SpinnerSystem(rotationSpeed: 2f))
    .AddSystem(new SpinnerGameObjectUpdater(gameObjectRegistry))
    .AddSystem(new LifetimeSystem())
    .BuildAndInitialize();