The Server Tick and Physics
A server advances its world at three different rates. Scripts run once per frame, the physics solver requires a delta time that never changes, and replication requires packets often enough to show continuous motion and few enough to fit a home connection. The server tick reconciles those three rates. It runs at 60 Hz, and the stages on this page all run inside one tick.
This page covers the internals: the order of the stages, the accumulator that drives the fixed-step loop, what Box3D does during the physics step, and what the tail of the tick does afterwards. If you are looking for the script-facing API instead, read The Simulation Tick and Entity and Script Lifecycle.
Simulation runs at 60 Hz and every physics step spans exactly 1/60 s, never a measured frame time. Replication runs at 30 Hz as a consequence of that rate: every second tick ends by packing and broadcasting a snapshot, so the 30 Hz figure holds for as long as the server sustains its 60 Hz tick. The client renders as fast as the display hardware allows and feeds the elapsed real time into an accumulator that steps its local simulation at the same fixed 1/60 s cadence. The step count follows elapsed time rather than frame count, so a crate integrates 60 steps per second of real time, minus the remainder left in the accumulator, on a machine drawing 144 frames per second and on one drawing 40.
One tick end to end#
The first tick runs once the place file is loaded. Loading it assigns a 64-bit network ID to every entity in the file and queues every script entity for execution; no script code has run yet. The tick then opens by loading those pending scripts, which is also how a script saved in the editor reaches the running world: it waits in the queue until the start of the next tick.
The stage order follows from three constraints. Gameplay code sets its values before the solver integrates, so all script work that runs at frame rate runs first; the solver reads gravity, the sleep threshold and the collision matrix out of the physics world rather than off each body, so those are pushed down immediately before it; and snapshot packing reads world transforms, which have to be recomputed from the bodies the solver just moved. That fixes the stage order:
After the pending scripts load, Update(dt) runs on every active script
instance with the frame delta, then the systems registered with World.System(name, query, fn, "Default"). Player joins come next: the engine
processes everyone who connected since the last tick and fires their Session.PlayerJoined events. Death verification then scans the entities that
carry Character and Health without a CharacterDead tag for lethal
conditions: health at zero, a position below Physics.FallenPartsDestroyHeight, or an invalid transform. Physics
configuration closes the gameplay half by pushing Physics.Gravity, the World
component’s SleepVelocityThreshold and the collision matrix into the active
physics world.
Systems run in their own stage rather than inside the per-instance hook,
because of how the engine passes their data. A query does not call back once
per entity: it passes the matched components as packed columns, q.count alongside q.Entity[i] and q.Position[i], each column a contiguous array
indexed from 1 to q.count, so a single loop over them reads memory
sequentially. The engine crosses into a script environment once per query
dispatch; a per-instance callback crosses once per matched entity.
The two hook families differ in scope. Both Update and PhysicsUpdate run
on the client as well as the server, but registered ECS systems run on the
server only, so a World.System callback never fires in a client script.
For a script author the practical consequence is ordering. Values written in Update reach the solver in the same tick, assuming the accumulator holds at
least one whole 1/60 s that tick. The write-back that ends the physics step
runs after Update has returned, so the earliest Update that observes an
integrated position is the next tick’s. The hooks themselves are documented in The Simulation Tick.
The fixed-step loop#
Real time does not arrive in 1/60 s pieces. A tick may take 11 ms or 25 ms, and if we handed that measured number to the solver as its delta time, the same stack of crates would settle differently depending on how busy the server was that second. So the measured time is added to an accumulator instead, and the loop runs one pass for every whole 1/60 s the accumulator holds, subtracting 1/60 s per pass. What is left over is under 1/60 s and stays in the accumulator for the next tick. A tick that ran long therefore runs the loop more than once, and a tick that arrived early does not run it at all. Nothing in that rule bounds how many passes one tick may run, and the passes themselves are part of the time the next tick measures, so a tick whose passes cost more real time than they consume raises the pass count of the tick after it.
Every pass runs the same stages in the same order. Queued player input is
converted into each character’s move intent. PhysicsUpdate(dt) runs on every
script instance with a constant dt = 1/60. The systems registered with the "Physics" phase run. Then the physics step itself closes the pass.
That order is why forces belong in PhysicsUpdate: it runs immediately before
integration, so a force applied there is still accumulated on the body when the
solver reads it, and the dt passed to it is the same number the solver uses.
The client runs the same loop against its own accumulator, so its prediction
integrates the same constant 1/60 s steps rather than a frame time. Matching
the server value for value would also require both machines to produce
identical floating-point results for the same operations, which the client does
not assume: it reconciles its predicted state against the authoritative
snapshot, described in Server Authority and Replication.
The limit an author hits here is that PhysicsUpdate and Update do not run
the same number of times. Update runs once per tick, while PhysicsUpdate runs once per pass, and the number of passes is the number of whole 1/60 s
intervals the accumulator held that tick, which can be zero. So per-frame
counters belong in Update and per-step forces belong in PhysicsUpdate.
What Box3D does in a substep#
The physics step includes more than the solver. It starts by flushing the
transforms scripts wrote during the pass. Writing Position or Orientation,
or calling Rotate, marks the entity dirty rather than moving its rigid body
at the point of assignment; the flush copies the transform onto the body at the
start of the next physics step, before the first substep. Read Position back
inside the same callback and you get the value you just assigned, because the
integrated transform only enters the component at the write-back stage that
ends the step. The step itself then runs in this order:
With the bodies up to date, characters evaluate their floor geometry and apply
their locomotion forces, which turns a move intent produced at the top of the
pass into forces the solver integrates. Box3D then advances the 1/60 s step as
4 sequential velocity and position substeps rather than one 1/60 s step. Each
substep solves against the positions the previous substep produced, so a box
resting on a box resting on the ground is solved four times with contact data
recomputed per substep instead of once. The solver is iterative, so this
reduces the residual penetration and constraint error one large step leaves
rather than producing an exact solution. Skeletal animations advance after the
solver, and the step ends by writing body transforms and linear and angular
velocities back into the Position, Rotation and Velocity components.
The substeps run on top of the broadphase. It stores an axis-aligned bounding box per active body and derives a collision pair list from those boxes; the detailed contact tests only run on pairs taken from that list. A static body does not move, so its bounding box stays valid and its pairs need no recalculation for as long as its motion type does not change.
The physics queries also avoid per-call heap allocation. Physics.OverlapBox and Physics.OverlapSphere write their matches into an internal fixed buffer
of up to 64 entity handles rather than allocating one per call, and each body
appears in it at most once. That buffer size is the cap: a query whose region
contains more than 64 distinct bodies returns 64 and drops the rest, and which
64 it keeps is not documented. The signatures are in The Simulation Tick.
For an author this means a transform you assign reaches the body at the flush stage of the next physics step rather than at assignment, and the value you read back before then is the one you wrote.
Sleeping and waking#
Most bodies in a world are at rest. A hundred crates that have not moved in ten
seconds would still cost contact solving in every substep, repeating the same
resolution each time. So the engine takes them out of that work: a body whose
linear speed drops below the World component’s SleepVelocityThreshold,
0.6 studs/s by default, goes to sleep and leaves the active solver set. The
comparison is against linear speed, and no angular term is documented, so a
body rotating in place with a linear speed under the threshold meets the sleep
condition.
The threshold is not stored per body. It is pushed into the physics world
during physics configuration, which runs after Update and before the first
pass, so a value written from Update takes effect in the same tick, and one
written later in the tick, from PhysicsUpdate for instance, takes effect in
the next. It is a property of the world, so it applies to every body at once.
A dynamic body moves between awake and sleeping like this:
Waking takes a write: an impulse, a velocity write, or a transform write
returns the body to the active set, and those are the wake paths the engine
documents. Physics.IsSleeping(e) reports the current state at any time.
A force accumulated on a sleeping body is not integrated while the body stays asleep, because integration happens in the solver work it has been taken out of. To move a sleeping body, wake it with an impulse or a velocity write first.
Anchored parts#
An Anchored part is a static body, and static bodies are the cheapest case
for the broadphase: their bounding boxes do not change, so their pairs are not
recalculated. A transform write from Lua breaks that condition. Rather than
reject the write, the engine promotes the static body to a kinematic one and
drives it toward the target transform. If no further write arrives for 8
consecutive frames, it demotes the body back to static; those are frames of the
tick loop rather than substeps, so the demotion lands about 0.13 s after the
last write, assuming the server holds its 60 Hz tick.
The motion type therefore follows the writes it receives:
Both transitions rebuild broadphase state. Promotion rebuilds the collision pair list for the bodies near the part, and the demotion 8 frames later rebuilds those pairs again. A part written once costs two rebuilds; a part written at least once every 8 frames does not demote, so it costs no further rebuild after the promotion; a part written in bursts costs two rebuilds per burst. Those counts are the rebuilds this one part triggers, assuming no other body near it changes motion type. Promotion is intended for one-off placement.
For continuous animation, set the motion type explicitly instead. Unanchoring the part and setting its motion type keeps it kinematic with no promotion or demotion:
part.Anchored = falsepart:SetBodyMotion("kinematic")Switching to "kinematic" zeroes the body’s linear and angular velocity, so a
part converted mid-flight keeps the transform it had at that moment and moves
only where later transform writes put it.
After physics#
Physics moved the bodies and the write-back stage copied their transforms into Position and Rotation, which hold coordinates local to the parent. So the
first thing the tail does is walk the parent-child hierarchy from the roots
down, recomputing each entity’s world transform from its parent’s already
recomputed one. This is why Position and Rotation read back as local
coordinates immediately after a reparent: the world transform for that entity
is not recomputed until this stage runs. Running it after integration also
means the values the tail is about to pack are the ones the solver just
produced, not last tick’s.
Event cleanup follows and clears this tick’s inbound EventTable rows. Rows
persist for exactly one tick, which is what makes an event table safe to read
with a plain loop: whatever arrived is present for the whole tick and cleared
at this stage, so the table does not grow across ticks and there is nothing to
unsubscribe.
The tick then processes welds, lifetimes and death notifications, and on every second tick packs all dirty state into a delta snapshot and transmits it to connected clients. That is where the 30 Hz replication rate comes from: not a separate timer, only the tail of every other 60 Hz tick. The wire format is covered in Server Authority and Replication.
For an author this bounds replication latency. A change made in Update is
packed by the tail of the same tick when that tick is a packing tick and by the
tail of the next one otherwise, so it waits at most one extra tick, about 17 ms
at 60 Hz. A change made after the tail has run this tick waits for the packing
after that. What Destroy does to an entity, and when its removal reaches
clients, is in Entity and Script Lifecycle.