Mark
Prix Créer un compte
← Tous les articles

Suivre les succès dans un jeu Phaser pour navigateur

Mike Dalton Par Mike Dalton ·

Une fenêtre de navigateur affichant un jeu avec des étoiles qui tombent, à côté du logo Game Stats AI, avec une flèche pointant vers une carte de tableau de bord montrant un trophée et un graphique à barres.

Le suivi des succès enregistre les jalons que les joueurs atteignent dans votre jeu, comme un score élevé, un niveau terminé ou un secret caché. Dans un jeu pour navigateur, ces données viennent du JavaScript qui s'exécute sur la machine du joueur : le jeu appelle donc l'API directement depuis la page.

Pour montrer à quoi cela ressemble, nous avons créé Star Catcher, un petit jeu Phaser 4 dans lequel le joueur attrape des étoiles qui tombent et évite des bombes, et nous y avons intégré l'API de Game Stats AI. Le projet terminé se trouve dans notre dépôt d'exemple. Il utilise Phaser 4.2, Vite 8 et du JavaScript sans framework.

Étape 1 : décider ce qui compte comme un succès

Associez les succès à des moments qui existent déjà dans votre jeu. Dans Star Catcher, le score augmente d'un point pour chaque étoile attrapée. Les succès consistent donc à attraper 10, 25 et 50 étoiles en une seule partie.

La scène gère déjà l'étoile attrapée, c'est donc là que va l'appel qui débloque le succès :

const STAR_ACHIEVEMENTS = {
  10: "Caught 10 Stars",
  25: "Caught 25 Stars",
  50: "Caught 50 Stars",
}

catchStar(_player, star) {
  star.destroy()
  this.score += 1
  this.scoreText.setText(`Stars: ${this.score}`)

  if (this.score in STAR_ACHIEVEMENTS) {
    unlockAchievement(STAR_ACHIEVEMENTS[this.score])
  }
}

unlockAchievement vient de src/game-stats.js, le module que le reste de ce guide construit.

Étape 2 : créer un token client

Game Stats AI reçoit les événements de succès sur un seul endpoint :

POST https://gamestats.ai/api/v1/accounts/{account_id}/achievement_events
Authorization: Bearer <token>
Content-Type: application/json

Le JavaScript d'un jeu pour navigateur est téléchargé par chaque joueur, et n'importe qui peut le lire dans les outils de développement du navigateur. Considérez le token comme public et créez un token client, pas un token serveur. Dans Paramètres > Jetons API, choisissez « Client » au moment de créer le token. Un token client ne peut qu'envoyer de la télémétrie ; s'il fuite, il ne peut pas lire les rapports de votre compte.

L'écran de création d'un token API, avec un champ nom et un choix de type de token Client ou Serveur

Étape 3 : autoriser l'origine de votre jeu

Une page web ne peut appeler une API sur un autre domaine que si cette API autorise l'origine de la page. C'est ce qu'on appelle CORS (partage de ressources entre origines). Game Stats AI n'autorise que les origines que vous ajoutez à votre compte : une page que vous n'avez pas ajoutée ne peut donc pas envoyer d'événements avec votre token.

Une origine est le schéma, l'hôte et le port depuis lesquels le jeu est servi, sans chemin. Ajoutez-en une pour chaque endroit où votre jeu tourne : http://localhost:5173 pour le serveur de développement de Vite, et votre adresse de production, par exemple https://play.yourgame.com. Ajoutez-les dans Paramètres > Origines navigateur :

La page de paramètres Origines navigateur avec http://localhost:5173 ajoutée

Si l'origine manque, le navigateur bloque la requête avant de l'envoyer, et la console affiche une erreur comme celle-ci :

Access to fetch at 'https://gamestats.ai/api/v1/accounts/8/achievement_events' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Pour savoir sur quelle origine tourne votre jeu, ouvrez la console du navigateur sur la page du jeu et exécutez location.origin. Si le jeu est intégré dans une iframe, exécutez-le dans le contexte de l'iframe : c'est l'origine de l'iframe qui appelle l'API.

Étape 4 : ajouter l'identifiant du compte et le token

Vite lit les variables d'environnement dans un fichier .env.local à la racine du projet. Les variables qui commencent par VITE_ sont disponibles dans votre code via import.meta.env :

VITE_GAME_STATS_ACCOUNT_ID=your_account_id
VITE_GAME_STATS_API_TOKEN=client_your_token

Le .gitignore que Vite crée par défaut pour les nouveaux projets exclut déjà les fichiers *.local, ce qui garde le token hors de votre dépôt. Cela ne le cache pas aux joueurs : vite build écrit les deux valeurs dans le bundle JavaScript.

La version provient de package.json. vite.config.js la rend disponible pour le jeu sous le nom __GAME_VERSION__ :

import { defineConfig } from "vite"
import packageJson from "./package.json" with { type: "json" }

export default defineConfig({
  define: {
    __GAME_VERSION__: JSON.stringify(packageJson.version),
  },
})

Mettez à jour la version de package.json avant de publier chaque build : Game Stats AI regroupe les déblocages par version.

Le début de src/game-stats.js lit ces valeurs une seule fois. Si l'identifiant du compte ou le token manque, l'intégration se désactive et le jeu fonctionne normalement :

const ACCOUNT_ID = import.meta.env.VITE_GAME_STATS_ACCOUNT_ID
const API_TOKEN = import.meta.env.VITE_GAME_STATS_API_TOKEN
const BASE_URL = import.meta.env.VITE_GAME_STATS_BASE_URL ?? "https://gamestats.ai"
const VERSION_NAME = __GAME_VERSION__
const STORAGE_KEY = "game_stats"
const RETRY_DELAY_MS = 30_000

const enabled = Boolean(ACCOUNT_ID && API_TOKEN)
const endpointUrl = `${BASE_URL}/api/v1/accounts/${ACCOUNT_ID}/achievement_events`
const state = loadState()
let requestInFlight = false

if (enabled) {
  window.addEventListener("online", sendNextEvent)
  sendNextEvent()
} else {
  console.warn("Game Stats AI is disabled: VITE_GAME_STATS_ACCOUNT_ID or VITE_GAME_STATS_API_TOKEN is missing.")
}

Étape 5 : construire le payload

Le corps comporte quatre champs obligatoires :

export function unlockAchievement(achievementName) {
  if (!enabled || state.unlockedAchievements.includes(achievementName)) {
    return
  }

  state.unlockedAchievements.push(achievementName)
  state.pendingEvents.push({
    version_name: VERSION_NAME,
    player_username: state.playerUsername,
    achievement_name: achievementName,
    occurred_at: new Date().toISOString(),
  })
  saveState()
  sendNextEvent()
}

toISOString() renvoie l'heure actuelle en UTC, par exemple 2026-10-11T16:42:07.913Z. Les succès, joueurs et versions sont créés à la première utilisation, il n'y a donc rien à déclarer à l'avance.

L'événement reçoit son occurred_at une seule fois, au moment où le joueur débloque le succès, et il est stocké avec cet horodatage. Game Stats AI ignore un déblocage déjà reçu seulement si le joueur, le succès, la version et occurred_at correspondent tous. Un renvoi qui reconstruirait le payload avec un nouvel horodatage enregistrerait un second déblocage.

Étape 6 : envoyer les événements avec fetch

Le module envoie le premier événement en attente et attend la réponse avant d'envoyer le suivant :

async function sendNextEvent() {
  if (requestInFlight || state.pendingEvents.length === 0) {
    return
  }

  requestInFlight = true
  const status = await postEvent(state.pendingEvents[0])
  requestInFlight = false

  if (status === 201 || status === 422) {
    state.pendingEvents.shift()
    saveState()
    sendNextEvent()
  } else {
    setTimeout(sendNextEvent, RETRY_DELAY_MS)
  }
}

async function postEvent(event) {
  try {
    const response = await fetch(endpointUrl, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(event),
    })
    if (response.status === 422) {
      console.warn("Game Stats AI rejected an event:", await response.text())
    }
    return response.status
  } catch (error) {
    console.warn("Game Stats AI could not send an event:", error.message)
    return null
  }
}

Un 201 signifie que l'événement a été accepté. Un 422 signifie que le payload lui-même est invalide, par exemple {"errors":["Version name can't be blank"]} : le renvoyer ne servira à rien, alors journalisez-le et passez au suivant. Tout autre résultat laisse l'événement en tête de file et le renvoie 30 secondes plus tard.

fetch lève une erreur au lieu de renvoyer un code de statut dans deux cas : le joueur est hors ligne, ou le navigateur a bloqué la requête parce que l'origine n'est pas autorisée. Les deux cas mènent au renvoi : les déblocages attendent dans la file jusqu'à ce que le problème soit résolu. L'écouteur online de l'étape 4 les envoie dès que la connexion revient.

Étape 7 : sauvegarder les déblocages dans localStorage

Le module conserve trois informations dans localStorage : un nom de joueur généré, les succès déjà débloqués et les événements pas encore envoyés.

function loadState() {
  const savedState = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}")
  return {
    playerUsername: savedState.playerUsername ?? `player_${crypto.randomUUID().replaceAll("-", "").slice(0, 12)}`,
    unlockedAchievements: savedState.unlockedAchievements ?? [],
    pendingEvents: savedState.pendingEvents ?? [],
  }
}

function saveState() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}

Grâce à la liste des succès débloqués, chaque succès est envoyé une fois par joueur, et non une fois par partie, même après un rechargement de la page. Grâce à la liste des événements en attente, un déblocage n'est pas perdu si le joueur ferme l'onglet avant son envoi : il est envoyé avec son horodatage d'origine au prochain chargement du jeu.

localStorage appartient à un navigateur sur un appareil : un joueur qui efface les données de son navigateur ou change d'appareil reçoit un nouveau nom de joueur. Si votre jeu a déjà des comptes joueurs, utilisez cet identifiant pour player_username au lieu d'un nom généré.

L'intégration complète se trouve dans le dépôt d'exemple.

Continuer la lecture

© 2026 Rowhome Labs, LLC