Achievements bijhouden in een LÖVE 2D-game
Door Mike Dalton ·
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.


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.