06 — Networked Counter
TL;DR: Extends the Click Counter to share state across all players in the instance. The instance owner holds authority: their client is the only one that mutates count and broadcasts updates. Non-owners receive Reliable messages and update their local display. Demonstrates Networking.SendMessage, Networking.OnReceiveMessage, BufferReaderWriter packing, and OnInstanceOwnerChange handoff.
Scene setup (World context)
Section titled “Scene setup (World context)”Exactly the same as 02 — Click Counter: Canvas + Button(s) + TextMeshProUGUI for the display. Wire the buttons to OnClicked and OnResetClicked. Add the NetworkedCounter component instead of ClickCounter.
using System;using UnityEngine;using TMPro;using WasmScripting;using CVR;
public partial class NetworkedCounter : WasmBehaviour{ public TextMeshProUGUI display; [WasmSerialized] private int count;
private const byte MSG_COUNT = 1;
void Start() { Networking.OnReceiveMessage += HandleMessage; Refresh(); }
void OnDestroy() { Networking.OnReceiveMessage -= HandleMessage; }
[ExternallyVisible] public void OnClicked() { if (!IsInstanceOwner()) return; count++; Broadcast(); Refresh(); }
[ExternallyVisible] public void OnResetClicked() { if (!IsInstanceOwner()) return; count = 0; Broadcast(); Refresh(); }
public void OnInstanceOwnerChange(Player newOwner) { if (!IsInstanceOwner()) return; Broadcast(); }
private static bool IsInstanceOwner() { Player owner = Networking.GetInstanceOwner(); Player me = LocalPlayer.PlayerObject; return owner != null && me != null && owner == me; }
private void Broadcast() { var w = new BufferReaderWriter(8); w.Write(MSG_COUNT); w.Write(count); Networking.SendMessage(w.Buffer.Slice(0, w.Length), playerIds: null, SendType.Reliable); }
private void HandleMessage(Player sender, Span<byte> message) { if (message.Length < 1) return; var r = new BufferReaderWriter(message); r.Read(out byte tag); if (tag != MSG_COUNT) return; r.Read(out int newCount);
// Only accept from the current instance owner. Player owner = Networking.GetInstanceOwner(); if (owner == null || sender == null || sender != owner) return;
count = newCount; Refresh(); }
private void Refresh() { if (display != null) display.text = $"Shared clicks: {count}"; }}Source file: examples/06_NetworkedCounter.cs.
The authority pattern
Section titled “The authority pattern”The two hardest bugs in networked scripts are:
- Everyone mutates state → conflicts, divergence, or last-writer-wins races.
- Non-authoritative clients broadcast noise → someone trolls by spamming messages that other clients happily accept.
This script avoids both by making the instance owner (a property of the CVR instance, not the script) the sole writer:
- Local writes are gated —
OnClicked/OnResetClickedearly-return unlessIsInstanceOwner(). Clicking on a non-owner’s client does nothing. - Incoming messages are verified —
HandleMessagedrops anything whosesenderis not the currentNetworking.GetInstanceOwner(). - Handoff is resilient — when
OnInstanceOwnerChangefires (the owner leaves and authority transfers), the new owner re-broadcasts the current state so late-joiners and any client that missed an update synchronize.
Non-owners never touch count except in response to an accepted HandleMessage. They can click all they want — nothing changes locally until the owner’s click round-trips.
Serialization with BufferReaderWriter
Section titled “Serialization with BufferReaderWriter”Instead of JSON strings, the wire format is:
[1 byte tag][4 bytes int count]Tiny, deterministic, and costs exactly one guest-side allocation per send (the buffer itself). See BufferReaderWriter.
The tag byte lets you grow the protocol without breaking parsers — future messages can define MSG_RESET = 2, MSG_SNAPSHOT = 3, etc., and the handler can ignore unknown tags gracefully.
SendType choice
Section titled “SendType choice”- Button clicks are discrete events —
SendType.Reliableensures every non-owner sees every increment in order. Latency on the reliable channel is modestly higher than unreliable, but still under typical interactive thresholds. - For continuous streams (player position every frame), you’d use
SendType.UnreliableorUnreliableSequencedwith a timestamp.
Permission model
Section titled “Permission model”Networking.SendMessage,GetInstanceOwner,OnReceiveMessage— no Object/Owner/Scope restrictions. Available from any content context.LocalPlayer.PlayerObject— world-only. The script has to be a world script to use this pattern (or a prop script that borrows a world-permissioned helper; not shown).
Extensions to try
Section titled “Extensions to try”- Per-player counters — switch from one shared counter to a
Dictionary<short, int>keyed byPlayer.GetNetworkId(). Each player broadcasts their own updates; no owner-authority required. - Late join snapshot — on
OnPlayerJoined(Player player), the owner sends a freshMSG_COUNTtonew short[] { player.GetNetworkId() }so the joining player sees the current state immediately. - Throttled broadcasts — add a
Time.time-based debounce so rapid clicks coalesce into fewer messages. - Optimistic UI — let non-owners increment locally and send an “I clicked” request; the owner adjudicates and broadcasts the authoritative value. More complex; only worth it if round-trip latency hurts UX.
Related
Section titled “Related”- Networking — send types, MTU probes, addressing.
- BufferReaderWriter — packing format.
- Events —
OnInstanceOwnerChangeand theNetworking.OnReceiveMessagedelegate.