一键导入
viverse-leaderboard
How to implement a global leaderboard for VIVERSE worlds using the official VIVERSE gameDashboard SDK
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
How to implement a global leaderboard for VIVERSE worlds using the official VIVERSE gameDashboard SDK
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Three.js Polygon Streaming .xrg integration and debugging playbook for @polygon-streaming/web-player-threejs, including correct wrapper events, asset publishing, model fitting, and fallback replacement policy
Bundle entry for the exported VIVERSE PlayCanvas Toolkit workflow pack. Use when you want bundled VIVERSE PlayCanvas Toolkit skills, prompts, catalogs, and helper assets in one standalone package.
Collision detection patterns for Three.js games without physics engines
Protect third-party API keys by moving secret-bearing calls into VIVERSE Play Lambda scripts and keeping only non-secret user data in VIVERSE Storage.
Build or fix CPU racing drivers for Three.js or VIVERSE racing games. Use for waypoint loops, tile-to-route reconstruction, lap completion, recovery logic, and low-overhead debug workflows.
VIVERSE Login SDK integration for user authentication and SSO
基于 SOC 职业分类
| name | viverse-leaderboard |
| description | How to implement a global leaderboard for VIVERSE worlds using the official VIVERSE gameDashboard SDK |
| prerequisites | ["VIVERSE Auth integration (checkAuth)","ViverseService","Viverse Studio App ID"] |
| tags | ["viverse","leaderboard","tracking","gamedashboard","api"] |
Use viverse.gameDashboard to upload and fetch persistent global rankings.
Before coding:
When AI assists a user, remind them to complete these Studio steps before test publish:
.env).VITE_VIVERSE_LEADERBOARD_NAME (exact match, case-sensitive)NumericalDescending for score-based gamesAppend for cumulative points across matches.env App ID or leaderboard-name change.If runtime error contains Unexpected token '<' during leaderboard API calls, remind user to re-check:
For every leaderboard integration task, AI MUST explicitly remind user to complete Studio setup before testing:
VITE_VIVERSE_LEADERBOARD_NAME (case-sensitive).Do not assume Studio setup exists even if code is correct.
access_token present)VITE_VIVERSE_CLIENT_ID matches target appVITE_VIVERSE_LEADERBOARD_NAME matches Studio API namegameDashboard client once per tokenclient.getToken() only when an actual auth client instance is already available; otherwise fallback to checkAuth().access_tokenCRITICAL: Use specific URLs and the correct token source to avoid bridge errors.
// Step A: Prefer specific dashboard token
let dashboardToken = auth.access_token;
if (client && typeof client.getToken === "function") {
const res = await client.getToken();
dashboardToken = typeof res === 'string' ? res : (res?.access_token || dashboardToken);
}
// Step B: Initialize with Base URLs
const v = window.vSdk || window.viverse || window.VIVERSE_SDK;
const DashboardClass = v?.gameDashboard || v?.GameDashboard;
const gameDashboardClient = new DashboardClass({
token: dashboardToken,
clientId: appId, // Mandatory
baseURL: "https://www.viveport.com/", // MANDATORY for production
communityBaseURL: "https://www.viverse.com/", // MANDATORY for production
});
Important:
sdk.leaderboard, sdk.Leaderboard, or constructor patterns like new sdk.leaderboard(...).gameDashboard / GameDashboard plus uploadLeaderboardScore() and getLeaderboard().accessToken, sdk, and appId, that is sufficient for a valid implementation.The name must exactly match Studio API name.
await gameDashboardClient.uploadLeaderboardScore(appId, [
{ name: leaderboardName, value: scoreValue },
]);
const configs = [
{ name: leaderboardName, range_start: 0, range_end: 9, region: "global", time_range: "alltime", around_user: false },
{ name: leaderboardName, range_start: 0, range_end: 9, region: "global", time_range: "alltime", around_user: true },
{ name: leaderboardName, range_start: 0, range_end: 9, region: "local", time_range: "alltime", around_user: false },
];
let rankings = [];
for (const conf of configs) {
const res = await gameDashboardClient.getLeaderboard(appId, conf);
// Robust Extraction
const extracted =
res?.rankings ||
res?.ranking ||
res?.leaderboard_rankings ||
res?.data?.rankings ||
res?.data?.ranking ||
res?.leaderboard?.rankings ||
res?.leaderboard?.ranking ||
[];
if (extracted.length > 0) {
rankings = extracted;
break;
}
}
// Optional read fallback for some deployments
if (rankings.length === 0 && typeof gameDashboardClient.getGuestLeaderboard === "function") {
for (const conf of configs) {
const res = await gameDashboardClient.getGuestLeaderboard(appId, conf);
const extracted =
res?.rankings ||
res?.ranking ||
res?.leaderboard_rankings ||
res?.data?.rankings ||
res?.data?.ranking ||
res?.leaderboard?.rankings ||
res?.leaderboard?.ranking ||
[];
if (extracted.length > 0) {
rankings = extracted;
break;
}
}
}
Fallback query order when rankings is empty:
global + around_user=falseglobal + around_user=truelocal + around_user=falseAlso support response-shape fallback (rankings, leaderboard_rankings, nested response objects).
max-height + overflow-y-auto) instead of shrinking or obscuring the main game viewport.Leaderboard submit APIs are side-effectful (especially with Append update rule).
Your game MUST prevent duplicate submissions for the same finished match.
Required pattern:
resultKey per match-end event (for example: matchId + endedAt + winner).submittedResultKeyRef (or equivalent state/ref).Why:
onGameEnd can fire more than once from re-renders, network replays, or lifecycle transitions.When leaderboard behaves as "upload success but empty rankings", log:
appId, leaderboardName, valueclient.getToken vs checkAuthregion, around_user, range_start/end)Recommended diagnostic line format:
diag(appId=..., leaderboard=..., tokenSource=..., token=present)
Interpretation:
upload success + rows=0 => write path works; investigate fetch params/shape/propagation timingUnexpected token '<' => backend returned HTML; usually app/name/token mismatchchrome-extension://...) are unrelated noise unless they reference app bundle pathsrows=0 with lastUpload.value=0 often means only zero-score results were uploaded; verify with a positive score upload.getLeaderboard remains empty, try getGuestLeaderboard as a read fallback for display.HTTP error ... /api/vrleaderboard/v1/apps/<appId> with 404 means leaderboard backend cannot find that app id; verify leaderboard was created in Studio under the same app and try VITE_VIVERSE_APP_ID / VITE_VIVERSE_CLIENT_ID candidate fallback.index-*.js) indicate stale cached build; verify latest published hash/build tag before debugging logic.#1, the cause is using r.rank || i+1 — the API returns 0-based ranks, so rank=0 (1st place) is falsy and falls back to i+1=1, and rank=1 (2nd place) is truthy but also renders as #1. Fix: (typeof r.rank === 'number') ? r.rank + 1 : i + 1.Leaderboard lookup key is: App ID + Leaderboard Name.
For test/prod:
VITE_VIVERSE_LEADERBOARD_NAME)APIs require valid auth token; guest users cannot upload.
Do not spam uploads in render/game loops.
A leaderboard name valid in app A fails in app B if not configured there.
Build-time env drift can target wrong App ID; rebuild before publish after env changes.
Upload leaderboard record successfully does not guarantee immediate non-empty ranking rows.
THREE.WebGLRenderer: Context Lost is graphics lifecycle related, not leaderboard API failure.
Duplicate end callbacks can repeatedly submit scores unless guarded by a per-match idempotency key.
r.rank is 0-based — do not use it with || fallback: The VIVERSE getLeaderboard API returns a 0-based rank field (rank=0 is #1, rank=1 is #2, etc.). The pattern r.rank || r.ranking || i + 1 is broken because rank=0 is falsy and falls through to i+1=1, while rank=1 is truthy and renders as #1 — causing the first two rows to both show #1.
Wrong:
const rank = r.rank || r.ranking || i + 1; // rank=0 is falsy → #1; rank=1 truthy → also #1
Correct:
// r.rank is 0-based; add 1 for 1-based display. Fallback to array index if field is absent.
const rank = (typeof r.rank === 'number') ? r.rank + 1 : i + 1;