Luduvo Contents
Theme
On this page

Events

A script that runs once at startup cannot react to anything. Scripts have to observe user input, physics collisions, clock ticks, and incoming network packets while the simulation runs.

Luduvo offers three event mechanisms:

  1. Lifecycle functions: callbacks the engine invokes each frame or each physics step.
  2. Signals: in-process dispatchers between scripts on the same machine.
  3. EventTables: typed tables that carry structured rows across the client-server boundary.

How the engine dispatches and ships these inside: Replication and Events.

Signals compared to EventTables#

Signals stay inside one process; EventTables cross the network. The two differ in every other respect as well:

DimensionSignalEventTable
Execution scopeIn-process only (never crosses the network)Inter-process (Client to Server)
Delivery timingImmediate synchronous dispatch within the tickQueued into network packet, delivered on the next tick
Data structureCallback functions you connectTyped columns you read by row index
Payload constraintsIn-memory Luau tables (max 16 deep, 4096 nodes)Fixed schema types (U8, I32, F32, Vec3, etc.)
LifecyclePermanent until disconnected or instance destroyedTransient: rows live for one tick, cleared at tick end

Lifecycle functions#

Lifecycle functions are specialized callbacks defined at the top level of any script. The engine’s simulation loop automatically picks them up and invokes them:

Luau
-- Runs once every rendered frame (variable delta dt)function Update(dt)	self.Rotation = self.Rotation + Vector3.new(0, dt * 90, 0)end-- Runs once every fixed 1/60 s physics step (constant dt = 1/60)function PhysicsUpdate(dt)	self:ApplyForce(Vector3.new(0, 50, 0))end

Choosing between Update and PhysicsUpdate#

  • Update(dt): The engine invokes this during the logic phase of the simulation tick. Use Update for general gameplay logic, UI updates, timers, camera interpolation, and processing rows that arrived via EventTable.
  • PhysicsUpdate(dt): The engine invokes this callback immediately before Box3D steps the physical world using a fixed time step (constant dt = 1/60 s). When applying forces (ApplyForce), setting linear velocities, or calculating character motor physics, place that logic inside PhysicsUpdate for deterministic physical simulations.

The Simulation Tick shows where these callbacks slot into the frame loop.

Signals#

A Signal is an in-process publish-subscribe channel. When an event fires, the signal walks its list of callback closures and invokes them synchronously, in place:

Luau
-- Connect a callback to a GUI button's Activated signal (client only)button.Activated:Connect(function()	print("Button clicked by local player!")	-- Emit a custom named signal on this instance	self:GetSignal("DoorTriggered"):Emit("PlayerApproached")end)-- Connect to a service-level signal (server only)game.Session.PlayerJoined:Connect(function(userId)	print("Player joined the server:", userId)end)

Key characteristics of Signals#

  • In-process only: Signals never cross the network wire. Emitting a signal on the client fires callbacks exclusively on that local machine; emitting a signal on the server fires callbacks strictly on the server.
  • Custom signals via GetSignal: instance:GetSignal(name) returns a named Signal object. Attach listeners with signal:Connect(fn) and broadcast events by calling signal:Emit(...).
  • Payload constraints: Arguments to Emit must be pure data. Functions, coroutine threads, and raw engine userdata are rejected. Tables cannot nest deeper than 16 levels or hold more than 4,096 nodes, which keeps a bad payload from overflowing the stack.

EventTables#

Client and server run as separate programs, usually on separate machines across the internet. Neither side can call the other’s functions or fire a signal across the wire. EventTables carry events between them.

Declaring a table#

An EventTable carries rows of a fixed shape. Declaring the table fixes its fields and their types up front, and every row pushed through it carries exactly those fields:

Luau
local ET = game.EventTable-- Define an EventTable named "ToolEquip" directed from Client to Server-- Schema fields: slot (U8)local ToolEquip = ET("ToolEquip", ET.ToServer, {	{ "slot", ET.U8 }})

Column types include F32, I32, U8, Bool, Vec3, Color, Entity, and String.

Client-to-server replication flow#

To push an action from the client to the server, invoke Push:

Luau
-- Hotbar.client.lua (Runs on client)slotButton.Activated:Connect(function()	-- Stages a row for the next replication tick	ToolEquip:Push(selectedSlotIndex)end)

The server drains the batched rows in its logic update:

Luau
-- Tools.server.lua (Runs on server)function Update(dt)	-- Process all rows that arrived this simulation tick	for i = 1, ToolEquip.count do		local senderId = ToolEquip.sender[i]		local slot = ToolEquip.slot[i]		applyToolEquip(senderId, slot)	endend

Instead of firing one callback per event, the server handles every row that arrived in a single loop.

The network tick lifecycle#

A row travels through the pipeline:

Push stages the rowSits on the sending sidePacked at the tickReplication runs at 30 HzUpdate drains the rowscount, sender, field columnsCleared at tick endNothing reaches the next tick

Calling ToolEquip:Push(...) stages the row on the sending side; nothing leaves the machine at that moment. At the 30 Hz network replication tick, the engine packs every staged row into the outgoing snapshot and sends it.

The rows surface in the next Update(dt) on the receiving side. ToolEquip.count reports exactly how many rows arrived, and each field is a column read by row index: ToolEquip.slot[i], ToolEquip.sender[i]. The rows are transient: they exist only for the duration of the tick in which they arrive, and the engine clears them at the end of that tick. A row that no Update reads in that tick is gone.

Directionality: ToServer vs. ToClients#

  • ET.ToServer: Pushed by clients and processed on the server. On the server side, the engine automatically injects the authenticated player’s userId into table.sender[i]. Because the engine assigns this value authoritatively, clients can never spoof or tamper with the sender column.
  • ET.ToClients: Pushed by the server and received by clients. The server can broadcast to all connected clients using table:Push(...), or target an individual player via table:PushTo(userId, ...).

Network limits and guardrails#

The engine caps every EventTable so that traffic stays predictable and a hostile client cannot flood a server:

  • Max 16 fields per table schema.
  • Max 256 bytes per individual serialized row.
  • Max 64 rows per table per tick.
  • Max 64 EventTables registered per place.
  • Bandwidth guardrail: A client that pushes more than 16 KiB of event rows in a single tick, or sends a malformed wire buffer, gets disconnected immediately.

Server Authority and Replication covers client prediction and state synchronization.