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-cpp-example-app, un petit service HTTP sur Drogon qui reçoit un déblocage depuis le jeu et le transmet à Game Stats AI. Drogon est un framework C++17 qui embarque un serveur HTTP, un client HTTP et du JSON, donc le code de l'application n'a qu'un seul framework à utiliser. 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 : configurer la compilation avec CMake et FetchContent
C++ n'a pas de serveur web dans sa bibliothèque standard, donc l'application dépend de Drogon. Le FetchContent de CMake télécharge et compile Drogon lui-même au moment de la configuration, donc vous n'installez ni ne compilez jamais le framework à la main.
cmake_minimum_required(VERSION 3.16)
project(game-stats-ai-cpp-example-app CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
drogon
GIT_REPOSITORY https://github.com/drogonframework/drogon
GIT_TAG v1.9.13
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(drogon)
add_executable(game-stats-ai-cpp-example-app
main.cc
GameStatsClient.cc
)
target_link_libraries(game-stats-ai-cpp-example-app PRIVATE drogon)
Lier avec drogon met aussi sa bibliothèque JSON sur le chemin d'inclusion, donc le reste de l'application peut construire un corps de requête sans configuration supplémentaire. Drogon s'appuie sur trois bibliothèques C répandues, jsoncpp, OpenSSL et zlib, que vous installez une fois avec votre gestionnaire de paquets (brew install jsoncpp openssl sur macOS, apt-get install libjsoncpp-dev libssl-dev zlib1g-dev sur Debian).
Étape 3 : stocker le token et l'id de compte dans la configuration
L'application lit l'hôte de l'API, l'id de compte et le token depuis des variables d'environnement, donc aucun secret ne vit dans le dépôt. Définissez l'id de compte et le token partout où l'application s'exécute :
export GAMESTATS_ACCOUNT_ID=your_account_id
export GAMESTATS_TOKEN=server_your_token_here
Un petit struct contient les trois valeurs, lues depuis l'environnement. GAMESTATS_BASE_URL est optionnel et vaut par défaut l'hôte de production :
#pragma once
#include <cstdlib>
#include <string>
struct Config {
std::string baseUrl;
std::string accountId;
std::string token;
};
inline std::string envOrDefault(const char *name, const std::string &fallback) {
const char *value = std::getenv(name);
return value ? std::string(value) : fallback;
}
inline Config loadConfig() {
return Config{
envOrDefault("GAMESTATS_BASE_URL", "https://gamestats.ai"),
envOrDefault("GAMESTATS_ACCOUNT_ID", ""),
envOrDefault("GAMESTATS_TOKEN", ""),
};
}
Lire chaque valeur depuis l'environnement garde le développement et la production identiques. Si vous préférez un fichier pour le développement local, gardez les export dans un fichier ignoré par git et chargez-le avec source avant de démarrer l'application.
Étape 4 : 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 struct les contient, et une méthode toJson construit l'objet en snake_case que l'API attend :
#pragma once
#include <chrono>
#include <ctime>
#include <json/json.h>
#include <string>
struct AchievementEvent {
std::string versionName;
std::string playerUsername;
std::string achievementName;
std::string occurredAt;
Json::Value toJson() const {
Json::Value body;
body["version_name"] = versionName;
body["player_username"] = playerUsername;
body["achievement_name"] = achievementName;
body["occurred_at"] = occurredAt;
return body;
}
};
inline std::string currentTimestamp() {
const auto now = std::chrono::system_clock::now();
const std::time_t seconds = std::chrono::system_clock::to_time_t(now);
std::tm utc{};
gmtime_r(&seconds, &utc);
char formatted[sizeof("2026-08-10T12:00:00Z")];
std::strftime(formatted, sizeof(formatted), "%Y-%m-%dT%H:%M:%SZ", &utc);
return formatted;
}
occurred_at est un horodatage ISO 8601. Formater la sortie de gmtime_r avec un Z final donne la valeur UTC que l'API attend.
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 5 : configurer le client HTTP Drogon
Le HttpClient de Drogon est construit une seule fois à partir de l'URL de base et réutilisé pour chaque requête. Une petite classe le conserve avec le token et l'id de compte :
#pragma once
#include "AchievementEvent.h"
#include "Config.h"
#include <drogon/HttpClient.h>
#include <functional>
#include <optional>
#include <string>
class GameStatsClient {
public:
explicit GameStatsClient(const Config &config);
using ResultCallback = std::function<void(std::optional<std::string> error)>;
void sendAchievement(const AchievementEvent &event, ResultCallback callback);
private:
drogon::HttpClientPtr httpClient_;
std::string token_;
std::string accountId_;
};
Le client analyse le schéma et l'hôte depuis l'URL de base, donc une URL https:// utilise TLS sur le port 443 sans configuration supplémentaire. Le callback ne renvoie rien en cas de succès, ou un message décrivant ce qui a mal tourné.
Étape 6 : envoyer l'événement et gérer la réponse
Le client construit une requête JSON, ajoute l'en-tête d'autorisation et l'envoie. Le sendRequest de Drogon est asynchrone, donc il ne bloque jamais le thread qui l'appelle :
#include "GameStatsClient.h"
#include <drogon/HttpRequest.h>
#include <drogon/HttpTypes.h>
using namespace drogon;
namespace {
std::string joinValidationErrors(const HttpResponsePtr &response) {
const auto body = response->getJsonObject();
if (!body || !(*body)["errors"].isArray()) {
return "unknown validation error";
}
std::string joined;
for (const auto &reason : (*body)["errors"]) {
if (!joined.empty()) {
joined += "; ";
}
joined += reason.asString();
}
return joined;
}
} // namespace
GameStatsClient::GameStatsClient(const Config &config)
: httpClient_(HttpClient::newHttpClient(config.baseUrl)),
token_(config.token),
accountId_(config.accountId) {}
void GameStatsClient::sendAchievement(const AchievementEvent &event,
ResultCallback callback) {
auto request = HttpRequest::newHttpJsonRequest(event.toJson());
request->setMethod(Post);
request->setPath("/api/v1/accounts/" + accountId_ + "/achievement_events");
request->addHeader("Authorization", "Bearer " + token_);
httpClient_->sendRequest(
request, [callback = std::move(callback)](
ReqResult result, const HttpResponsePtr &response) {
if (result != ReqResult::Ok) {
callback("could not reach game stats");
return;
}
const int status = static_cast<int>(response->getStatusCode());
if (status == k422UnprocessableEntity) {
callback("game stats rejected the event: " +
joinValidationErrors(response));
return;
}
if (status >= k300MultipleChoices) {
callback("game stats returned status " + std::to_string(status));
return;
}
callback(std::nullopt);
});
}
newHttpJsonRequest sérialise le struct et définit l'en-tête Content-Type, il ne reste donc que l'en-tête d'autorisation à ajouter. 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 regroupe dans l'erreur signalée. Un token incorrect ou absent renvoie 401 ou 403 sans corps, que le client signale comme un code de statut inattendu.
Étape 7 : recevoir le déblocage et démarrer le serveur
Partout où votre backend apprend un déblocage, appelez le client et envoyez l'événement. L'application d'exemple le fait depuis un handler HTTP que le client du jeu appelle :
#include "AchievementEvent.h"
#include "Config.h"
#include "GameStatsClient.h"
#include <drogon/drogon.h>
#include <functional>
#include <memory>
#include <optional>
#include <string>
using namespace drogon;
int main() {
auto client = std::make_shared<GameStatsClient>(loadConfig());
app().registerHandler(
"/players/{username}/achievements",
[client](const HttpRequestPtr &request,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &username) {
const auto unlock = request->getJsonObject();
if (!unlock) {
auto response = HttpResponse::newHttpResponse();
response->setStatusCode(k400BadRequest);
callback(response);
return;
}
AchievementEvent event;
event.versionName = (*unlock)["versionName"].asString();
event.playerUsername = username;
event.achievementName = (*unlock)["achievementName"].asString();
event.occurredAt = currentTimestamp();
client->sendAchievement(
event, [callback](std::optional<std::string> error) {
auto response = HttpResponse::newHttpResponse();
response->setStatusCode(error ? k502BadGateway : k202Accepted);
callback(response);
});
},
{Post});
LOG_INFO << "listening on :8080";
app().addListener("0.0.0.0", 8080).run();
}
Le corps entrant transporte le nom du succès et le nom de version envoyés par le client du jeu, et Drogon passe le segment de chemin {username} comme dernier argument du handler. Le handler remplit occurred_at côté serveur, puis confie l'événement au client et répond 202 Accepted une fois l'envoi réussi.
Envoyer l'événement en ligne est le chemin minimal. Comme le client de Drogon est asynchrone, le handler répond déjà sans bloquer un thread, et renvoyer un déblocage n'a aucun effet, donc réessayer un envoi échoué est sans risque à ajouter.
Le projet complet se trouve dans game-stats-ai-cpp-example-app.