Clicker Game
TL;DR: A button increments a counter; every time the counter crosses a configured threshold, the matching GameObject becomes visible (a cosmetic pedestal, a decoration, a door). Pure local state, no networking — every player has their own count. Demonstrates [WasmSerialized] state, inspector-configured tier table, and HUD updates gated on state change.
Context: World. Plays fine in single-player; remote players each get their own counter because nothing is synced.
Scene setup
Section titled “Scene setup”- World Canvas with two buttons: “Click” (
onClick→OnClicked) and “Reset” (onClick→OnResetClicked). - Two
TextMeshProUGUIs:countLabelandhintLabel. - A
Clickercomponent on the world root with the two TMP fields wired and atiers[]array filled in the inspector. Each tier points at aGameObjectto toggle and carries a threshold + human-readable label. - Start the tier GameObjects inactive; the script enables them as the count crosses their thresholds.
public partial class Clicker : WasmBehaviour{ [System.Serializable] public struct Tier { public int threshold; public GameObject unlock; public string label; }
public TextMeshProUGUI countLabel; public TextMeshProUGUI hintLabel; public Tier[] tiers;
[WasmSerialized] private int count; [WasmSerialized] private int unlockedIndex = -1;
void Start() { ApplyTiers(force: true); RefreshCountLabel(); RefreshHintLabel(); }
public void OnClicked() { count++; RefreshCountLabel(); ApplyTiers(); } public void OnResetClicked() { count = 0; unlockedIndex = -1; ApplyTiers(force: true); RefreshCountLabel(); RefreshHintLabel(); }
// ApplyTiers, RefreshCountLabel, RefreshHintLabel — see examples/world/Clicker.cs}Source file: examples/world/Clicker.cs.
- Keep
count[WasmSerialized]so the inspector value + any play-mode testing carry across. - Compute
unlockedIndexincrementally. Looping the tier array on every click is cheap; materialising the UI only when it changes (if (newUnlocked == unlockedIndex) return;) keeps the host calls low. - Pre-configure tiers in the inspector rather than building them from code at runtime. Drag-and-drop is the point.
- Use
SetActive(...)once per tier transition, not every click.
Don’ts
Section titled “Don’ts”- Don’t allocate a new
StringBuilderper click. String interpolation once per event is fine; thousands of clicks per second still fits. - Don’t write
counttoFileStorageon every click. If you want persistence, save on reset, on unlock-crossing, or on a short-idle timer — see Idle for the persistence pattern. - Don’t sync the count over the network unless you have a multi-player mode. Clicker games are typically personal progress; networking adds ownership complexity.
Validator
Section titled “Validator”Paste Clicker.cs into the validator. Expect:
err 0,warn 0— no sandbox or API issues.infoline reportingUnity events detected: Start.
Switch the context dropdown to Avatar or Prop and compile: the script still has no world-gated calls, so it remains clean — but you’d only get per-client behaviour anyway.
Extensions
Section titled “Extensions”- Auto-clicker over time — leads into Idle.
- Upgrade cost — subtract a cost from
countwhen an upgrade unlocks, refund on reset. - Audio feedback — add an
AudioSourcechild, play a clip inOnClicked.AudioSource.Play()is bound.
Full source
Section titled “Full source”using TMPro;using UnityEngine;using WasmScripting;
public partial class Clicker : WasmBehaviour{ [System.Serializable] public struct Tier { public int threshold; public GameObject unlock; public string label; }
public TextMeshProUGUI countLabel; public TextMeshProUGUI hintLabel; public Tier[] tiers;
[WasmSerialized] private int count; [WasmSerialized] private int unlockedIndex = -1;
void Start() { ApplyTiers(force: true); RefreshCountLabel(); RefreshHintLabel(); }
public void OnClicked() { count++; RefreshCountLabel(); ApplyTiers(); }
public void OnResetClicked() { count = 0; unlockedIndex = -1; ApplyTiers(force: true); RefreshCountLabel(); RefreshHintLabel(); }
private void ApplyTiers(bool force = false) { if (tiers == null) return; int newUnlocked = unlockedIndex; for (int i = 0; i < tiers.Length; i++) { if (count >= tiers[i].threshold) newUnlocked = i; } if (newUnlocked == unlockedIndex && !force) return;
for (int i = 0; i < tiers.Length; i++) { if (tiers[i].unlock == null) continue; tiers[i].unlock.SetActive(i <= newUnlocked); } unlockedIndex = newUnlocked; RefreshHintLabel(); }
private void RefreshCountLabel() { if (countLabel != null) countLabel.text = $"{count}"; }
private void RefreshHintLabel() { if (hintLabel == null || tiers == null) return; int next = unlockedIndex + 1; if (next >= tiers.Length) { hintLabel.text = "All unlocked!"; return; } int need = tiers[next].threshold - count; hintLabel.text = need > 0 ? $"{need} more for \"{tiers[next].label}\"" : $"Next: {tiers[next].label}"; }}