Custom job interfaces
Little Physics recommends its own job interfaces — IBodiesJob, ICollisionJob, ISurfaceJob, and ILineCastJob — for the common iteration patterns inside LittlePhysicsUserSystemGroup. Those interfaces handle buffer wiring, loop bounds, and PhysicsJobHandle chaining through ScheduleAndChain.
When you need something they do not cover — multiple native arrays in one pass, custom iteration logic, or a one-off IJobEntity query — write a standard Unity job (IJob, IJobParallelFor, IJobEntity) and chain it yourself.
When to use raw Unity jobs
| Situation | Prefer |
|---|---|
Per-body read/write over BodiesList | IBodiesJob |
| Per collision slot or pair | ICollisionJob |
| Per surface contact | ISurfaceJob |
| Linecast along the spatial map | ILineCastJob |
BodiesList + one extra native array (e.g. Randoms) | IBodiesJob.IWriteIndex — see Randoms |
| Multiple native buffers with custom iteration | IJobParallelFor — this page |
| Custom outer loop over collision slots with extra state | IJobParallelFor or IJob |
Post-export ECS queries (LocalTransform, destroy entity) | IJobEntity — see Export workflow |
PhysicsJobHandle chaining
Any job that reads or writes physics native memory must combine with SimulationDataComponent.PhysicsJobHandle before scheduling, then assign the result back to both PhysicsJobHandle and state.Dependency.
ref var simulation = ref SystemAPI.GetSingletonRW<SimulationDataComponent>().ValueRW;
var combined = JobHandle.CombineDependencies(
state.Dependency, simulation.PhysicsJobHandle);
var handle = myJob.Schedule(/* args */, combined);
simulation.PhysicsJobHandle = handle;
state.Dependency = handle;
This serializes access to shared buffers across import, the fixed-step inner loop, and export. Internal physics systems and package ScheduleAndChain extensions follow the same rule.
Scheduling over
BodiesList, collision maps, orRandomswithout combiningPhysicsJobHandlecan race with internal jobs and corrupt native data.
See SimulationDataComponent and Pipeline — Chaining jobs.
IJobParallelFor — per-body work
The most common raw pattern mirrors IBodiesJob: parallel over body indices 0 … ActiveBodiesCount - 1, batch size 32 (same default the package uses).
For BodiesList plus an extra array such as Randoms, prefer IBodiesJob.IWriteIndex with ScheduleAndChain — see Randoms. Use raw IJobParallelFor when you need full control over scheduling or several writable buffers:
ref var simulation = ref SystemAPI.GetSingletonRW<SimulationDataComponent>().ValueRW;
var combined = JobHandle.CombineDependencies(state.Dependency, simulation.PhysicsJobHandle);
var handle = new DampVelocityParallelJob
{
BodiesList = structures.BodiesList,
Factor = 0.99f,
}.Schedule(simulation.ActiveBodiesCount, 32, combined);
simulation.PhysicsJobHandle = handle;
state.Dependency = handle;
[BurstCompile]
private struct DampVelocityParallelJob : IJobParallelFor
{
[NativeDisableParallelForRestriction]
public NativeArray<PhysicsBodyData> BodiesList;
public float Factor;
public void Execute(int index)
{
var body = BodiesList[index];
if (body.Main == Entity.Null || !body.IsDynamic)
{
return;
}
var velocity = body.VelocityData;
velocity.Linear *= Factor;
body.VelocityData = velocity;
BodiesList[index] = body;
}
}
Guidelines for parallel body jobs
| Rule | Why |
|---|---|
Loop length = simulation.ActiveBodiesCount | Only imported bodies are live this frame |
Skip body.Main == Entity.Null | Cleared pool slots are not active bodies |
Mark writable arrays [NativeDisableParallelForRestriction] | Each thread writes its own index, but Unity’s safety system needs the attribute |
Use [ReadOnly] on arrays you only read | Enables stricter safety when sharing read-only views |
Multiply forces by LittlePhysicsTimeComponent.DeltaTime | Substep-scaled delta inside LittlePhysicsUserSystemGroup |
The allocated pool size is MaxEntitiesCount; never schedule parallel jobs beyond ActiveBodiesCount unless you intentionally process empty slots.
IJobParallelFor — collision slots
Package ICollisionJob already iterates CollisionDataMap slots per body. Use a raw job when you need a different parallelization — for example one thread per body that performs extra work across all slots with local state:
[BurstCompile]
private struct CustomCollisionResponseJob : IJobParallelFor
{
[NativeDisableParallelForRestriction]
public NativeArray<PhysicsBodyData> BodiesList;
[ReadOnly]
public ListsArray<CollisionData> CollisionDataMap;
public void Execute(int bodyIndex)
{
var body = BodiesList[bodyIndex];
if (body.Main == Entity.Null)
{
return;
}
int count = CollisionDataMap.GetCount(bodyIndex);
for (int slot = 0; slot < count; slot++)
{
var collision = CollisionDataMap.GetValue(bodyIndex, slot);
if (!collision.HasValue)
{
continue;
}
// Read collision.OtherIndex, ContactPoint, Normal, etc.
}
BodiesList[bodyIndex] = body;
}
}
Schedule with the same PhysicsJobHandle chaining and ActiveBodiesCount loop length. See CollisionsSingleton for buffer layout.
IJob — single-threaded native work
Use IJob when work must run on one thread — custom aggregation, debugging, or sequential passes over native data that are awkward to split.
[BurstCompile]
private struct SummarizeEnergyJob : IJob
{
[ReadOnly] public NativeArray<PhysicsBodyData> BodiesList;
public int ActiveBodiesCount;
public NativeReference<float> TotalEnergy;
public void Execute()
{
float sum = 0f;
for (int i = 0; i < ActiveBodiesCount; i++)
{
var body = BodiesList[i];
if (body.Main == Entity.Null || !body.IsDynamic)
{
continue;
}
sum += math.lengthsq(body.VelocityData.Linear);
}
TotalEnergy.Value = sum;
}
}
var combined = JobHandle.CombineDependencies(state.Dependency, simulation.PhysicsJobHandle);
var handle = new SummarizeEnergyJob { /* fields */ }.Schedule(combined);
simulation.PhysicsJobHandle = handle;
state.Dependency = handle;
For linecasts, prefer ILineCastJob (already single-threaded). If you call PhysicsStructuresComponent.LineCast / LineCastFirst directly, still chain PhysicsJobHandle.
IJobEntity — ECS components
IJobEntity fits import and export hooks where you read or write ECS components rather than native simulation buffers:
- Before import — modify
LocalTransform,PhysicsVelocityComponent, or custom components;PhysicsJobHandleoptional. - After export — read
LocalTransformafterExportPhysicsDataSystem;PhysicsJobHandleoptional unless you also touch native memory in the same system.
The Scene 3 — Pachinko sample uses IJobEntity in LittlePhysicsExportGroup to destroy entities below a Y threshold — see Export workflow.
Do not rely on ECS component queries for mid-step physics inside LittlePhysicsUserSystemGroup — component data is stale until export. Read PhysicsStructuresComponent instead.
Which group to use
| Group | Raw job touches | Chain PhysicsJobHandle? |
|---|---|---|
LittlePhysicsImportGroup (after internal import) | Native buffers | Yes |
LittlePhysicsImportGroup (before internal import) | ECS components only | No |
LittlePhysicsUserSystemGroup | Native buffers | Yes |
LittlePhysicsExportGroup (before internal export) | Native buffers | Yes |
LittlePhysicsExportGroup (after internal export) | ECS components only | No |
See Custom job groups for UpdateBefore / UpdateAfter attributes.
Package interfaces vs raw jobs
If your logic maps to an existing package interface, use it — less boilerplate and the same chaining behavior:
// Package interface — chaining handled for you
new DampVelocityJob { Factor = 0.99f }
.ScheduleAndChain(ref state, in structures, ref simulation);
// Equivalent raw job — you chain PhysicsJobHandle yourself
var combined = JobHandle.CombineDependencies(state.Dependency, simulation.PhysicsJobHandle);
var handle = new DampVelocityParallelJob { BodiesList = structures.BodiesList, Factor = 0.99f }
.Schedule(simulation.ActiveBodiesCount, 32, combined);
simulation.PhysicsJobHandle = handle;
state.Dependency = handle;
Reach for raw Unity jobs when the package callback shape is too narrow — extra native arrays, custom filtering, or non-standard iteration.
Related
- IBodiesJob, ICollisionJob, ISurfaceJob, ILineCastJob — preferred package interfaces
- Inside the pipeline — when mid-step systems run
- Randoms —
IBodiesJob.IWriteIndexexample withRandoms - PhysicsStructuresComponent — native buffers to pass into your jobs