Replicating
Once the replicators are set up, replication has two parts:
- A system running in the server that collects what changed and sends it to every player.
- Activating each player when they join, by sending them the full state of the world.
Both parts live in the same system. A player is activated right before the updates are collected, this page explains why.
Sending Updates
Section titled “Sending Updates”Changes should be sent inside a system that runs every frame. Its recommended that you throttle the updates to a lower interval, a good rate is somewhere around 10-30 times a second.
In the server, you will call server_replicator:collect_updates().
This will return an iterator that will be executed for every player.
The iterator will give you the following values:
- player:
Player- the player to send the updates to - buf:
buffer- the buffer containing the updates - variants:
{ {any} }- the variants tables containing non-serialized values
Updates can then be applied in the client using client_replicator:apply_updates(buf, variants).
Example:
Section titled “Example:”local replicator = require("@server/replicator")local interval = require("@utils/interval")local system = require("@utils/scheduler/system")
-- interval throttles systems to a specific intervallocal updates_interval = interval(1 / 20)
function replecs_server() -- will send updates 20 times a second if updates_interval() then for player, buf, variants in replicator:collect_updates() do remotes_server.send_updates:fire(player, buf, variants) end endend
system(replecs_server)local replicator = require("@client/replicator")local collect = require("@utils/collect")local system = require("@utils/scheduler/system")
-- collect converts events into iteratorslocal updates = collect(remotes_client.send_updates)
-- system running every framefunction replecs_client() for buf, variants in updates do replicator:apply_updates(buf, variants) endend
system(replecs_client)Sending Unreliable Values
Section titled “Sending Unreliable Values”Unreliable updates are sent similarly to normal updates. You can call server_replicator:collect_unreliable().
This will return an iterator that will be executed for every player.
The iterator will give you the same values as normal updates.
Updates can then be applied in the client using client_replicator:apply_unreliable(buf, variants).
Because unreliable values are always sent, deciding how often you send them is crucial, making it slower to not overload the network or faster for smoother replication.
Activating Players
Section titled “Activating Players”Replecs ignores every player that joins the game until you activate them, even if a filter names them explicitly. This prevents players from getting updates when they haven’t received the full state of the world yet.
Activating a player takes two calls, in this order:
server_replicator:mark_player_ready(player)activates the player.server_replicator:get_full(player)returns the current state of the world for that player, as abufferand avariantstable with the non-serialized values.
The client applies the result with client_replicator:apply_full(buf, variants).
You can check if a player has been activated with server_replicator:is_player_ready(player).
Where to call get_full
Section titled “Where to call get_full”Both calls must happen in the same frame as collect_updates(), right before it, with no world changes in between.
Remote callbacks run at arbitrary points of the frame, so calling get_full directly inside one leaves room
for your systems to change the world before the updates are collected. Instead, defer the work to the replication system.
There are two ways to do this:
Queueing the player
Section titled “Queueing the player”The client requests the full state with a remote event. The server only queues the player, and the replication system activates the queued players right before collecting the updates.
local replicator = require("@server/replicator")local interval = require("@utils/interval")local system = require("@utils/scheduler/system")
local updates_interval = interval(1 / 20)
-- players waiting for the full state of the worldlocal pending_players: { Player } = {}
remotes_server.request_full:on(function(player) table.insert(pending_players, player)end)
function replecs_server() if updates_interval() then -- activate pending players right before collecting updates for _, player in pending_players do if not player.Parent then continue -- left before we got here end replicator:mark_player_ready(player) local buf, variants = replicator:get_full(player) remotes_server.receive_full:fire(player, buf, variants) end table.clear(pending_players)
for player, buf, variants in replicator:collect_updates() do remotes_server.send_updates:fire(player, buf, variants) end endend
system(replecs_server)local replicator = require("@client/replicator")local collect = require("@utils/collect")local system = require("@utils/scheduler/system")
local updates = collect(remotes_client.send_updates)
-- the full state is applied as soon as it arrives,-- updates are queued and applied by the system afterwardsremotes_client.receive_full:on(function(buf, variants) replicator:apply_full(buf, variants)end)
-- ask the server for the full state of the worldremotes_client.request_full:fire()
function replecs_client() for buf, variants in updates do replicator:apply_updates(buf, variants) endend
system(replecs_client)Queueing a coroutine
Section titled “Queueing a coroutine”If you prefer a remote function, the callback can yield its coroutine and let the replication system resume it
right before collecting the updates. The callback then calls get_full from inside the system and returns the result.
local replicator = require("@server/replicator")local interval = require("@utils/interval")local system = require("@utils/scheduler/system")
local updates_interval = interval(1 / 20)
-- callbacks waiting to be resumed by the systemlocal pending_threads: { thread } = {}
remotes_server.receive_full:set_callback(function(player) -- yield until the replication system resumes us table.insert(pending_threads, coroutine.running()) coroutine.yield()
if not player.Parent then return -- left while waiting end replicator:mark_player_ready(player) return replicator:get_full(player)end)
function replecs_server() if updates_interval() then -- resume the callbacks right before collecting updates local threads = table.clone(pending_threads) table.clear(pending_threads) for _, thread in threads do coroutine.resume(thread) end
for player, buf, variants in replicator:collect_updates() do remotes_server.send_updates:fire(player, buf, variants) end endend
system(replecs_server)local replicator = require("@client/replicator")local collect = require("@utils/collect")local system = require("@utils/scheduler/system")
local updates = collect(remotes_client.send_updates)
-- yields until the server resumes the callback inside its systemlocal buf, variants = remotes_client.receive_full:invoke_server()replicator:apply_full(buf, variants)
function replecs_client() for buf, variants in updates do replicator:apply_updates(buf, variants) endend
system(replecs_client)A few things to keep in mind:
- In both cases the full state is sent before the updates of that same frame, so the client receives them in order. Make sure the full state is applied before any update packet.
- Mark the player as ready before calling
get_full. Non-ready players are always excluded from replication so calling it on a player that is not ready logs a warning and returns an empty state. - Make sure the player is also marked as ready just right before it gets full.
Summary
Section titled “Summary”Combining updates, unreliable values and player activation, your client/server systems should look something like this:
local replicator = require("@server/replicator")local interval = require("@utils/interval")local system = require("@utils/scheduler/system")
local updates_interval = interval(1 / 20)local unreliables_interval = interval(1 / 30)
local pending_players: { Player } = {}
remotes_server.request_full:on(function(player) table.insert(pending_players, player)end)
function replecs_server() -- will send updates 20 times a second if updates_interval() then -- new players get the full state right before the updates are collected for _, player in pending_players do if not player.Parent then continue end replicator:mark_player_ready(player) local buf, variants = replicator:get_full(player) remotes_server.receive_full:fire(player, buf, variants) end table.clear(pending_players)
for player, buf, variants in replicator:collect_updates() do remotes_server.send_updates:fire(player, buf, variants) end end
-- will send unreliables 30 times a second if unreliables_interval() then for player, buf, variants in replicator:collect_unreliable() do remotes_server.send_unreliables:fire(player, buf, variants) end endend
system(replecs_server)local replicator = require("@client/replicator")local collect = require("@utils/collect")local system = require("@utils/scheduler/system")
local updates = collect(remotes_client.send_updates)local unreliables = collect(remotes_client.send_unreliables)
remotes_client.receive_full:on(function(buf, variants) replicator:apply_full(buf, variants)end)remotes_client.request_full:fire()
function replecs_client() for buf, variants in updates do replicator:apply_updates(buf, variants) end for buf, variants in unreliables do replicator:apply_unreliable(buf, variants) endend
system(replecs_client)