Skip to content

Jobs & Burst

Trecs integrates with Unity's job system and Burst compiler for parallel, high-performance entity processing. You can write job structs by hand — Trecs's source generator handles the scheduling boilerplate (ScheduleParallel, dependency tracking, field auto-wiring). For the simplest cases, the generator emits the entire job struct from a single annotated method.

[WrapAsJob] — the simplest approach

Mark a static [ForEachEntity] method with [WrapAsJob] and Trecs generates a Burst-compiled parallel job for you:

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

Two requirements:

  • static — the generated Burst job calls your method directly, with no system instance, so it must be static.
  • Use NativeWorldAccessor, not WorldAccessor for any world-level operations (DeltaTime, structural changes, set ops) inside the body.

Trecs schedules the job instead of calling your Execute() method directly.

[PassThroughArgument] is supported here too and forwards the data to the generated job.

Manual job structs

To define a custom job struct instead of using [WrapAsJob], put [ForEachEntity] on the job's Execute method — Trecs generates a ScheduleParallel for it the same way:

public partial class ParticleJobSystem : ISystem
{
    [BurstCompile]
    partial struct MoveJob
    {
        public float DeltaTime;

        [ForEachEntity(typeof(SampleTags.Particle))]
        public readonly void Execute(in Velocity velocity, ref Position position)
        {
            position.Value += DeltaTime * velocity.Value;
        }
    }

    public void Execute()
    {
        new MoveJob { DeltaTime = World.DeltaTime }.ScheduleParallel(World);
    }
}

[ForEachEntity] is not required. You can write an entirely hand-rolled IJobFor / IJobParallelFor and still pass Trecs data into it. See Advanced Job Features.

Per-iteration EntityHandle

A [ForEachEntity] callback inside a job can take an EntityHandle parameter alongside its components — works in both [WrapAsJob] and manual job structs:

[ForEachEntity(typeof(SampleTags.Particle))]
[WrapAsJob]
static void Cull(ref Lifetime lifetime, EntityHandle handle, in NativeWorldAccessor world)
{
    if (lifetime.Remaining <= 0)
        handle.Remove(world);
}

The handle is materialized per iteration from a component buffer the source generator wires up automatically. Inside jobs, entity-targeted ops go through EntityHandle taking a NativeWorldAccessor parameter — the same handle.Op(accessor) pattern as on the main thread (where the accessor is a WorldAccessor instead).

NativeWorldAccessor

NativeWorldAccessor is the Burst-compatible counterpart to WorldAccessor. Get one with world.ToNative() (or take it as an in NativeWorldAccessor parameter — [WrapAsJob] auto-injects it). World-level operations (entity creation, set membership) live on NativeWorldAccessor directly; entity-targeted operations live on EntityHandle and take the native accessor as an argument:

// In a job:
nativeWorld.AddEntity<GameTags.Bullet>(sortKey: (uint)index)
    .Set(new Position(pos));

entityHandle.Remove(nativeWorld);
entityHandle.SetTag<BallTags.Resting>(nativeWorld);     // partition transition (turn tag on / switch variant)
entityHandle.UnsetTag<BallTags.Active>(nativeWorld);    // partition transition (presence/absence dim only)

Warning

WorldAccessor is main-thread only. Inside jobs always use NativeWorldAccessor and the native read/write types — see Advanced Job Features for how to wire them via [FromWorld].

For deeper coverage — [FromWorld] auto-wiring, cross-template lookup types, native set operations, [GlobalIndex], external job-handle tracking — see Advanced Job Features. For the parallel iteration pattern itself, see Queries & Iteration.