Suivre les succès dans un jeu Unity
Par Mike Dalton ·
Le suivi des succès enregistre les jalons que les joueurs atteignent dans votre jeu, comme un meilleur score, un niveau terminé ou un secret découvert. Dans un jeu Unity, un script qui tourne sur la machine du joueur envoie chaque jalon à une API pendant que le jeu continue de tourner.
Pour montrer à quoi cela ressemble, nous avons créé Orb Rush, un petit jeu Unity dans lequel le joueur a 30 secondes pour ramasser autant d'orbes que possible, 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 Unity 6000.5 et C#, et cible les builds pour ordinateur. Si vous envisagez aussi le service de Unity, consultez Unity Analytics en 2026 : tarifs, limites et alternatives.
É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 Orb Rush, le score augmente d'un point pour chaque orbe ramassé : les succès consistent donc à ramasser 10, 20 et 30 orbes en une seule manche.
Le script du jeu gère déjà le ramassage d'un orbe, c'est donc là que va l'appel qui débloque le succès :
static readonly Dictionary<int, string> OrbAchievements = new()
{
{ 10, "Collected 10 Orbs" },
{ 20, "Collected 20 Orbs" },
{ 30, "Collected 30 Orbs" },
};
void CollectOrb()
{
orbsCollected += 1;
if (OrbAchievements.TryGetValue(orbsCollected, out var achievementName))
{
GameStats.Instance.Unlock(achievementName);
}
MoveOrb();
}
GameStats est le composant 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
Ce token est embarqué dans votre jeu, que les joueurs téléchargent et exécutent : considérez-le donc comme public. 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.


Enregistrez l'identifiant du compte et le token dans Assets/StreamingAssets/game_stats.json :
{
"account_id": "your_account_id",
"api_token": "client_your_token"
}
Ajoutez le fichier et son fichier .meta à .gitignore pour garder le token hors de votre dépôt. L'étape 7 explique pourquoi les joueurs peuvent quand même le lire.
Étape 3 : ajouter un composant GameStats qui reste actif
Unity appelle une méthode statique marquée [RuntimeInitializeOnLoadMethod] au lancement du jeu. Avec BeforeSceneLoad, elle s'exécute avant le chargement de la première scène : GameStats crée donc son propre GameObject, et aucune scène n'a besoin de le contenir. DontDestroyOnLoad garde ce GameObject actif d'une scène à l'autre, si bien que n'importe quel script peut appeler GameStats.Instance :
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void CreateInstance()
{
var gameObject = new GameObject("GameStats");
DontDestroyOnLoad(gameObject);
Instance = gameObject.AddComponent<GameStats>();
}
void Awake()
{
var configPath = Path.Combine(Application.streamingAssetsPath, ConfigFileName);
if (!File.Exists(configPath))
{
Debug.LogWarning($"Game Stats AI is disabled: {configPath} is missing.");
enabled = false;
return;
}
var config = JsonUtility.FromJson<GameStatsConfig>(File.ReadAllText(configPath));
endpointUrl = $"{config.base_url}/api/v1/accounts/{config.account_id}/achievement_events";
apiToken = config.api_token;
statePath = Path.Combine(Application.persistentDataPath, StateFileName);
LoadState();
StartCoroutine(SendPendingEvents());
}
Dans Awake, le composant lit le fichier de configuration et construit l'URL une seule fois. Si le fichier est absent, il se désactive et le jeu fonctionne normalement.
JsonUtility remplit les champs d'une classe [Serializable] à partir des clés JSON qui portent le même nom :
[Serializable]
class GameStatsConfig
{
public string base_url = "https://gamestats.ai";
public string account_id;
public string api_token;
}
Les champs absents du fichier gardent leur valeur par défaut : base_url pointe donc vers Game Stats AI, sauf si vous ajoutez une clé base_url.
Étape 4 : construire le payload
Le corps comporte quatre champs obligatoires :
public void Unlock(string achievementName)
{
if (!enabled || state.unlockedAchievements.Contains(achievementName))
{
return;
}
state.unlockedAchievements.Add(achievementName);
state.pendingEvents.Add(new AchievementEvent
{
version_name = Application.version,
player_username = state.playerUsername,
achievement_name = achievementName,
occurred_at = DateTime.UtcNow.ToString("o"),
});
SaveState();
if (!sending)
{
StartCoroutine(SendPendingEvents());
}
}
[Serializable]
class AchievementEvent
{
public string version_name;
public string player_username;
public string achievement_name;
public string occurred_at;
}
Les noms des champs correspondent aux clés JSON attendues par l'API, car JsonUtility utilise les noms des champs comme clés. Application.version provient de Edit > Project Settings > Player > Version. Mettez-la à jour avant de publier chaque build : Game Stats AI regroupe les déblocages par version. DateTime.UtcNow.ToString("o") renvoie l'heure UTC actuelle au format ISO 8601, par exemple 2026-10-12T21:47:13.0283120Z. 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 5 : envoyer les événements avec UnityWebRequest dans une coroutine
Une coroutine est une méthode qu'Unity peut mettre en pause et reprendre d'une frame à l'autre. Quand une coroutine fait un yield de SendWebRequest(), Unity la met en pause jusqu'à l'arrivée de la réponse, et le jeu continue de tourner pendant ce temps.
La coroutine envoie le premier événement en attente et attend la réponse avant d'envoyer le suivant :
IEnumerator SendPendingEvents()
{
sending = true;
while (state.pendingEvents.Count > 0)
{
var body = JsonUtility.ToJson(state.pendingEvents[0]);
using var request = UnityWebRequest.Post(endpointUrl, body, "application/json");
request.SetRequestHeader("Authorization", $"Bearer {apiToken}");
request.timeout = 10;
yield return request.SendWebRequest();
if (request.responseCode == 422)
{
Debug.LogWarning($"Game Stats AI rejected an event: {request.downloadHandler.text}");
}
if (request.responseCode == 201 || request.responseCode == 422)
{
state.pendingEvents.RemoveAt(0);
SaveState();
}
else
{
yield return new WaitForSeconds(RetryDelaySeconds);
}
}
sending = false;
}
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, y compris une absence totale de connexion, laisse l'événement en tête de file et le renvoie 30 secondes plus tard. Le drapeau sending garantit qu'une seule copie de la coroutine tourne à la fois : Unlock ajoute donc à la file pendant qu'une requête est en cours.
Étape 6 : sauvegarder les déblocages entre les sessions
Le composant conserve trois informations dans un fichier JSON situé dans Application.persistentDataPath, un dossier propre à chaque utilisateur qu'Unity conserve d'une exécution à l'autre : un nom de joueur généré, les succès déjà débloqués et les événements pas encore envoyés.
void LoadState()
{
state = File.Exists(statePath)
? JsonUtility.FromJson<GameStatsState>(File.ReadAllText(statePath))
: new GameStatsState();
if (string.IsNullOrEmpty(state.playerUsername))
{
state.playerUsername = "player_" + Guid.NewGuid().ToString("N").Substring(0, 12);
}
SaveState();
}
void SaveState()
{
File.WriteAllText(statePath, JsonUtility.ToJson(state));
}
[Serializable]
class GameStatsState
{
public string playerUsername;
public List<string> unlockedAchievements = new();
public List<AchievementEvent> pendingEvents = new();
}
Grâce à la liste des succès débloqués, chaque succès est envoyé une fois par joueur, et non une fois par manche. Grâce à la liste des événements en attente, un déblocage obtenu hors ligne n'est pas perdu. Pour le tester, nous avons arrêté le serveur, joué une manche d'Orb Rush, puis relancé le jeu une fois le serveur de nouveau disponible : les trois déblocages ont été envoyés au lancement avec leur horodatage d'origine. Si votre jeu a déjà des comptes joueurs, utilisez cet identifiant pour player_username au lieu d'un nom généré.
Étape 7 : inclure le fichier de configuration dans vos builds
Unity copie tout le contenu de Assets/StreamingAssets dans chaque build, sans le modifier : le fichier de configuration ne demande donc aucune étape supplémentaire. Dans un build macOS, il se retrouve dans OrbRush.app/Contents/Resources/Data/StreamingAssets/game_stats.json.
Toute personne qui télécharge votre jeu peut lire ce fichier : c'est pourquoi il doit contenir un token client, jamais un token serveur.
Étape 8 : voir les déblocages dans un rapport
Pour comparer les succès, ouvrez Rapports, choisissez Nouveau rapport, sélectionnez Rapport de succès et cochez les succès à inclure.
Le rapport ci-dessous provient de nos propres parties de test d'Orb Rush, auxquelles s'ajoutent des joueurs simulés envoyés au même endpoint. Les 213 joueurs ont ramassé 10 orbes, 116 (54 %) en ont ramassé 20 et 36 (17 %) en ont ramassé 30 :


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