Prop: Dice Roller
TL;DR: A 6-sided die prop (sides configurable). Only the spawner can roll; results broadcast over Reliable, and every viewer accepts only messages whose sender matches the prop’s current spawner. Demonstrates prop-context ownership via WasmUtils.GetOwnerContext(), spawner-authoritative networking, and accepting inbound sync with sender verification.
Context: Prop. LocalPlayer.* is unavailable; ownership is context-based.
Scene setup (prop root)
Section titled “Scene setup (prop root)”- A prop root GameObject with a
DiceRollercomponent. - A
TextMeshProUGUIchild forfaceLabel(renders the rolled number). - An optional
statusLabelTMP for feedback. - A
Button(or CCK interactable) whose click invokesRoll(). The[ExternallyVisible]attribute marks the method as a valid UnityEvent target for forward-compat — current CCK accepts any public method, but future versions may require the attribute (see Unity Events Rewiring).
Code highlights
Section titled “Code highlights”[ExternallyVisible]public void Roll(){ if (!IsSpawner()) { SetStatus("only the spawner can roll"); return; } int face = UnityEngine.Random.Range(1, Mathf.Max(2, sides + 1)); ApplyResult(face); Broadcast(face);}
private void HandleMessage(Player sender, Span<byte> message){ // ... read tag ... Prop current = Prop.GetCurrentProp(); Player spawner = current?.GetSpawner(); if (spawner == null || sender != spawner) return; // reject non-spawner senders // ... read face and apply ...}
private bool IsSpawner(){ return WasmUtils.GetOwnerContext() == CVRScriptOwnerContext.Self;}Source: examples/prop/DiceRoller.cs.
Why spawner-authoritative
Section titled “Why spawner-authoritative”A prop lives in every viewer’s scene, but only the spawner should be able to change its state. If every client could roll independently, everyone would see different numbers. Restricting rolls to the spawner + broadcasting gives everyone a consistent view, and verifying the sender on receive stops any player from faking a roll on another’s prop.
Late viewer re-sync
Section titled “Late viewer re-sync”When a new player joins, they see the prop spawn via their own OnPropSpawned. If the spawner’s side has already rolled, that message might have landed before the new viewer joined — they’d see the default. To fix, the spawner re-broadcasts on OnPropSpawned:
public void OnPropSpawned(Prop prop){ if (IsSpawner() && lastRoll > 0) Broadcast(lastRoll);}OnPropSpawned fires on all viewers whenever any prop spawns — the spawner filters to their own prop via IsSpawner() and rebroadcasts the last-known state. Cheap because it’s once per join event.
- Use
WasmUtils.GetOwnerContext()for prop ownership checks. Works from prop context without any world-only calls. - Verify sender against
Prop.GetCurrentProp().GetSpawner()on inboundMSG_RESULT. Prevents cross-prop spoofing. - Use
Reliablefor discrete events like dice rolls; a lost roll is noticeable. - Cache
Prop.GetCurrentProp()inStartif you call it often — it’s a host round-trip per call. The example re-reads it because the rate is low. - Mark UI-invocable methods with
[ExternallyVisible]to stay forward-compatible with the planned attribute enforcement.
Don’ts
Section titled “Don’ts”- Don’t call
LocalPlayer.PlayerObjectfrom a prop script. World-only; throws. - Don’t call
prop.Destroy()from inside the prop’s own script. That binding is world-only; a prop wanting to self-destruct should network a “destroy me” request to a world script that then invokes destruction. - Don’t broadcast on every
Update. Dice-roll events are user-driven; broadcast once per roll. - Don’t skip sender verification for state-mutating messages. Without it, any player can fake a roll on your prop.
- Don’t forget
Random.Range’s inclusive/exclusive behaviour —Random.Range(1, 7)returns 1–6 inclusive for int overloads (min inclusive, max exclusive). The example usessides + 1accordingly.
Validator
Section titled “Validator”Paste DiceRoller.cs into the validator and select Prop context. Expect:
err 0,warn 0.inforeportingUnity events detected: Start, OnDestroyandGame events detected: OnPropSpawned.- Switch the context to World — still clean; the script avoids any prop-only assumptions.
Extensions
Section titled “Extensions”- Multi-die roller — N dice in one roll; broadcast a fixed-size
byte[]of results. - Physics die — replace the on-click roll with a physics-driven die + face detection on
OnCollisionExit. - Visual flip animation — interpolate the face label over 200 ms before settling on the result to sell the roll.
- Roll history — keep the last 10 results in a ring buffer; display as a secondary text.
Full source
Section titled “Full source”using System;using TMPro;using UnityEngine;using WasmScripting;using CVR;
public partial class DiceRoller : WasmBehaviour{ public TextMeshProUGUI faceLabel; public TextMeshProUGUI statusLabel; public int sides = 6;
[WasmSerialized] private int lastRoll = 0;
private const byte MSG_RESULT = 1;
void Start() { Networking.OnReceiveMessage += HandleMessage; Refresh(); } void OnDestroy() { Networking.OnReceiveMessage -= HandleMessage; }
// Called from a Unity Button.onClick on the prop. [ExternallyVisible] public void Roll() { if (!IsSpawner()) { SetStatus("only the spawner can roll"); return; } int face = UnityEngine.Random.Range(1, Mathf.Max(2, sides + 1)); ApplyResult(face); Broadcast(face); }
public void OnPropSpawned(Prop prop) { // When a prop spawns on someone else's client, they already have the default state. // The spawner's side re-broadcasts so late viewers sync. if (IsSpawner() && lastRoll > 0) Broadcast(lastRoll); }
private void ApplyResult(int face) { lastRoll = face; Refresh(); SetStatus($"rolled {face}"); }
private void Broadcast(int face) { var w = new BufferReaderWriter(8); w.Write(MSG_RESULT); w.Write(face); Networking.SendMessage(w.Buffer.Slice(0, w.Length), null, SendType.Reliable); }
private void HandleMessage(Player sender, Span<byte> message) { if (message.Length < 1 || sender == null) return; var r = new BufferReaderWriter(message); r.Read(out byte tag); if (tag != MSG_RESULT) return;
// Accept results only from the prop's spawner. Prop current = Prop.GetCurrentProp(); if (current == null) return; Player spawner = current.GetSpawner(); if (spawner == null || sender != spawner) return;
r.Read(out int face); if (face < 1 || face > sides) return; ApplyResult(face); }
private bool IsSpawner() { // Ownership context is "Self" on the spawner's client for prop VMs. return WasmUtils.GetOwnerContext() == CVRScriptOwnerContext.Self; }
private void Refresh() { if (faceLabel != null) faceLabel.text = lastRoll > 0 ? $"{lastRoll}" : "-"; } private void SetStatus(string s) { if (statusLabel != null) statusLabel.text = s; }}Related
Section titled “Related”- Prop context — capability overview.
- Events → Prop events —
OnPropSpawnedfull signature. - Networking —
SendTypeand addressing.