Skip to content
Bit Quirky Behavior Trees and
State Machines

Search documentation

All topics and APIs

Close returns focus to Search.

What are you looking for?

Try a common topic or enter a complete API identifier.

Browse the reference instead

Esc closes · Tab reaches results · Enter opens the focused link

v0.6.0
Menu

Burst and jobs

See exactly which part of the behavior runtime is Burst compiled, which part stays managed, and where your code sits relative to the job.

The decision itself compiles. Horde Survival’s enemies run the same node semantics as a Platformer GameObject, inside a Burst job, across worker threads, with their state in ECS buffers.

“Visual scripting is slow” is a fair worry, because plenty of graph tools evaluate a managed object tree per agent per frame, allocate while they do it, and cannot be scheduled. When 2,400 enemies each want a decision, that shape costs more than the gameplay.

Horde Survival’s enemy decision is baked into flat native tables before the game runs. The tick reads those tables and writes per-entity buffers, so there is no managed object graph to walk while enemies are deciding.

Piece Where it lives Compiled
The tick kernel BehaviorTick, in BehaviorTickCore.cs Yes, through the jobs that call it
Batched entity tick BehaviorTickJob, an IJobEntity marked [BurstCompile] Yes
Reactive entity tick BehaviorReactiveTickJob, the same shape for graphs with reactive conditions Yes
Runtime entity tick BehaviorEntityTickJob, an IJobChunk marked [BurstCompile] Yes
Native blackboard Blackboard, a readonly struct over NativeArray handles, marked [BurstCompile] Yes
Generated entity member access The generated Carrier, an IJobChunk marked [BurstCompile] Yes
Entity runtime object BehaviorEntityRuntime, its registration lists, your command handler objects No, managed orchestration
GameObject host composition Binders, providers, movement adapters, services, coroutines, events No, managed, on Unity’s thread

The same BehaviorTick kernel decides on both hosts, which is why a graph behaves identically wherever it runs. The compilation differs: the entity host calls that kernel from inside a Burst job, and the GameObject host calls it as ordinary managed code on Unity’s thread. Nothing under the package’s GameObject folder carries [BurstCompile].

Two schedules exist, and which one your graph uses depends on whether it binds entity members.

  • A graph without entity member bindings is scheduled straight from the runtime: BehaviorEntityTickJob goes out with ScheduleParallel(agents, dependency), so agents in the query advance on worker threads in one schedule. BehaviorTickSystem, the package’s own system in SimulationSystemGroup, does the same with BehaviorTickJob.ScheduleParallel.
  • A graph with generated entity member bindings runs through its generated entry point. The kernel is handed to the generated Carrier, which owns the typed component handles and the declared access rows, and the scheduler advances it in ordered stages: sample, tick, then push. Horde Survival’s EnemyBrain takes this path, so sample, tick, and push complete in order inside one update, and each stage is one Burst Carrier job over the agent query.

A graph without entity member bindings is one ScheduleParallel per update. Neither shape schedules or completes per agent, and neither allocates a temporary per agent.

  • An input binder returns a JobHandle from Schedule(EntityQuery agents, JobHandle dependency), so your fact sampling can be a Burst job of your own. The runtime chains it ahead of the tick.
  • Command handlers are managed objects, called after the tick completes, with the entity resolved. Spawning, damage, and navigation belong there, not inside the graph job.
  • Entity member bindings need Burst AOT in a player. There is no managed fallback path for them, which is the trade for generated typed access with no lookup by name.
  • The GameObject host runs the core and everything around it as managed code: binders, providers, adapters, coroutines, and events on Unity’s thread. That is the whole reason that host can touch a MonoBehaviour at all.

The runtime allocates while it is getting ready and stops allocating once it is warm. Bake and initialization create the native tables and the per-agent buffers; a warmed tick reuses them in place.

Four tests in the package hold that line: BehaviorAgentTests.WarmedTick_AllocatesZeroManagedBytes, BehaviorEntityRuntimeTests.Runtime_WarmedCompleteTicksAllocateNoManagedMemory, MoveToTests.GameObjectMovementPath_WarmedTickAllocatesZeroManagedBytes, and LowerPriorityInterruptionTests.WarmedLowerPriorityInterruption_AllocatesZeroManagedBytes. Performance carries the measured player figures.

Your own code is your own budget. A binder that allocates a NativeArray with Allocator.Temp every frame, or a command handler that builds a string, allocates whatever it allocates, and the graph cannot help with that.

When it goes wrong

SymptomCheckFix
Burst reports that a job will not compile.Read which type it names. A managed field reached into a job is the usual cause.Keep managed composition outside the job. The runtime object and handlers are managed by design.
An entity graph runs in the editor and fails in an IL2CPP player.Confirm Burst AOT is enabled for the target and the generated bindings compiled.Entity member bindings need Burst AOT. See Addressables and builds.
The profiler shows the tick on the main thread.Read whether the tick call completes inside the same update.TickGraph and TickAll complete before returning by design. Schedule the work you want overlapped around them, not inside them.
Managed allocation appears in a warmed frame.Sample allocation with the graph tick disabled to separate your code from the runtime.Check your binder, your handlers, and anything building strings for logs.
Full-size image