Logo
Crear cuenta
← Todas las publicaciones

Cómo registrar logros en un juego LÖVE 2D

Mike Dalton Por Mike Dalton ·

El logo de Game Stats AI con íconos de un trofeo, un control de videojuegos y un gráfico de barras junto a una ballena azul sonriente.

El seguimiento de logros registra los hitos que los jugadores alcanzan en tu juego: entrar a una zona nueva, encontrar un objeto, vencer 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 un juego LÖVE 2D real, integramos la API de Game Stats AI en Cavern, un juego de plataformas open source.

Esta guía recorre la integración paso a paso.

Paso 1: decide qué cuenta como un logro

Asigna los logros a momentos que existan en tu juego. Para Cavern eso significó la primera visita a cada sala, los seis objetos recogibles, los puntos de guardado, derrotar al jefe y terminar el juego.

Cada uno de esos lugares llama a un módulo gamestats, que el resto de esta guía construye:

-- source/levels/map_loader.lua, when the player enters a room
gamestats:enterRoom(newMap)

-- source/pickup.lua, when the player collects an item
gamestats:pickup(p.name)

-- source/enemies/boss.lua, when the boss dies
gamestats:unlock("Defeated the Boss")

Paso 2: incluye una biblioteca JSON

Lua no trae JSON incorporado, así que incluye rxi/json.lua: un solo archivo que copias en tu proyecto.

json = require("source/libraries/json")

json.encode construye el cuerpo de la petición, y json.decode sirve para inspeccionar los rechazos de la API como {"errors":["Occurred at can't be blank"]}.

Paso 3: construye el payload

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

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. Reserva los tokens de servidor para las solicitudes que hagas desde un backend que tú controles.

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

El cuerpo son cuatro campos obligatorios:

local payload = json.encode({
  version_name = "1.0.0",
  player_username = "cavern_player_a7f3c2",
  achievement_name = name,
  occurred_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
})

Los logros, jugadores y versiones se crean con el primer uso, así que no hay nada que registrar por adelantado.

Paso 4: usa el módulo https de LÖVE 12

LÖVE 12 incluye lua-https, así que no necesitas una biblioteca externa para hacer una petición HTTPS.

local https = require("https")

local code, body = https.request(url, {
  method = "POST",
  headers = {
    ["Authorization"] = "Bearer " .. token,
    ["Content-Type"] = "application/json",
  },
  data = payload,
})

Si todavía estás en LÖVE 11.x, no hay soporte HTTPS incorporado; tendrías que compilar lua-https por tu cuenta o enviar la petición mediante una herramienta externa.

Paso 5: mueve la petición a un hilo separado

No llames https.request en el hilo principal. Bloquea hasta que el servidor responde, así que cada logro congelaría el juego durante un viaje completo de red. Empujar un mensaje a un canal de love.thread cuesta una fracción de milisegundo.

La solución es un hilo de trabajo persistente. El hilo principal solo empuja a un canal; el worker hace la llamada bloqueante y devuelve el resultado:

-- source/threads/gamestats_worker.lua
local https = require("https")

local requests = love.thread.getChannel("gamestats")
local results = love.thread.getChannel("gamestats_result")

while true do
  local job = requests:demand()
  if job == "__quit__" then break end

  local id, url, token, body =
    string.match(job, "^([^\n]*)\n([^\n]*)\n([^\n]*)\n(.*)$")

  local ok, code, response = pcall(https.request, url, {
    method = "POST",
    headers = {
      ["Authorization"] = "Bearer " .. token,
      ["Content-Type"] = "application/json",
    },
    data = body,
  })

  local status = (ok and code) and tostring(code) or "000"
  results:push(id .. "|" .. status .. "|" .. tostring(response or ""))
end

Arráncalo una vez al cargar:

gamestats.thread = love.thread.newThread("source/threads/gamestats_worker.lua")
gamestats.thread:start()

El pcall importa: una conexión caída lanza un error en lugar de devolver un código de estado, y eso debe contar como un fallo reintentable, no matar el hilo.

Un detalle: agrega un handler de love.quit que empuje "__quit__" y espere al hilo, o el juego se cuelga al salir:

function love.quit()
  gamestats:quit() -- pushes "__quit__", then thread:wait()
end

Paso 6: encola los eventos y maneja las respuestas

Mantén los eventos pendientes en una cola, una simple tabla Lua en memoria, y entrégaselos al worker desde ahí. Cavern va un paso más allá y primero escribe cada payload a disco: así los eventos sobreviven a un cierre inesperado o a una sesión sin conexión y se pueden reenviar tal cual en el siguiente arranque. Esa persistencia es opcional: empieza con la cola en memoria y agrega la escritura a disco solo si perder un evento por un cierre inesperado importa en tu juego.

Despacha como máximo un evento encolado por frame, para que una ráfaga de desbloqueos nunca inunde el worker.

La integración completa, incluido el código de cola, reintentos e identidad que esta guía resume, está en nuestro fork de Cavern.

© 2026 Rowhome Labs, LLC