05 — Job System¶
Parallel entity processing with Unity's job system and Burst, shown side-by-side with a main-thread version.
Source: com.trecs.core/Samples~/Tutorials/05_JobSystem/
What it does¶
Particles spawn, move, and bounce off boundaries. A toggle switches between main-thread and Burst-compiled parallel execution to compare performance side by side. Arrow keys adjust particle count.
Schema¶
Components¶
Position, Rotation, UniformScale, ColorComponent from Common (via IndirectRenderable), Velocity defined locally, plus global components for configuration:
[Unwrap]
public partial struct DesiredNumParticles : IEntityComponent { public int Value; }
[Unwrap]
public partial struct IsJobsEnabled : IEntityComponent { public bool Value; }
Templates¶
public partial class ParticleEntity : ITemplate,
IExtends<CommonTemplates.IndirectRenderable>,
ITagged<SampleTags.Particle>
{
Velocity Velocity;
}
public partial class Globals : ITemplate, IExtends<TrecsTemplates.Globals>
{
DesiredNumParticles DesiredNumParticles = new() { Value = 5000 };
IsJobsEnabled IsJobsEnabled = new() { Value = true };
}
IndirectRenderable is the no-GameObject base — it adds Position, Rotation, UniformScale, ColorComponent. The sample uses GPU-instanced indirect rendering so particles never get a companion GameObject.
Systems¶
ParticleMoveSystem — job vs main thread¶
Defines both a main-thread and a [WrapAsJob] version of the same logic, switching at runtime:
public partial class ParticleMoveSystem : ISystem
{
[ForEachEntity(typeof(SampleTags.Particle))]
void ExecuteMainThread(in Velocity velocity, ref Position position)
{
position.Value += World.DeltaTime * velocity.Value;
}
[ForEachEntity(typeof(SampleTags.Particle))]
[WrapAsJob]
static void ExecuteAsJob(
in Velocity velocity,
ref Position position,
in NativeWorldAccessor world
)
{
position.Value += world.DeltaTime * velocity.Value;
}
public void Execute()
{
if (World.GlobalComponent<IsJobsEnabled>().Read.Value)
{
ExecuteAsJob();
}
else
{
ExecuteMainThread();
}
}
}
The job version is static and uses NativeWorldAccessor; the main-thread version is an instance method that accesses World directly.
The source generator emits a job struct that calls the static method per entity, plus a wrapper that resolves groups, wires dependencies, and schedules:
// Generated by source generator (simplified)
// Generated wrapper — this is what you call from Execute()
void ExecuteAsJob()
{
var job = new ExecuteAsJob_AutoJob();
// ...
// For each matching tag combination: populate buffers, wire up dependencies, schedule
job.ScheduleParallel(count, batchSize, dependencies);
// ... track job for dependency system ...
}
[BurstCompile]
struct ExecuteAsJob_AutoJob : IJobFor
{
public NativeComponentBufferRead<Velocity> Velocities;
public NativeComponentBufferWrite<Position> Positions;
public NativeWorldAccessor World;
public void Execute(int i)
{
// Calls your static method for each entity
ParticleMoveSystem.ExecuteAsJob(
in Velocities[i], ref Positions[i], in World);
}
}
The generated ExecuteAsJob() method handles dependency tracking and scheduling — call it and you're done.
ParticleSpawnerSystem — job-based entity creation¶
Entity handles must be reserved on the main thread before spawning inside jobs. Otherwise IDs assigned by parallel threads would depend on scheduling order — non-deterministic. Pre-reserving also lets the job use the handles immediately for cross-entity references.
// Reserve handles on the main thread
var reservedRefs = World.ReserveEntityHandles(count, Allocator.TempJob);
var jobHandle = new SpawnParticleJob
{
ReservedRefs = reservedRefs,
Tags = TagSet<SampleTags.Particle>.Value,
// ...
}.ScheduleParallel(World, count);
reservedRefs.Dispose(jobHandle);
Inside the job, the AddEntity overload takes the pre-built TagSet, a sort key, and the reserved handle:
World
.AddEntity(Tags, (uint)i, ReservedRefs[i])
.Set(new Position(position))
.Set(new Velocity(velocity));
The sort key controls the deterministic post-submission order of entities created in parallel.
Concepts introduced¶
[WrapAsJob]— source-generated parallel Burst iteration. See Jobs & Burst.NativeWorldAccessor— structural operations and reads inside jobs. See Advanced Job Features.ReserveEntityHandles— pre-allocate stable handles before parallel creation.- Sort keys — deterministic ordering of job-created entities. See Structural Changes.
[FromWorld]/[PassThroughArgument]— wire job parameters from the world or outer scope. See Advanced Job Features.