Logo
Crear cuenta
← Todas las publicaciones

Cómo registrar logros desde un backend en C++

Mike Dalton Por Mike Dalton ·

Un panel de editor de código y el logo de C++ junto al logo de Game Stats AI, con una flecha que apunta a una tarjeta de dashboard con un trofeo y un gráfico de barras.

El seguimiento de logros registra los hitos que los jugadores alcanzan en tu juego: superar un nivel, encontrar un objeto oculto, vencer a un jefe. Esos registros muestran hasta dónde llegan los jugadores, dónde se detienen y qué contenido nunca encuentran.

Si tu juego ya se comunica con un backend que tú controlas, ese backend es un buen lugar desde donde enviar los eventos de logros. Tu token de la API se queda en tus servidores en lugar de ir dentro del cliente del juego, y la integración funciona igual sin importar qué motor use el cliente.

Para mostrar cómo se ve eso, construimos game-stats-ai-cpp-example-app, un pequeño servicio HTTP sobre Drogon que recibe un desbloqueo desde el juego y lo reenvía a Game Stats AI. Drogon es un framework de C++17 que incluye un servidor HTTP, un cliente HTTP y JSON, así que el código de la aplicación tiene un solo framework con el que trabajar. Esta guía lo recorre paso a paso.

Paso 1: crea un token de servidor

Game Stats AI tiene dos tipos de tokens de API. Los tokens de cliente solo pueden enviar telemetría, así que es seguro incluirlos en el cliente del juego. Los tokens de servidor pueden enviar telemetría y leer reportes, y deben quedarse en un backend de confianza.

Como este código corre en tu backend, crea un token de servidor en gamestats.ai/api_tokens. La misma página muestra el id de la cuenta que la URL del endpoint necesita.

El formulario de nuevo token de API con el nombre diligenciado y el tipo de token Server seleccionado.

Paso 2: configura la compilación con CMake y FetchContent

C++ no tiene un servidor web en su biblioteca estándar, así que la aplicación depende de Drogon. El FetchContent de CMake descarga y compila el propio Drogon en el momento de la configuración, así que nunca instalas ni compilas el framework a mano.

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)

Enlazar con drogon también pone su biblioteca JSON en la ruta de inclusión, así que el resto de la aplicación puede construir un cuerpo de petición sin configuración adicional. Drogon se apoya en tres bibliotecas de C muy comunes, jsoncpp, OpenSSL y zlib, que instalas una vez con tu gestor de paquetes (brew install jsoncpp openssl en macOS, apt-get install libjsoncpp-dev libssl-dev zlib1g-dev en Debian).

Paso 3: guarda el token y el id de la cuenta en la configuración

La aplicación lee el host de la API, el id de la cuenta y el token desde variables de entorno, así que ningún secreto vive en el repositorio. Define el id de la cuenta y el token dondequiera que se ejecute la aplicación:

export GAMESTATS_ACCOUNT_ID=your_account_id
export GAMESTATS_TOKEN=server_your_token_here

Un struct pequeño guarda los tres valores, tomados del entorno. GAMESTATS_BASE_URL es opcional y usa por defecto el host de producción:

#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", ""),
  };
}

Leer cada valor desde el entorno mantiene el desarrollo y la producción idénticos. Si prefieres un archivo para el desarrollo local, guarda los export en un archivo ignorado por git y cárgalo con source antes de iniciar la aplicación.

Paso 4: define el payload del evento

Game Stats AI acepta eventos de logros en un único endpoint:

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

El cuerpo son cuatro campos obligatorios. Un struct los contiene, y un método toJson construye el objeto en snake_case que la API espera:

#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 es una marca de tiempo ISO 8601. Formatear la salida de gmtime_r con una Z al final da el valor UTC que la API espera.

Los logros, los jugadores y las versiones se crean la primera vez que aparecen en un evento, así que no hay nada que registrar por adelantado. Enviar el mismo logro para el mismo jugador otra vez no tiene efecto, lo que hace seguros los reintentos.

Paso 5: configura el cliente HTTP de Drogon

El HttpClient de Drogon se construye una sola vez a partir de la URL base y se reutiliza en cada petición. Una clase pequeña lo guarda junto con el token y el id de la cuenta:

#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_;
};

El cliente analiza el esquema y el host desde la URL base, así que una URL https:// usa TLS en el puerto 443 sin configuración adicional. El callback no devuelve nada si tiene éxito, o un mensaje que describe qué salió mal.

Paso 6: envía el evento y maneja la respuesta

El cliente construye una petición JSON, agrega el encabezado de autorización y la envía. El sendRequest de Drogon es asíncrono, así que nunca bloquea el hilo que lo llama:

#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 serializa el struct y define el encabezado Content-Type, así que solo queda por agregar el encabezado de autorización. Un evento encolado devuelve 201 Created con un objeto JSON vacío. Un payload que falla la validación devuelve 422 Unprocessable Entity con las razones, como {"errors":["Achievement name can't be blank"]}, y el cliente las une en el error reportado. Un token incorrecto o ausente devuelve 401 o 403 sin cuerpo, que el cliente reporta como un código de estado inesperado.

Paso 7: recibe el desbloqueo e inicia el servidor

Dondequiera que tu backend se entere de un desbloqueo, llama al cliente y envía el evento. La aplicación de ejemplo lo hace desde un handler HTTP que el cliente del juego llama:

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

El cuerpo entrante lleva el nombre del logro y el nombre de la versión que envía el cliente del juego, y Drogon pasa el segmento de ruta {username} como el último argumento del handler. El handler completa occurred_at del lado del servidor, luego entrega el evento al cliente y responde 202 Accepted una vez que el envío tiene éxito.

Enviar el evento en línea es el camino mínimo. Como el cliente de Drogon es asíncrono, el handler ya responde sin bloquear un hilo, y reenviar un desbloqueo no tiene efecto, así que reintentar un envío fallido es seguro de agregar.

El proyecto completo está en game-stats-ai-cpp-example-app.

© 2026 Rowhome Labs, LLC