Mark
Precios Crear cuenta
← Todas las publicaciones

Cómo registrar logros en un juego de Godot

Mike Dalton Por Mike Dalton ·

Una ventana de juego y el logo de Godot 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, derrotar a un jefe. Esos registros muestran hasta dónde llegan los jugadores, dónde se detienen y qué contenido nunca encuentran.

Para mostrar cómo se ve eso en Godot, integramos la API de Game Stats AI en Dodge the Creeps, el juego que se construye en el tutorial Your first 2D game de Godot. El proyecto terminado está en nuestro repositorio de ejemplo. Usa Godot 4.7 y GDScript.

Paso 1: decide qué cuenta como un logro

Asigna los logros a momentos que ya existan en tu juego. Dodge the Creeps no tiene niveles: el puntaje sube en uno por cada segundo que el jugador sobrevive. Por eso los logros son hitos de supervivencia a los 10, 30 y 60 segundos.

El puntaje ya se actualiza en main.gd, así que ahí va la llamada que desbloquea el logro:

const SURVIVAL_ACHIEVEMENTS = {
    10: "Survived 10 Seconds",
    30: "Survived 30 Seconds",
    60: "Survived 60 Seconds",
}


func _on_ScoreTimer_timeout():
    score += 1
    $HUD.update_score(score)
    if score in SURVIVAL_ACHIEVEMENTS:
        GameStats.unlock(SURVIVAL_ACHIEVEMENTS[score])

GameStats es el autoload 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 de API, elige Client cuando crees el token. Un token client solo puede enviar telemetría, así que uno filtrado no puede leer los reportes 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 un archivo game_stats.cfg en la raíz del proyecto:

[game_stats]

account_id="your_account_id"
api_token="client_your_token"

Agregar el archivo a .gitignore evita que el token quede en tu repositorio, pero no lo oculta de los jugadores: el archivo va dentro de cada build exportado.

Paso 3: agrega un autoload

Un autoload es un nodo que Godot crea cuando arranca el juego y que se mantiene vivo entre cambios de escena, por eso es un buen lugar para código que cualquier escena puede llamar. Crea game_stats.gd y regístralo como GameStats en Project > Project Settings > Globals > Autoload. Eso agrega esto a project.godot:

[autoload]

GameStats="*res://game_stats.gd"

En _ready, el autoload lee el archivo de configuración y construye la URL y los encabezados una sola vez. Si el archivo no existe, se desactiva y el juego funciona con normalidad:

func _ready():
    var config = ConfigFile.new()
    if config.load(CONFIG_PATH) != OK:
        push_warning("Game Stats AI is disabled: %s is missing." % CONFIG_PATH)
        return

    var base_url = config.get_value("game_stats", "base_url", "https://gamestats.ai")
    var account_id = config.get_value("game_stats", "account_id")
    var api_token = config.get_value("game_stats", "api_token")
    endpoint_url = "%s/api/v1/accounts/%s/achievement_events" % [base_url, account_id]
    headers = PackedStringArray([
        "Authorization: Bearer %s" % api_token,
        "Content-Type: application/json",
    ])
    version_name = ProjectSettings.get_setting("application/config/version")

    load_state()

    http_request = HTTPRequest.new()
    http_request.timeout = 10.0
    http_request.request_completed.connect(_on_request_completed)
    add_child(http_request)

    enabled = true
    send_next_event()

La versión viene de Project Settings > Application > Config > Version. Actualízala antes de publicar cada build, y Game Stats AI agrupa los desbloqueos por esa versión.

Paso 4: construye el payload

El cuerpo tiene cuatro campos obligatorios:

func unlock(achievement_name):
    if not enabled or achievement_name in unlocked_achievements:
        return

    unlocked_achievements.append(achievement_name)
    pending_events.append({
        "version_name": version_name,
        "player_username": player_username,
        "achievement_name": achievement_name,
        "occurred_at": Time.get_datetime_string_from_system(true) + "Z",
    })
    save_state()
    send_next_event()

Time.get_datetime_string_from_system(true) devuelve la hora UTC actual sin sufijo de zona horaria, así que la Z la marca como UTC. 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 un nodo HTTPRequest

HTTPRequest es un nodo que hace peticiones HTTP y HTTPS sin bloquear el juego. Llamas a request, el juego sigue corriendo y el nodo emite request_completed cuando llega la respuesta.

Cada nodo HTTPRequest maneja una petición a la vez, así que el autoload envía el primer evento pendiente y espera la respuesta antes de enviar el siguiente:

func send_next_event():
    if request_in_flight or pending_events.is_empty():
        return

    var body = JSON.stringify(pending_events[0])
    if http_request.request(endpoint_url, headers, HTTPClient.METHOD_POST, body) == OK:
        request_in_flight = true
    else:
        retry_later()


func _on_request_completed(result, response_code, _headers, body):
    request_in_flight = false

    if result == HTTPRequest.RESULT_SUCCESS and response_code == 201:
        pending_events.pop_front()
    elif result == HTTPRequest.RESULT_SUCCESS and response_code == 422:
        push_warning("Game Stats AI rejected an event: %s" % body.get_string_from_utf8())
        pending_events.pop_front()
    else:
        retry_later()
        return

    save_state()
    send_next_event()


func retry_later():
    get_tree().create_timer(RETRY_DELAY_SECONDS).timeout.connect(send_next_event)

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.

Paso 6: guarda los desbloqueos entre sesiones

El autoload guarda tres cosas en user://game_stats_state.cfg: un nombre de jugador generado, los logros ya desbloqueados y los eventos que aún no se han enviado.

func load_state():
    var state = ConfigFile.new()
    state.load(STATE_PATH)
    player_username = state.get_value("player", "username", "")
    if player_username.is_empty():
        player_username = "player_" + Crypto.new().generate_random_bytes(6).hex_encode()
    unlocked_achievements = state.get_value("achievements", "unlocked", [])
    pending_events = state.get_value("achievements", "pending", [])
    save_state()


func save_state():
    var state = ConfigFile.new()
    state.set_value("player", "username", player_username)
    state.set_value("achievements", "unlocked", unlocked_achievements)
    state.set_value("achievements", "pending", pending_events)
    state.save(STATE_PATH)

La lista de desbloqueados hace que cada logro se envíe una vez por jugador, no una vez por partida. La lista de pendientes hace que un desbloqueo hecho sin conexión no se pierda: se envía con su marca de tiempo original la próxima vez que arranca el juego. 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 las exportaciones

Por defecto, Godot solo exporta archivos de recursos, y game_stats.cfg no es un recurso. Sin un paso adicional, el juego exportado no encuentra el archivo y la integración se desactiva.

En Project > Export, abre la pestaña Resources de cada preset de exportación y escribe game_stats.cfg en Filters to export non-resource files/folders.

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

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

Sigue leyendo

© 2026 Rowhome Labs, LLC