Suivre les succès depuis un backend C#
Par Mike Dalton ·
Le suivi des succès enregistre les jalons que les joueurs atteignent dans votre jeu : terminer un niveau, trouver un objet caché, vaincre un boss. Ces données montrent jusqu'où vont les joueurs, où ils s'arrêtent et quel contenu ils ne trouvent jamais.
Si votre jeu communique déjà avec un backend que vous contrôlez, ce backend est un bon endroit d'où envoyer les événements de succès. Votre token d'API reste sur vos serveurs au lieu d'être embarqué dans le client du jeu, et l'intégration fonctionne de la même façon quel que soit le moteur utilisé côté client.
Pour montrer à quoi cela ressemble, nous avons construit game-stats-ai-csharp-example-app, un petit service ASP.NET Core sur .NET 8 qui reçoit un déblocage depuis le jeu et le transmet à Game Stats AI en utilisant uniquement HttpClient et System.Text.Json de la bibliothèque de classes de base. Ce guide le déroule étape par étape.
Étape 1 : créer un token serveur
Game Stats AI propose deux types de tokens d'API. Les tokens client ne peuvent qu'envoyer de la télémétrie, ils peuvent donc être embarqués sans risque dans un client de jeu. Les tokens serveur peuvent envoyer de la télémétrie et lire les rapports, et ils doivent rester sur un backend de confiance.
Comme ce code s'exécute sur votre backend, créez un token serveur sur gamestats.ai/api_tokens. La même page affiche l'id de compte dont l'URL de l'endpoint a besoin.


Étape 2 : stocker le token et l'id de compte dans la configuration
appsettings.json ne contient que l'hôte de l'API :
"GameStats": {
"BaseUrl": "https://gamestats.ai"
}
Le token est un secret, et stocker l'id de compte de la même façon garde les deux hors du dépôt. En développement, utilisez les user secrets :
dotnet user-secrets set "GameStats:AccountId" "your_account_id"
dotnet user-secrets set "GameStats:Token" "server_your_token_here"
En production, définissez-les comme variables d'environnement GameStats__AccountId et GameStats__Token. Toutes les sources se lient à la même section, donc le reste du code n'a jamais besoin de savoir d'où viennent les valeurs.
Une classe d'options contient les trois valeurs :
namespace GameStatsExample;
public sealed class GameStatsOptions
{
public string BaseUrl { get; set; } = "https://gamestats.ai";
public string AccountId { get; set; } = "";
public string Token { get; set; } = "";
}
Étape 3 : définir le payload de l'événement
Game Stats AI accepte 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 corps comporte quatre champs obligatoires. Un record avec des attributs JsonPropertyName fait correspondre les noms C# aux noms en snake_case que l'API attend :
using System.Text.Json.Serialization;
namespace GameStatsExample;
public sealed record AchievementEvent(
[property: JsonPropertyName("version_name")] string VersionName,
[property: JsonPropertyName("player_username")] string PlayerUsername,
[property: JsonPropertyName("achievement_name")] string AchievementName,
[property: JsonPropertyName("occurred_at")] DateTime OccurredAt);
Sur .NET 8, vous pouvez supprimer les attributs et définir PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower dans JsonSerializerOptions ; les attributs gardent le format attendu par l'API visible à un seul endroit.
occurred_at est un horodatage ISO 8601. System.Text.Json sérialise un DateTime en UTC dans ce format avec un Z final, donc DateTime.UtcNow n'a besoin d'aucune chaîne de format.
Les succès, les joueurs et les versions sont créés la première fois qu'ils apparaissent dans un événement, il n'y a donc rien à enregistrer à l'avance. Renvoyer le même succès pour le même joueur n'a aucun effet, ce qui rend les nouvelles tentatives sûres.
Étape 4 : enregistrer un HttpClient typé
Enregistrez un client typé dans Program.cs. IHttpClientFactory gère la durée de vie des connexions, et le callback de la fabrique lit les options pour définir l'adresse de base et le token bearer une seule fois :
builder.Services.Configure<GameStatsOptions>(builder.Configuration.GetSection("GameStats"));
builder.Services.AddHttpClient<GameStatsClient>((services, client) =>
{
var options = services.GetRequiredService<IOptions<GameStatsOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.Token);
});
Si votre application n'utilise pas l'injection de dépendances, un unique HttpClient à longue durée de vie configuré de la même façon fonctionne aussi.
Étape 5 : envoyer l'événement et gérer la réponse
Le client envoie le record en POST et inspecte le résultat :
using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
namespace GameStatsExample;
public sealed class GameStatsClient(HttpClient http, IOptions<GameStatsOptions> options)
{
private readonly string _accountId = options.Value.AccountId;
public async Task SendAchievementAsync(AchievementEvent achievementEvent, CancellationToken cancellationToken = default)
{
var response = await http.PostAsJsonAsync(
$"api/v1/accounts/{_accountId}/achievement_events", achievementEvent, cancellationToken);
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
var body = await response.Content.ReadFromJsonAsync<ValidationErrors>(cancellationToken);
throw new InvalidOperationException(string.Join("; ", body?.Errors ?? []));
}
response.EnsureSuccessStatusCode();
}
}
public sealed record ValidationErrors(
[property: JsonPropertyName("errors")] string[] Errors);
Un événement mis en file d'attente renvoie 201 Created avec un objet JSON vide. Un payload qui échoue à la validation renvoie 422 Unprocessable Entity avec les raisons, comme {"errors":["Achievement name can't be blank"]}, et le client les expose dans le message de l'exception. Un token incorrect ou absent renvoie 401 ou 403 sans corps, que EnsureSuccessStatusCode transforme en HttpRequestException.
Étape 6 : l'appeler quand un joueur débloque un succès
Partout où votre backend apprend un déblocage, injectez GameStatsClient et envoyez l'événement. L'application d'exemple le fait dans un endpoint de minimal API que le client du jeu appelle :
app.MapPost("/players/{username}/achievements", async (string username, UnlockRequest request, GameStatsClient gameStats, CancellationToken cancellationToken) =>
{
await gameStats.SendAchievementAsync(new AchievementEvent(
VersionName: request.VersionName,
PlayerUsername: username,
AchievementName: request.AchievementName,
OccurredAt: DateTime.UtcNow), cancellationToken);
return Results.Accepted();
});
UnlockRequest est un record à deux champs avec le nom du succès et le nom de version envoyés par le client du jeu.
Attendre l'envoi avec await dans la requête est le chemin minimal. En option, ajoutez le handler de résilience standard de Microsoft.Extensions.Http.Resilience pour des réessais automatiques, ce qui est sûr puisque renvoyer un déblocage n'a aucun effet, ou déplacez les envois vers une file d'attente en arrière-plan avec Channel<T> et un hosted service si vous ne voulez pas qu'ils soient sur le chemin de la requête.
Le projet complet se trouve dans game-stats-ai-csharp-example-app.