Streaming state
Anything the server moves and every client draws – a creature, a platform, an arm reaching across the
map – ends up with the same code around an OrderedUnreliable event. This guide shows that code, and
what is left of it once the event is a stream.
Before
Section titled “Before”The schema carries the timestamp as a field of its own:
struct Head { Id: u8, At: vector<f24>, Back: boolean}
struct HeadPacket { Stamp: u16, Heads: Head[..16]}
event Heads { From: Server, Type: OrderedUnreliable, Call: SingleSync, Data: HeadPacket}And the server keeps a clock, a copy of what it last sent, and the rules for when to send:
local PUBLISH, PUBLISH_FAST, STILL, KEEPALIVE = 1 / 10, 1 / 20, 0.05, 1local clock, sinceSent, urgent, afterMotion = 0, 0, false, falselocal sent: { [number]: Vector3 } = {}local sentCount = 0
RunService.Heartbeat:Connect(function(dt) local heads = collectHeads() if #heads == 0 then if sentCount > 0 then Net.Heads.FireAll({ Stamp = stamp(), Heads = {} }) -- once, when the last one goes end sentCount, clock, sinceSent = 0, 0, 0 return end clock += dt sinceSent += dt if not urgent and clock < (if fast then PUBLISH_FAST else PUBLISH) then return end local forced = urgent clock, urgent = 0, false local changed, moved = compare(heads, sent, STILL) -- count, positions, flags if not (forced or changed or afterMotion or sinceSent >= KEEPALIVE) then return end afterMotion, sinceSent = moved, 0 remember(heads, sent) Net.Heads.FireAll({ Stamp = math.floor(Workspace:GetServerTimeNow() * 1000) % 65536, Heads = heads })end)The client undoes the stamp’s wrap before it can use it:
Net.Heads.On(function(packet) local now = Workspace:GetServerTimeNow() * 1000 local time = now - now % 65536 + packet.Stamp if time > now + 32768 then time -= 65536 elseif time < now - 32768 then time += 65536 end push(packet.Heads, time / 1000)end)The stamp leaves the schema and the settings take its place:
struct Head { Id: u8, At: vector<f24>, Back: boolean}
event Heads { From: Server, Type: OrderedUnreliable, Call: SingleSync, Stream: { Rate: 10, Fast: 20, Keepalive: 1, Epsilon: 0.05 }, Data: Head[..16]}The server says what the state is, whenever it changes or once a frame, and nothing else:
RunService.Heartbeat:Connect(function() local heads = collectHeads() if #heads == 0 then Net.Heads.Clear() else Net.Heads.Set(heads) endend)
-- where the game used to raise its own flagNet.Heads.SetFast(lunging)Net.Heads.Urgent()And the client receives the time already in seconds:
Net.Heads.On(function(heads, serverTime) push(heads or {}, serverTime)end)A packet is one byte smaller: 4 bytes of header – the index, a byte of presence bits, the stamp – where the hand-made version had 3 and the stamp inside the struct. A stream carries no sequence number: the stamp orders it. Its one extra byte is the presence bit that tells a cleared stream from a state.
What changes, exactly
Section titled “What changes, exactly”The stream follows the hand-made rules closely, and differs from the version above in four places, each on purpose:
- Unchanged means within
Epsilonper component, wherecomparemeasured each head’s distance moved. A head moving diagonally counts as moved slightly later with the stream (atEpsilonon one axis rather thanEpsilonof distance); pickEpsilona little smaller to match. - Any change is followed by one unchanged send, where
afterMotionfollowed only a head that moved. A flag that flips without a move costs one packet more. - The clock keeps what it overran by, where
clock = 0threw it away, and a step within a millionth of a second of the interval counts as due. Six frames of 1/60 add to a hair under 0.1, so at 60 frames a second the hand-made loop sent every seventh frame, 8.6 times a second; the stream sends every sixth, ten. - An emptied set is
Clear, which the client receives as nil, sent once.Set({})is a state like any other – sent, then kept alive – which is right for a list that is empty for now rather than gone.
