Logo
Aanmelden
← Alle artikelen

Achievements bijhouden in een LÖVE 2D-game

Mike Dalton Door Mike Dalton ·

Het Game Stats AI-logo met pictogrammen van een trofee, een gamecontroller en een staafgrafiek naast een lachende blauwe walvis.

Achievement-tracking legt de mijlpalen vast die spelers in je game bereiken: een nieuw gebied betreden, een item vinden, een eindbaas verslaan. Die gegevens laten zien hoe ver spelers komen, waar ze afhaken en welke content ze nooit vinden.

Om te laten zien hoe dat eruitziet in een echte LÖVE 2D-game hebben we de API van Game Stats AI ingebouwd in Cavern, een open-source platformer.

Deze gids loopt stap voor stap door de integratie.

Stap 1: bepaal wat telt als achievement

Koppel achievements aan momenten die bestaan in je game. Voor Cavern betekende dat het eerste bezoek aan elke kamer, de zes op te pakken items, save-checkpoints, het verslaan van de eindbaas en het uitspelen van de game.

Op elk van die plekken komt een aanroep naar een gamestats-module, die de rest van deze gids opbouwt:

-- 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")

Stap 2: neem een JSON-bibliotheek op

Lua heeft geen ingebouwde JSON, dus neem rxi/json.lua op: een enkel bestand dat je in je project plaatst.

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

json.encode bouwt de request-body, en met json.decode kun je API-afwijzingen zoals {"errors":["Occurred at can't be blank"]} inspecteren.

Stap 3: bouw de payload

Game Stats AI accepteert achievement-events op een enkel endpoint:

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

Dit token zit in je game, die spelers downloaden en uitvoeren, dus behandel het als openbaar. Maak een client-token aan, geen server-token. Kies bij Instellingen > API-tokens voor Client wanneer je het token aanmaakt. Een client-token kan alleen telemetrie versturen, dus een gelekt token kan de rapporten van je account niet lezen. Bewaar server-tokens voor verzoeken die je doet vanaf een backend die je zelf beheert.

Het scherm voor een nieuw API-token, met een naamveld en een keuze voor tokentype Client of Server

De body bestaat uit vier verplichte velden:

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

Achievements, spelers en versies worden bij het eerste gebruik aangemaakt, dus je hoeft vooraf niets te registreren.

Stap 4: gebruik de https-module van LÖVE 12

LÖVE 12 levert lua-https mee, dus je hebt geen externe bibliotheek nodig om een HTTPS-request te doen.

local https = require("https")

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

Zit je nog op LÖVE 11.x, dan is er geen ingebouwde HTTPS-ondersteuning; dan zou je lua-https zelf moeten compileren of het request via een externe tool versturen.

Stap 5: verplaats het request naar een aparte thread

Roep https.request niet aan op de hoofdthread. De aanroep blokkeert totdat de server antwoordt, dus elke achievement zou de game een volledige netwerk-roundtrip lang stilzetten. Een bericht naar een love.thread-kanaal pushen kost een fractie van een milliseconde.

De oplossing is dus een blijvende worker-thread. De hoofdthread pusht alleen naar een kanaal; de worker doet de blokkerende aanroep en stuurt het resultaat terug:

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

Start hem een keer bij het laden:

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

De pcall is belangrijk: een weggevallen verbinding gooit een error in plaats van een statuscode terug te geven, en dat moet tellen als een herhaalbare fout, niet de thread om zeep helpen.

Een valkuil: voeg een love.quit-handler toe die "__quit__" pusht en op de thread wacht, anders blijft de game hangen bij het afsluiten:

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

Stap 6: zet events in een wachtrij en verwerk de antwoorden

Houd wachtende events bij in een wachtrij, een simpele Lua-tabel in het geheugen, en geef ze van daaruit door aan de worker. Cavern gaat een stap verder en schrijft elke payload eerst naar schijf: zo overleven events een crash of een offline sessie en kun je ze bij de volgende start ongewijzigd opnieuw versturen. Die persistentie is optioneel: begin met de wachtrij in het geheugen en voeg schijfopslag alleen toe als het verliezen van een event bij een crash ertoe doet voor je game.

Verstuur hoogstens een event uit de wachtrij per frame, zodat een reeks unlocks de worker nooit overspoelt.

De volledige integratie, inclusief de wachtrij-, retry- en identiteitscode die deze gids inkort, staat in onze fork van Cavern.

© 2026 Rowhome Labs, LLC