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.
On this page
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.
The problem
Section titled “The problem”“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.
What is compiled
Section titled “What is compiled”| 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].
How the entity tick is scheduled
Section titled “How the entity tick is scheduled”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:
BehaviorEntityTickJobgoes out withScheduleParallel(agents, dependency), so agents in the query advance on worker threads in one schedule.BehaviorTickSystem, the package’s own system inSimulationSystemGroup, does the same withBehaviorTickJob.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’sEnemyBraintakes this path, so sample, tick, and push complete in order inside one update, and each stage is one BurstCarrierjob 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.
What that means for your code
Section titled “What that means for your code”- An input binder returns a
JobHandlefromSchedule(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.
Allocation
Section titled “Allocation”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
| Symptom | Check | Fix |
|---|---|---|
| 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. |
- Performance: the recorded figures and their method.
- Entity agents: registering and ticking the graph.
- Entity bindings: the generated access this page schedules.