Performance · CPU · common
Time Slicing
let a different group think each frame
Split expensive per-entity work into buckets and run one bucket per frame. Cost per frame drops by the bucket count; each entity reacts a little later.
Each dot is an agent chasing the yellow beacon. Only the orange bucket thinks this frame and re-aims at the beacon; cyan agents keep moving toward the goal they picked earlier (white line), so they trail behind. Raise the bucket count and watch the swarm react later and spread out; slow the tick to see the rotation.
What it is
Some work is too expensive to run for every entity every frame but doesn't need to: AI re-planning, line-of-sight checks, path requests, LOD selection, sensor queries. Time slicing divides those entities into B buckets and updates one bucket per frame in round-robin order. The per-frame cost drops to about 1/B of the total, and every entity still updates once every B frames.
The price is latency. With 4 buckets at 60 Hz an agent notices a change up to 4 frames (67 ms) late. That's invisible for a guard choosing a patrol point and noticeable for an enemy dodging a bullet, so slice only work that tolerates it and keep reflex-level checks per frame.
Sliced updates must use the time since that entity's last update, B · dt on average, not the current frame's dt, or timers and integrators run B times too slowly. Cheap per-frame work such as movement keeps running every frame along the last decision, so motion stays smooth while decisions are staggered.
Two common shapes: fixed buckets (index % B or contiguous ranges) for predictable latency, and a round-robin cursor with an item budget ("update up to 50 per frame") that adapts as the entity count changes. Both keep the frame budget flat instead of spiking.
Key formulas
- Cost per frame
N entities, c ms of work each, B buckets.
- Worst-case reaction delay
B = 4 at 60 Hz: up to 66.7 ms.
Rules of thumb
- Start with 2–4 buckets for AI decisions and 8–16 for slow systems (ambient life, far-away LOD, cover evaluation).
- Keep reaction-critical checks (incoming damage, collision, input) out of slices.
- Pass the accumulated time since the entity's last update into sliced logic.
- Prefer an item budget (e.g. max 40 path requests per frame) when the count varies widely.
- Give new entities a staggered start bucket so a spawned wave doesn't all think on the same frame.
Implementation
const BUCKETS = 4
let frame = 0
function updateAI(agents: Agent[], dt: number, now: number) {
const bucket = frame++ % BUCKETS
for (let i = 0; i < agents.length; i++) {
const a = agents[i]
a.moveAlongPlan(dt) // cheap: every frame
if (i % BUCKETS !== bucket) continue
const elapsed = now - a.lastThink // ≈ BUCKETS * dt
a.lastThink = now
a.think(elapsed) // expensive: 1 frame in 4
}
}budget = 40
repeat min(budget, count) times:
process(items[cursor])
cursor = (cursor + 1) mod countCommon mistakes
- Passing the frame's
dtto sliced logic, so timers and cooldowns run B times slower. - Slicing reflex checks and making enemies feel sluggish or miss hits.
- Using
index % Bon a list that reorders or removes items, so some entities skip updates or run twice. Store a stable bucket per entity. - Spawning a crowd that all lands in one bucket and recreates the spike you tried to remove.
Prompt
Add time slicing to my TypeScript + Three.js game's AI. Write a `Scheduler` that holds entities with a stable `bucket` assigned at registration (round robin), a configurable bucket count B, and per-entity `lastUpdate` time. Each frame it calls `think(entity, elapsedSinceLastThink)` only for the active bucket and `move(entity, dt)` for all. Also provide a budgeted mode that processes at most K entities per frame with a wrapping cursor that survives removals. Handle B changing at runtime by reassigning buckets evenly. Write tests: every entity thinks exactly once per B frames, elapsed time ≈ B·dt, and removal mid-cycle never skips anyone.
Related
Further reading
Updated 2026-11-01