Unity quick start
Add leaderboards to a Unity game. One C# file, no dependencies, works on every platform Unity builds to — desktop, mobile, WebGL, console, VR.
Before you start
- Unity 2022.3 LTS or newer — the SDK is pure
UnityWebRequest, no packages. - A CheddaBoards game — register at cheddaboards.com for a Game ID and API key.
Want it all at once? Skip to the one-file example.
Step 1 — Add the SDK
Copy CheddaBoards.cs from the Unity SDK repo into your project, e.g. Assets/Scripts/CheddaBoards.cs. That's the whole install — the SDK auto-creates its own singleton GameObject with DontDestroyOnLoad, so there's no scene setup.
Prefer to start from a working example? The repo's Demo/ folder contains CheddaClick, a complete one-script game showing login, guest flow, play sessions, score submit, leaderboard render, and delta-synced achievements.
Step 2 — Configure and log in
using UnityEngine;
using System.Collections.Generic; // for Dictionary when reading boards (Step 4)
using CheddaTech; // the SDK lives in the CheddaTech namespace
public class Leaderboards : MonoBehaviour
{
void Start()
{
var cb = CheddaBoards.Instance; // auto-creates the singleton
cb.SetApiKey("cb_my-game_xxxxxxxxx");
cb.SetGameId("my-game");
cb.OnLoginSuccess += (nickname) =>
Debug.Log($"Welcome {(string.IsNullOrEmpty(nickname) ? "Guest" : nickname)}!");
cb.OnScoreSubmitted += (score, streak) => Debug.Log($"Saved: {score}");
cb.LoginAnonymous(); // no name — see below
}
}LoginAnonymous gets the player onto the board instantly with a persistent device ID — no account needed. It completes immediately: OnLoginSuccess fires during the call, so subscribe to events before calling it, as above. If no API key has been set, it fires OnLoginFailed instead. A brand-new player logged in without a name receives an empty string, so show "Guest" until they have one.
Log in without a name: returning players keep the nickname they already saved, and brand-new players get a server-assigned name (Player_1248) when their first submit creates the profile. GetNickname() returns "" until a profile fetch or rename has told the SDK the name — fetch the profile after the first submit if you want to display or highlight it. Only pass a name to LoginAnonymous when the player has just chosen it, because a passed name becomes the current nickname and is written to the server on the next submit — overwriting whatever they had. To let players pick or change their name, use ChangeNickname() (see Nicknames); a complete name-entry panel is on Player names.
Step 3 — Submit a score
Call this from your own game-over logic, with the run's score and streak:
void OnGameOver(int score, int streak)
{
CheddaBoards.Instance.SubmitScore(score, streak);
}SubmitScore fans out to every standard board on your game (all-time, weekly, daily). Only the player's best survives on each board.
Step 4 — Read the leaderboard
var cb = CheddaBoards.Instance;
cb.OnScoreboardLoaded += (id, config, entries) =>
{
foreach (Dictionary<string, object> entry in entries)
Debug.Log($"#{entry["rank"]} {entry["nickname"]}: {entry["score"]}");
};
cb.GetAlltimeLeaderboard(); // or GetWeeklyLeaderboard(), GetDailyLeaderboard()
cb.GetScoreboard("weekly", 100); // or any board by IDBoard reads come straight from the CheddaBoards canister for speed, with an automatic fallback to the proxy if the direct path can't get through — you don't have to do anything to get either.
Step 5 — Anti-cheat play sessions (recommended)
Wrap each run in a play session so the backend can validate the score against elapsed time. Start when gameplay begins, end after submitting — the SDK attaches the active session token to your submit automatically.
void StartRun()
{
CheddaBoards.Instance.StartPlaySession();
// …your game-start code…
}
void OnGameOver(int score, int streak)
{
var cb = CheddaBoards.Instance;
cb.SubmitScore(score, streak); // session token attached automatically
cb.EndPlaySession();
}StartPlaySession() is asynchronous: the token arrives a moment later via OnPlaySessionStarted (or OnPlaySessionError if it fails). A submit sent before then goes without a token. That's only a risk for very short runs, but if your game can end within a second or two of starting, check CheddaBoards.Instance.HasPlaySession() before submitting, or wait for OnPlaySessionStarted before letting the run begin.
Set the actual limits (score caps, time validation) from your dashboard's Security tab — see Anti-cheat. Without a session, scores still submit — unless the game has time validation enabled, in which case the session token is required and sessionless submits are rejected.
The whole thing in one file
Everything from Steps 2–5 in a single MonoBehaviour. Drop it on any GameObject, fill in your key and Game ID, and wire StartRun() / GameOver() to your own game logic.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using CheddaTech; // the SDK namespace (CheddaBoards.cs)
/// One-file CheddaBoards integration: login, play session, submit, leaderboard.
public class CheddaBoardsExample : MonoBehaviour
{
[Header("From cheddaboards.com/developers")]
public string apiKey = "cb_my-game_xxxxxxxxx";
public string gameId = "my-game";
[Header("Optional: any UI Text to render the board into")]
public Text leaderboardText;
CheddaBoards cb;
void Start()
{
cb = CheddaBoards.Instance; // auto-creates the singleton (DontDestroyOnLoad)
cb.SetApiKey(apiKey);
cb.SetGameId(gameId);
// Subscribe BEFORE LoginAnonymous(): OnLoginSuccess fires during the call.
cb.OnLoginSuccess += nick => { Debug.Log($"Logged in as {(nick == "" ? "Guest" : nick)}"); cb.GetAlltimeLeaderboard(); };
cb.OnLoginFailed += err => Debug.LogError($"Login failed: {err}"); // usually a missing API key
cb.OnPlaySessionStarted += tok => Debug.Log("Play session ready");
cb.OnScoreSubmitted += (score, streak) => { Debug.Log($"Saved {score}"); cb.GetAlltimeLeaderboard(); };
cb.OnScoreError += err => Debug.LogWarning($"Score rejected: {err}");
cb.OnScoreboardLoaded += RenderBoard;
cb.OnScoreboardError += err => Debug.LogWarning($"Board error: {err}");
cb.LoginAnonymous(); // guest login on a persistent device ID, no account needed
}
/// Call when a run begins. Starts an anti-cheat play session (token arrives via OnPlaySessionStarted).
public void StartRun()
{
cb.StartPlaySession();
}
/// Call when a run ends. Submits to all-time, weekly and daily boards; only the player's best is kept.
public void GameOver(int score, int streak = 0)
{
if (!cb.HasPlaySession()) Debug.LogWarning("Submitting without a play session (started too fast?)");
cb.SubmitScore(score, streak); // play-session token is attached automatically
cb.EndPlaySession();
}
/// Fires for every board read; entries are dictionaries with rank / nickname / score / streak.
void RenderBoard(string boardId, Dictionary<string, object> config, List<object> entries)
{
var lines = new List<string> { $"== {boardId} ==" };
foreach (Dictionary<string, object> e in entries)
lines.Add($"#{e["rank"]} {e["nickname"]} {e["score"]}");
string text = string.Join("\n", lines);
if (leaderboardText != null) leaderboardText.text = text;
Debug.Log(text);
}
}That's the complete integration. SubmitScore fans out to the three default boards; swap in SubmitScoreToBoard("level-14", score, streak) for per-level boards, and GetWeeklyLeaderboard() / GetDailyLeaderboard() / GetScoreboard("any-id") for other reads — they all arrive through the same OnScoreboardLoaded event.
Signing in with Google / Apple (optional)
Device Code Auth — the player authorises on their phone, no in-game browser popups, works on every platform:
var cb = CheddaBoards.Instance;
cb.OnDeviceCodeReceived += (code, url, qrDataUrl) =>
{
// Show the code + URL, or render qrDataUrl (a base64 PNG) as a scannable QR.
codeLabel.text = $"Go to {url}\nEnter code: {code}";
};
cb.OnDeviceCodeApproved += (nickname) => Debug.Log($"Signed in as {nickname}");
cb.LoginWithDeviceCode();Players sign in once — the session persists across restarts. If the server later rejects a stored session, OnSessionExpired fires (and OnLogoutSuccess with it, so a menu that handles logout falls back to its sign-in screen). Full flow: Authentication.
Two things the SDK (2.3.0+) does for you here. A pending code survives an app restart or WebGL reload: it's saved to PlayerPrefs, polling resumes on the same code, and a LoginWithDeviceCode() call on your login screen re-emits that code instead of minting a new one (pass true to force a fresh one; HasPendingDeviceCode(), GetDeviceVerificationUrl() and GetDeviceCodeSecondsRemaining() let you redraw a restored code with its real time left). And closing the code popup is not cancelling: hide the UI and leave polling running, and OnDeviceCodeApproved still fires when the player finishes on their phone. Only call CancelDeviceCode() on an explicit "Cancel", since an approval given after that call is never picked up. If the player was anonymous, OnAccountUpgraded (or OnAccountUpgradeFailed) follows the approval once their progress has merged — see Device code login.
Nicknames
Nicknames are 3–16 characters, letters, digits, and underscores. A taken name is auto-suffixed (Chedz → Chedz_1) rather than rejected; only genuinely invalid names raise OnNicknameError, and that's permanent for that value — ask for a different one. Before a brand-new player's first score, ChangeNickname() holds the name locally and sends it with that submit; redraw from GetNickname() on OnProfileLoaded to pick up what the server stored. Full model and a drop-in name-entry panel: Player names.
cb.OnNicknameChanged += (newNick) => Debug.Log($"Now: {newNick}");
cb.ChangeNickname("NewName");Category & timed boards
Submit to one specific board (per-level, per-mode) with SubmitScoreToBoard, and run daily/weekly/monthly competitions with automatic archiving. Both work the same as elsewhere — the concepts are engine-agnostic:
cb.SubmitScoreToBoard("level-14", score, streak); // targeted board only, no fan-out
cb.GetLastWeekScoreboard(); // read an archived periodSee Category boards and Timed leaderboards.
Events reference
The events you'll connect to most:
| Event | Parameters |
|---|---|
OnSdkReady | — |
OnLoginSuccess / OnLoginFailed | nickname / error |
OnLogoutSuccess | — |
OnSessionExpired | — (stored session rejected; OnLogoutSuccess also fires) |
OnScoreSubmitted | score, streak |
OnScoreSubmittedToBoard | boardId, score, streak |
OnScoreError | error |
OnScoreboardLoaded / OnScoreboardError | id, config, entries / error |
OnScoreboardRankLoaded | id, rank, score, streak, total |
OnAchievementUnlocked / OnAchievementsLoaded | achievementId / achievements |
OnPlaySessionStarted / OnPlaySessionError | token / error |
OnDeviceCodeReceived | code, url, qrDataUrl |
OnDeviceCodeApproved / OnDeviceCodeExpired | nickname / — |
OnDeviceCodeError | error |
OnAccountUpgraded / OnAccountUpgradeFailed | profile, migration (migratedGames / migratedScoreboards) / error |
OnProfileLoaded / OnNoProfile | nickname, score, streak, achievements, playCount / — (brand-new player, no profile yet) |
OnNicknameChanged / OnNicknameError | nickname / error |
OnArchivedScoreboardLoaded | archiveId, config, entries |
Full method and event reference lives in the SDK repo README.
Common issues
| Issue | Fix |
|---|---|
| "Not authenticated" on submit | LoginAnonymous() wasn't called, or failed because no API key was set (check OnLoginFailed). With device code, wait for OnDeviceCodeApproved before submitting |
| Leaderboard fires twice | You subscribed to OnScoreboardLoaded inside a method that runs repeatedly — subscribe once, in Start() |
| Empty leaderboard | Confirm your Game ID matches the dashboard, and that the board ID exists. OnScoreboardError reports the reason |
| Score rejected | Start a play session before the run so the backend can time-validate it, and make sure it had started (OnPlaySessionStarted) before the submit. Check OnScoreError for the reason |
| WebGL build can't reach CheddaBoards | If the page hosting your build sets a Content-Security-Policy, add https://api.cheddaboards.com and https://fdvph-sqaaa-aaaap-qqc4a-cai.raw.icp0.io (direct board reads) to connect-src |
Full error reference: Errors.
See also: REST API · Authentication · Anti-cheat · What's stored · Unity SDK repo
