Skip to content

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.

  • World Canvas with two buttons: “Click” (onClickOnClicked) and “Reset” (onClickOnResetClicked).
  • Two TextMeshProUGUIs: countLabel and hintLabel.
  • A Clicker component on the world root with the two TMP fields wired and a tiers[] array filled in the inspector. Each tier points at a GameObject to 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 unlockedIndex incrementally. 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’t allocate a new StringBuilder per click. String interpolation once per event is fine; thousands of clicks per second still fits.
  • Don’t write count to FileStorage on 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.

Paste Clicker.cs into the validator. Expect:

  • err 0, warn 0 — no sandbox or API issues.
  • info line reporting Unity 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.

  • Auto-clicker over time — leads into Idle.
  • Upgrade cost — subtract a cost from count when an upgrade unlocks, refund on reset.
  • Audio feedback — add an AudioSource child, play a clip in OnClicked. AudioSource.Play() is bound.
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}";
}
}