Mark
Precios Crear cuenta
← Todas las publicaciones

Cómo registrar logros en un juego de Unity

Mike Dalton Por Mike Dalton ·

Una ventana de juego de escritorio con un cuadrado azul y un orbe dorado, y 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, como un puntaje alto, un nivel superado o un secreto encontrado. En un juego de Unity, un script que corre en la máquina del jugador envía cada hito a una API mientras el juego sigue corriendo.

Para mostrar cómo se ve eso, construimos Orb Rush, un juego pequeño de Unity en el que el jugador tiene 30 segundos para recoger tantos orbes como pueda, e integramos en él la API de Game Stats AI. El proyecto terminado está en nuestro repositorio de ejemplo. Usa Unity 6000.5 y C#, y está pensado para builds de escritorio. Si también estás considerando el servicio de Unity, consulta Unity Analytics en 2026: precios, límites y alternativas.

Paso 1: decide qué cuenta como un logro

Asigna los logros a momentos que ya existan en tu juego. En Orb Rush, el puntaje sube en uno por cada orbe que el jugador recoge, así que los logros son recoger 10, 20 y 30 orbes en una sola ronda.

El script del juego ya maneja un orbe recogido, así que ahí va la llamada que desbloquea el logro:

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 es el componente que el resto de esta guía construye.

Paso 2: crea un token client

Game Stats AI recibe 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

Este token va dentro de tu juego, que los jugadores descargan y ejecutan, así que trátalo como público. Crea un token client, no un token de servidor. En Configuración > Tokens API, elige Client cuando crees el token. Un token client solo puede enviar telemetría, así que uno filtrado no puede leer los informes de tu cuenta.

La pantalla de nuevo token de API, con un campo de nombre y una opción de tipo de token Client o Server

Guarda el id de la cuenta y el token en Assets/StreamingAssets/game_stats.json:

{
  "account_id": "your_account_id",
  "api_token": "client_your_token"
}

Agrega el archivo y su archivo .meta a .gitignore para que el token no quede en tu repositorio. El paso 7 explica por qué los jugadores igual pueden leerlo.

Paso 3: agrega un componente GameStats que se mantenga vivo

Unity llama a un método estático marcado con [RuntimeInitializeOnLoadMethod] cuando arranca el juego. Con BeforeSceneLoad, se ejecuta antes de que cargue la primera escena, así que GameStats crea su propio GameObject y ninguna escena necesita contenerlo. DontDestroyOnLoad mantiene vivo ese GameObject entre cambios de escena, así que cualquier script puede llamar a 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());
}

En Awake, el componente lee el archivo de configuración y construye la URL una sola vez. Si el archivo no existe, se desactiva y el juego funciona con normalidad.

JsonUtility llena los campos de una clase [Serializable] con las claves JSON que tienen el mismo nombre:

[Serializable]
class GameStatsConfig
{
    public string base_url = "https://gamestats.ai";
    public string account_id;
    public string api_token;
}

Los campos que no están en el archivo conservan su valor por defecto, así que base_url apunta a Game Stats AI a menos que agregues una clave base_url.

Paso 4: construye el payload

El cuerpo tiene cuatro campos obligatorios:

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

Los nombres de los campos coinciden con las claves JSON que espera la API, porque JsonUtility usa los nombres de los campos como claves. Application.version viene de Edit > Project Settings > Player > Version. Actualízala antes de publicar cada build, y Game Stats AI agrupa los desbloqueos por esa versión. DateTime.UtcNow.ToString("o") devuelve la hora UTC actual en formato ISO 8601, por ejemplo 2026-10-12T21:47:13.0283120Z. Los logros, los jugadores y las versiones se crean la primera vez que se usan, así que no hay nada que registrar por adelantado.

El evento recibe su occurred_at una sola vez, cuando el jugador desbloquea el logro, y se guarda con esa marca de tiempo. Game Stats AI ignora un desbloqueo que ya recibió solo cuando coinciden el jugador, el logro, la versión y occurred_at. Un reintento que reconstruyera el payload con una marca de tiempo nueva registraría un segundo desbloqueo.

Paso 5: envía los eventos con UnityWebRequest en una corrutina

Una corrutina es un método que Unity puede pausar y reanudar entre frames. Cuando una corrutina hace yield de SendWebRequest(), Unity la pausa hasta que llega la respuesta, y mientras tanto el juego sigue corriendo.

La corrutina envía el primer evento pendiente y espera la respuesta antes de enviar el siguiente:

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 significa que el evento fue aceptado. Un 422 significa que el payload en sí no es válido, por ejemplo {"errors":["Version name can't be blank"]}, así que volver a enviarlo no sirve: regístralo en el log y sigue con el siguiente. Cualquier otro resultado, incluida la falta total de conexión, deja el evento al frente de la cola y lo intenta de nuevo en 30 segundos. La variable sending asegura que solo corra una copia de la corrutina a la vez, así que Unlock agrega a la cola mientras hay una petición en curso.

Paso 6: guarda los desbloqueos entre sesiones

El componente guarda tres cosas en un archivo JSON dentro de Application.persistentDataPath, una carpeta por usuario que Unity conserva entre ejecuciones: un nombre de jugador generado, los logros ya desbloqueados y los eventos que aún no se han enviado.

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

La lista de desbloqueados hace que cada logro se envíe una vez por jugador, no una vez por ronda. La lista de pendientes hace que un desbloqueo hecho sin conexión no se pierda. Para probarlo, detuvimos el servidor, jugamos una ronda de Orb Rush y volvimos a abrir el juego cuando el servidor estuvo de nuevo en línea: los tres desbloqueos se enviaron al arrancar con sus marcas de tiempo originales. Si tu juego ya tiene cuentas de jugador, usa ese id en player_username en lugar de uno generado.

Paso 7: incluye el archivo de configuración en tus builds

Unity copia todo lo que está en Assets/StreamingAssets en cada build sin cambios, así que el archivo de configuración no necesita ningún paso adicional. En un build de macOS, queda en OrbRush.app/Contents/Resources/Data/StreamingAssets/game_stats.json.

Cualquier persona que descargue tu juego puede leer este archivo, por eso debe contener un token client y nunca un token de servidor.

Paso 8: mira los desbloqueos en un informe

Para comparar logros, abre Informes, elige Nuevo informe, selecciona Informe de logros y marca los logros que quieres incluir.

El informe de abajo viene de nuestras propias partidas de prueba de Orb Rush más jugadores simulados enviados al mismo endpoint. Los 213 jugadores recogieron 10 orbes, 116 (54 %) recogieron 20 y 36 (17 %) recogieron 30:

Un informe de logros de Orb Rush con un gráfico de barras y una tabla de cuántos jugadores desbloquearon cada logro de orbes

La integración completa está en el repositorio de ejemplo.

Sigue leyendo

© 2026 Rowhome Labs, LLC