Logo
Aanmelden
← Alle artikelen

Achievements bijhouden vanuit een C#-backend

Mike Dalton Door Mike Dalton ·

Een paneel van een code-editor en de C#- en .NET-logo's naast het Game Stats AI-logo, met een pijl naar een dashboardkaart met een trofee en een staafgrafiek.

Achievement-tracking legt de mijlpalen vast die spelers in je game bereiken: een level uitspelen, een verborgen item vinden, een eindbaas verslaan. Die gegevens laten zien hoe ver spelers komen, waar ze afhaken en welke content ze nooit vinden.

Als je game al met een backend praat die jij beheert, is die backend een goede plek om achievement-events vandaan te versturen. Je API-token blijft op je servers in plaats van mee te gaan in de gameclient, en de integratie werkt hetzelfde ongeacht welke engine de client gebruikt.

Om te laten zien hoe dat eruitziet hebben we game-stats-ai-csharp-example-app gebouwd, een kleine ASP.NET Core-service op .NET 8 die een unlock van de game ontvangt en doorstuurt naar Game Stats AI, met alleen HttpClient en System.Text.Json uit de base class library. Deze gids loopt er stap voor stap doorheen.

Stap 1: maak een servertoken aan

Game Stats AI heeft twee soorten API-tokens. Clienttokens kunnen alleen telemetrie versturen en zijn daarom veilig om in een gameclient op te nemen. Servertokens kunnen telemetrie versturen en rapporten lezen, en moeten op een vertrouwde backend blijven.

Omdat deze code op je backend draait, maak je een servertoken aan op gamestats.ai/api_tokens. Dezelfde pagina toont het account-id dat de endpoint-URL nodig heeft.

Het formulier voor een nieuw API-token met de naam ingevuld en het tokentype Server geselecteerd.

Stap 2: bewaar het token en het account-id in configuratie

appsettings.json bevat alleen de host van de API:

"GameStats": {
  "BaseUrl": "https://gamestats.ai"
}

Het token is een geheim, en het account-id op dezelfde manier opslaan houdt beide buiten de repository. Gebruik in development user secrets:

dotnet user-secrets set "GameStats:AccountId" "your_account_id"
dotnet user-secrets set "GameStats:Token" "server_your_token_here"

Stel ze in productie in als de omgevingsvariabelen GameStats__AccountId en GameStats__Token. Alle bronnen binden aan dezelfde sectie, dus de rest van de code hoeft nooit te weten waar de waarden vandaan komen.

Een options-klasse bevat de drie waarden:

namespace GameStatsExample;

public sealed class GameStatsOptions
{
    public string BaseUrl { get; set; } = "https://gamestats.ai";
    public string AccountId { get; set; } = "";
    public string Token { get; set; } = "";
}

Stap 3: definieer de payload van het event

Game Stats AI accepteert achievement-events op een enkel endpoint:

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

De body bestaat uit vier verplichte velden. Een record met JsonPropertyName-attributen koppelt de C#-namen aan de snake_case-namen die de API verwacht:

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);

Op .NET 8 kun je de attributen weglaten en in plaats daarvan PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower instellen in JsonSerializerOptions; de attributen houden het formaat van de API op een plek zichtbaar.

occurred_at is een ISO 8601-tijdstempel. System.Text.Json serialiseert een DateTime in UTC in dat formaat met een Z aan het einde, dus DateTime.UtcNow heeft geen format string nodig.

Achievements, spelers en versies worden aangemaakt zodra ze voor het eerst in een event voorkomen, dus je hoeft vooraf niets te registreren. Hetzelfde achievement nogmaals versturen voor dezelfde speler heeft geen effect, waardoor retries veilig zijn.

Stap 4: registreer een typed HttpClient

Registreer een typed client in Program.cs. IHttpClientFactory beheert de levensduur van verbindingen, en de factory-callback leest de options om het basisadres en het bearer-token een keer in te stellen:

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);
});

Gebruikt je app geen dependency injection, dan werkt een enkele langlevende HttpClient met dezelfde configuratie ook.

Stap 5: verstuur het event en verwerk de respons

De client post het record en inspecteert het resultaat:

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);

Een event dat in de wachtrij is gezet geeft 201 Created terug met een leeg JSON-object. Een payload die de validatie niet haalt geeft 422 Unprocessable Entity terug met de redenen, zoals {"errors":["Achievement name can't be blank"]}, en de client toont ze in het bericht van de exception. Een verkeerd of ontbrekend token geeft 401 of 403 terug zonder body, wat EnsureSuccessStatusCode omzet in een HttpRequestException.

Stap 6: roep het aan wanneer een speler een achievement unlockt

Overal waar je backend een unlock te weten komt, injecteer je GameStatsClient en verstuur je het event. De voorbeeldapp doet dat in een minimal API-endpoint dat de gameclient aanroept:

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 is een record met twee velden, de naam van het achievement en de versienaam die de gameclient meestuurt.

Het versturen inline awaiten is het minimale pad. Voeg optioneel de standaard resilience handler uit Microsoft.Extensions.Http.Resilience toe voor automatische retries, wat veilig is omdat het opnieuw versturen van een unlock geen effect heeft, of verplaats het versturen naar een achtergrondwachtrij met Channel<T> en een hosted service als je het niet op het request-pad wilt.

Het volledige project staat in game-stats-ai-csharp-example-app.

© 2026 Rowhome Labs, LLC