Luduvo Contents
Theme
On this page

The Simulation Tick

The Luduvo server simulates the world at a fixed 60 Hz rate. Physics never steps with a variable delta time: every single step spans exactly 1/60 s, and the Box3D solver runs 4 substeps inside it to resolve contacts and overlapping constraints.

Replication runs at 30 Hz, which means every second simulation tick concludes by packing and broadcasting a network snapshot. On the other end of the wire, the client renders frames as fast as the display hardware allows, feeding a physics accumulator that steps the local simulation at the exact same fixed 1/60 s cadence.

How the engine runs that loop inside: The Server Tick and Physics.

Order of one server tick#

During each simulation tick, the server executes the following stages in strict sequential order:

Scripts and gameplayPhysics and tail1 Script loadingPending scripts load2 Update hookFrame delta per instance3 Default systemsWorld.System Default phase4 Player joinsFires Session.PlayerJoined5 Death verificationHealth, height, transform6 Physics configurationGravity, sleep, collisions7 Physics stepPhysicsUpdate, then Box3DFixed 1/60 s, 4 substeps8 Hierarchy updateTransforms down the tree9 Event cleanupEventTable rows cleared10 Lifecycle and networkingWelds, lifetimes, delta

The tick opens by loading all pending scripts into the runtime. Update(dt) then runs across every active script instance, where dt represents the frame delta, followed by the systems registered with World.System(name, query, fn, "Default"). Next the engine processes players who joined since the last tick and fires their Session.PlayerJoined events, then inspects characters for lethal conditions: health dropping to zero, falling below Physics.FallenPartsDestroyHeight, or an invalid or broken transform. Just before physics, the engine pushes Physics.Gravity, the World component’s SleepVelocityThreshold, and the collision matrix down into the active physics world.

The fixed-step loop runs whenever the accumulator holds at least 1/60 s of accumulated time. Each pass converts queued player input into each character’s move intent, runs PhysicsUpdate(dt) on every script instance with a constant dt = 1/60, and executes the systems registered with the "Physics" phase. The physics step that closes the pass applies any transforms written by scripts to their bodies, has characters evaluate their floor geometry and apply locomotion forces, and lets Box3D advance through its 4 substeps to resolve contacts and constraints. Skeletal animations advance, and the engine writes the resulting body transforms and linear and angular velocities back to the Position, Rotation, and Velocity components.

The tail of the tick recomputes world-space transforms down through the parent-child hierarchy, clears the current tick’s inbound EventTable rows, and processes welds, lifetimes, and death notifications. On every second tick, the server packs all dirty state into a delta snapshot and transmits it to connected clients.

Only the server executes World.System callbacks. While client scripts receive both Update and PhysicsUpdate, registered ECS systems do not execute on the client.

Where to put work#

When structuring game code, choose execution hooks intentionally:

  • Use Update for high-level game logic that does not depend on a rigid fixed rate: spawning items, awarding points, or reacting to incoming EventTable rows.
  • Use PhysicsUpdate or a "Physics" phase system to apply forces, steer bodies, or calculate motion. Because this callback runs once per physics step (immediately prior to solver integration), it guarantees a constant, predictable dt.

To process many entities of the same type, register an ECS system rather than attaching individual per-instance script callbacks. A query hands the system parallel arrays (q.count, q.Entity[i], q.Position[i]), so one loop visits every pickup instead of the engine entering a script environment once per entity:

Luau
function Update(dt)	for i = 1, ToolEquip.count do		applyEquip(ToolEquip.sender[i], ToolEquip.slot[i])	endendlocal kPickupHalf = Vector3.new(1.5, 1.5, 1.5)local pickups = game.World.Query("Tool", "Position")game.World.System("pickups", pickups, function(q)	for i = 1, q.count do		local n, hits = game.Physics.OverlapBox(q.Position[i], kPickupHalf)		for h = 1, n do			tryPickup(q.Entity[i], hits[h])		end	endend, "Physics")

Transforms and bodies#

Writing to Position, Orientation, or invoking Rotate marks that entity dirty. The physics body picks the value up at the start of the next physics step - not at the exact moment of assignment. Reading Position back within the same callback returns the freshly assigned value; the simulated position only becomes readable once that step finishes.

Moving an Anchored part from Lua temporarily promotes the static body to a kinematic body and drives it toward the target transform. If no further writes occur for 8 consecutive frames, the engine demotes it back to a static body. Treat this behavior strictly as a convenience for one-off placement, never as a mechanism for continuous animation.

script writes transform8 frames without writesStaticKinematic

To animate a part frame-by-frame, unanchor the part once and set its motion type to kinematic with SetBodyMotion("kinematic"):

Luau
part.Anchored = falsepart:SetBodyMotion("kinematic")

Forces, impulses, and velocity#

Three primary methods manipulate dynamic bodies directly:

CallWhat the step does with it
ApplyForce(force, at?)Accumulates force to integrate during the upcoming physics step (cleared afterward), and torque when world-space position at is specified. Call this inside every PhysicsUpdate to maintain continuous thrust.
ApplyImpulse(impulse, at?)Changes linear velocity instantaneously by dv = impulse / mass, and applies angular impulse when world-space position at is specified.
SetLinearVelocity(v)Directly overwrites linear velocity; this new velocity persists until the physics solver or a subsequent script call alters it.

All three methods operate exclusively on dynamic bodies. Invoking them on an Anchored part or a body switched to "kinematic" via SetBodyMotion causes the engine to ignore the call and emit a warning to the console. Calling SetBodyMotion("kinematic") also instantly zeroes out both linear and angular velocities.

Sleep states#

To conserve processing power, any body whose linear speed drops below the World component’s SleepVelocityThreshold (defaulting to 0.6 studs/s) enters a sleeping state, removing itself from active solver calculations. Check this state at any time with Physics.IsSleeping(e). Applying an impulse, overwriting velocity, or shifting the body’s transform wakes it back up.

speed below sleep thresholdimpulse, velocity or transformAwakeSleeping

Spatial queries#

The physics engine provides several query methods to probe the environment:

  • Physics.Raycast(origin, dir, maxDist, ignore?) casts a ray into the scene and returns the closest intersection as { Position, Normal, Entity, Distance }, or nil if nothing was struck. The optional ignore parameter excludes the specified entity, its child hierarchy, and any attached welds.
  • Physics.OverlapSphere, Physics.OverlapBox, and Physics.Shapecast return the tuple count, hits. Each body appears at most once, capped at 64 hits.
  • Shapecast sweeps a "Box", "Sphere", or "Capsule" through space and returns hits sorted nearest-first, with each entry formatted as { Entity, Point, Normal, Fraction }.

Two colliders only interact if the collision matrix enables contact between their respective groups. On the server, configure this matrix using Physics.RegisterCollisionGroup and Physics.SetCollisionRule, while the CollisionGroup property on an entity assigns its active group.

Execution budgets and profiling#

To prevent rogue scripts from hanging the simulation, every callback, system phase, and signal handler runs under a hard limit of 1,000,000 VM instructions. If execution exceeds this threshold, the runtime halts the call and raises an error: script exceeded its instruction budget. If an instance errors 5 times in succession inside Update, PhysicsUpdate, or an ECS system, the engine disables that script instance until the place reloads.

Inspect performance using the built-in profiler:

  • game.Profiler.Frame() returns a FrameStats table holding durations in seconds across sim, script, physics, network, and render stages, alongside Lua GC memory metrics.
  • game.Profiler.Scripts() returns an array of ScriptCost records listing call counts and average execution duration per call (avg) in seconds.