Je game is een harness

Ik heb een presentatie van 45 minuten gegeven met de titel “Your Game Is A Harness” tijdens Indie Rodeo in Calgary op 12 september 2026. Na gesprekken met veel aanwezigen na mijn presentatie, dacht ik dat het goed zou zijn om de ideeën breder te delen in een vorm die ook toegankelijk is voor mensen die niet bij de conferentie waren.

Daarnaast, omdat ik op deze blog veel over AI spreek, nemen mensen vaak aan dat ik AI gebruik om te schrijven. Dat is niet waar! Alles hieronder is 100% door een mens geschreven; ik heb deze post geschreven met enkel de slides van mijn presentatie als referentie (dit is geen transcriptie). Hoewel ik ervan geniet om AI te gebruiken voor het schrijven van code en tools, haal ik een vergelijkbaar plezier uit het schrijven op "de oude manier", zeker omdat ik geen reden heb om dit proces efficiënter te maken.

Dus, nu dat duidelijk is, presenteer ik mijn eerste "blogificatie" van een presentatie. Veel leesplezier!

Introductie

Ik wil het hebben over het gebruik van Large Language Models (LLM's) in game-ontwikkeling. Niet over 'one-shot' demo's die er leuk uitzien op Twitter, maar over echte ontwikkeling. Waar je acht uur per dag, vijf dagen per week aan werkt.

Voordat ik begin, is het nuttig om de basis van LLM's op een rij te zetten, zodat de rest van mijn betoog begrijpelijk is. Voor velen zal dit herhaling van zetten zijn, dus wie direct naar de 'dev-loop' wil gaan, kan naar de volgende sectie scrollen.

Mijn weg naar het gebruik van LLM's voor ontwikkelwerk begon met een moodboarding-site die ik maakte genaamd mood.site. Dit was vóór de tijd van "agentic" AI, maar ik wilde testen of ChatGPT destijds daadwerkelijk code kon schrijven. Het proces bestond grotendeels uit het blindelings kopiëren en plakken van code in de webinterface van ChatGPT. Het proces was verschrikkelijk, maar ik was verrast door de resultaten. Ik heb de site gebouwd en hij werkte! Ik moest nog steeds veel zaken zelf verifiëren, maar het was genoeg om me te overtuigen. Zonder toegang tot een LLM had ik deze site nooit in zo'n korte tijd kunnen realiseren (ongeveer 2-3 weken in de avonden, totaal zo'n 20 uur). Ik zou waarschijnlijk vandaag nog steeds werken aan de eerste versie.

Sindsdien is mood een testbed geworden voor mijn experimenten met LLM's (en op zichzelf een mild succesvol project!), wat me heeft geïnspireerd om veel andere projecten met vergelijkbare stacks op te starten. Daarnaast zijn LLM's beter geworden, vooral met tools zoals Claude Code en latere Claude-modellen. Vanuit mijn eigen ervaring voelt het alsof er tegenwoordig weinig tot niets is dat een LLM niet kan wanneer het gaat om een web-gebaseerde stack.

Het is makkelijk om dan te concluderen dat LLM's alles kunnen, aangezien zoveel software websoftware is. Maar telkens wanneer ik een LLM probeerde te gebruiken voor een non-web game-project, was de conclusie steevast:

"LLM's zijn slecht in games"

Elke keer dat ik een LLM inzette voor iets 'game-achtigs', liep het vast. Misschien is dit mijn eigen bias. Ik ben bijna 15 jaar een game-programmeur; ik denk dat ik weet hoe dingen werken. Ik ruik "slechte" game-code van een kilometer afstand. Maar als het gaat om Svelte web-apps ben ik veel minder onderlegd en ben ik geneigd te denken dat de code die de LLM produceert "goed" is. Misschien is het eigenlijk troep. Er is echter reden om te geloven dat de Svelte- en web-stack content van kwaliteit is, mede omdat er enorm veel kwalitatieve webprojecten met open source code online staan. In vergelijking daarmee vermoed ik dat er voor games orden van grootte minder data beschikbaar is om op te trainen.

Bovendien bevatten games veel zaken die geen code zijn. Games gaan niet alleen over het plaatsen van statische afbeeldingen en tekst in een layout. Er zijn talloze elementen in games die niet eenvoudig in code te representeren zijn. En bovenop dat alles komt de tekstlayout van het web, maar dan slechts als een subset van de honderd andere systemen die een game moet afhandelen.

Dit is mede de reden waarom veel mensen games maken in Three.js — ze kunnen assets eenvoudig representeren als simpele boxy-primitieven en slaan zo veel van het asset-generatie-, conformerings- en verwerkingswerk over dat normaal gesproken vereist is voor games. Als gevolg daarvan is veel van dat werk "slop" (oppervlakkig), omdat het geen echte auteursstem heeft buiten het gemiddelde van de LLM.

Toch hebben ze misschien een punt.

Een kernidee van de one-shot Three.js workflow (en de bekwaamheid van LLM's bij het bouwen van web-apps) is dat alles code is. Door alles als code te hebben, is het voor een LLM zeer eenvoudig om erover na te denken, wijzigingen aan te brengen, etc.

Bij traditionele games is dit echter niet het geval. Games hebben overduidelijk code, maar er is veel andere content die dat niet is. Dit betekent dat er cruciale componenten in games zijn die effectief opaak (ondoorzichtig) zijn voor een LLM. Als je game vervolgens vertrouwt op het aansturen en transformeren van die opaque zaken, dan zal een LLM vastlopen bij het werken aan je game.

Ik denk dat een groot deel van de gamedev-community LLM's een kans heeft gegeven en direct tegen dit probleem is aangelopen. Het sentiment dat "LLM's slecht/niet nuttig zijn" (met de grote kanttekening dat ik hier puur over capaciteit spreek en niet over potentiële maatschappij-beëindigende implicaties) kwam voort uit het rechtstreeks botsen met deze beperkingen.

Kortom: structureel gezien zijn LLM's slecht in het "lezen" van games. Tenminste, in hun standaardtoestand.

LLM Basisprincipes

Laten we dieper ingaan op het waarom, en wat we daaraan kunnen doen.

Elke call of prompt naar een LLM-service bestaat uit drie basiscomponenten: een system prompt, een assistant prompt en een user prompt.

  • System prompt: Generieke tekst die gedeeld wordt door alle gebruikers van een service. Dit beschrijft meestal de basis van de LLM zelf, zoals: “Je bent Opus 5, een intelligent Large Language Model gemaakt door Anthropic…”. Dit is niet om te zeggen dat ze niet belangrijk zijn (dat zijn ze wel), maar dat ze minder relevant zijn voor ons specifieke gebruiksscenario.
  • Assistant prompt: Dit is een soort sidecar-prompt die bedoeld is om de AI te begeleiden in waar hij op reageert. Je kunt het zien als de basis "interactieve" vector van het model met jouw prompt, en het representeert de "reactie" van het model op de gebruiker op basis van bepaalde input. Meestal hoef je hier niet over na te denken.
  • User Prompt: Dit is de kern. Bijna elke input die je in een AI-tool stopt, komt hier terecht. Elke verzonden chat, code, etc. wordt in deze sectie gedumpt. Wanneer mensen spreken over "context", bedoelen ze grotendeels "hoeveel tokens zijn opgeslagen in het user- (en assistant-) gedeelte van een prompt".

Het is makkelijk om te denken dat elke chat een "frisse" versie is van bovenstaande, maar wat er eigenlijk gebeurt is dat, terwijl je met een model werkt in een bepaalde sessie, elk vorig bericht dat je naar het model hebt gestuurd, opnieuw wordt ingediend telkens wanneer je een nieuw bericht stuurt. Het is een beetje als dat oude spel op het schoolplein waarbij je op jouw beurt eerst alles moet herhalen wat de anderen hebben gezegd, voordat je je eigen toevoeging doet voor de volgende persoon.

Context en Contextvervuiling

Door dit herhaaldelijk te doen, bouw je de "context" op van de sessie waarin je werkt.

Dit is prima, maar een belangrijk punt is dat je eigenlijk alleen de "laatste" zaak in je context kunt adresseren (door een nieuw bericht te sturen). Zaken uit eerdere berichten worden steeds opnieuw naar het model gestuurd. Daarom wil je er zeker van zijn dat wat er in je context staat "goed" is, anders kun je in degeneratieve scenario's belanden waarbij je context vervuild raakt omdat "slechte" zaken uit het verleden continu nieuw werk beïnvloeden.

Het beheren van context is dus een sleutelfactor bij het werken met LLM's, en er is een nieuwe engineering-praktijk ontstaan genaamd “context engineering”. Context engineering stelt de vraag: “hoe kunnen we de beste informatie in de context krijgen, met zo min mogelijk vervuiling?”.

Kortom: het ervoor zorgen dat LLM's goed werken met games is een contextprobleem, niet iets dat intrinsiek gekoppeld is aan hoe games of LLM's "zijn" (of "niet zijn").

De complicerende factor hier is dat games vreemd zijn. Of liever gezegd: we hebben geen echt template om een game te bouwen. We hebben game-engines, maar er is effectief nul gestandaardiseerde tooling voor iets, mede omdat elke game zijn eigen behoeften anders manifesteert.

Dit is uniek voor games (naar mijn mening), maar het manifesteert zich genoeg in andere technologische domeinen dat er een nieuw concept is ontstaan om agents effectief te laten "werken" met wat dan ook je product is: Harnesses.

Het concept van een Harness

Het algemene idee van een Harness is dat het een set patronen van acties is die een agent moet ondernemen in reactie op een goed (of losjes) gedefinieerde verwachte input. Je gebruikt waarschijnlijk al een harness zoals Claude Code, maar deze kunnen worden uitgebreid, gedestilleerd of geabstraheerd om het specifieke werk dat je probeert te doen beter te ondersteunen.

De kernvraag hier is: “hoe kan ik beter begrijpen wat ik bouw, en dat proces begeleiden terwijl het wordt ontwikkeld”. Harnesses kunnen zaken koppelen zoals iteratie met een browser, telemetrie, profiling, en zelfs volledige SDLC-taken zoals het maken van tickets en documenten.

Echter, wat geen van deze tools goed doet, is daadwerkelijk communiceren met je game. Je game is een black box, en het meeste wat zichtbaar is voor een LLM is enkel de code zoals die geschreven is, wat we al bespraken niet voldoende is. We weten dat LLM's al goed zijn in het werken met code, dus een harness op zich lost het probleem "games zijn niet alleen code" niet op.

Wat er echt moet gebeuren, is dat we alle elementen in games proberen om te zetten in iets waar de LLM mee kan werken. We moeten de black box van je game openbreken en meer ervan direct adresseerbaar maken voor een LLM.

Maak je game leesbaar voor AI-agents

Hier zijn concrete, actievolle zaken die je kunt doen om je game leesbaarder en werkbaarder te maken voor een LLM. Merk op dat veel van deze tips over het algemeen goed zijn voor game-ontwikkeling; ze worden simpelweg versterkt door AI.

Vaste willekeur (Fixed Randomness)

Een terugkerend thema is het concept van determinisme: hoe nauwkeurig kun je een zeer specifieke staat in je game reproduceren. Het doel is dat dit zeer eenvoudig en exact moet zijn. Dit aan het einde van je productieproces implementeren zal extreem moeilijk zijn en vereist waarschijnlijk fundamentele wijzigingen in het elfde uur.

Als je deze beperking echter hanteert voordat je je eerste regel code schrijft, plavei je de weg naar een veel productiever eindstadium en voorkom je dat je moet vechten tegen entropie en spaghetti-code.

Begin simpel: maak aan het begin van je game één centrale bron van seeded randomness. Zorg dat elke call naar iets non-deterministisch deze bron gebruikt. Dit betekent dat je game bijna elke staat deterministisch kan recreëren. Als je code op 30 verschillende plekken Random.Float aanroept om gameplay-gedrag te bepalen, weet je dat de 30ste call elke keer hetzelfde resultaat geeft.

Screenshots en visuele verificatie

Waarom reproduceerbare staten? Voor een agent is het essentieel om keer op keer exact dezelfde staat te kunnen bereiken zonder te worstelen met replicatiefouten, zeker wanneer er herhaaldelijk wordt geïtereerd om iets te corrigeren.

En zodra je daar bent, hoe weet je dan of het werkt? Hoe evalueer je de wijzigingen? Screenshots! De meeste engines hebben ingebouwde screenshot-API's. Ik raad ook aan de mogelijkheid toe te voegen om UI-lagen of gameplay-lagen eenvoudig aan/uit te zetten, zodat visuele ruis de LLM niet afleidt van de laag die aangepakt moet worden.

Het maken van een "voor en na" foto tussen agent-wijzigingen is een mooie manier voor een agent om te begrijpen wat er is veranderd. ...Of is het dat?

Wanneer je twee screenshots neemt, zal een LLM — zelfs een multi-modale met visuele capaciteiten — waarschijnlijk grote moeite hebben om niet alleen te begrijpen wat de wijziging is, maar ook of de wijziging het dichter bij het einddoel brengt. Deze ruis uit imagedata is een vorm van contextvervuiling: het naïef voeren van screenshots aan modellen kan hun gevoel voor progressie vertroebelen.

Image Diffing

Je moet de LLM dwingen om alleen te denken aan wat er moet veranderen. Hoe beoordeel je wat er is veranderd? Image diffing.

Als je twee afbeeldingen "diffed", zoals je dat doet met code-wijzigingen, zie je direct wat er is veranderd. In combinatie met slimme cropping kan een LLM met een kwantitatieve maatstaf begrijpen hoe dicht de output na de wijziging bij het doelbeeld ligt.

Er is een gratis command-line tool genaamd odiff die dit doet. odiff vindt het verschil tussen twee afbeeldingen en kan dit als een "diff"-afbeelding outputten. Daarnaast kan het een kwantitatieve waarde geven, zoals "85% gelijkaardig", wat een LLM zeer goed kan lezen en kan gebruiken als stabiele baseline voor evaluatie bij elke beurt.

De workflow wordt dan: screenshot → progress diff check → evaluatie. De inner-loop waar de agent de meeste tijd doorbrengt, ziet er als volgt uit: Wijziging maken → Verifiëren → Herhalen tot resultaat behaald.

Het doel is om elke wijziging zo minimaal mogelijk te maken voor het gewenste resultaat, en deze snel te kunnen verifiëren. Je wilt een stabiele, "saaie" baseline om te voorkomen dat er technische schuld (spaghetti-code) ontstaat.

Deterministische State

Ik wil specifieke strategieën bespreken voor het bereiken van deterministische state, aangezien deze essentieel zijn.

Een simpele vraag om te bepalen hoe dicht je bij deterministische state bent: hoe gemakkelijk kun je een exacte staat in je game opslaan en laden? Strategisch nadenken over save/load vanaf het begin van de ontwikkeling is een "superpower".

Thing-Driven Development

In object-georiënteerde (OOP) architecturen ontdekken veel ontwikkelaars aan het einde van hun project dat zaken die theoretisch verschillend zijn (bijv. een Boom en een Hond), in de code eigenlijk bijna hetzelfde zijn. Beiden hebben een mesh en een collider. Een hond kan bewegen, maar "beweging" is niet exclusief voor honden. Ze zijn in feite composities van componenten.

In plaats van een strikt OOP-model, stel ik voor om ervan uit te gaan dat alles in feite hetzelfde type object is (bijvoorbeeld het type Thing). Een Thing zou er in code zo uit kunnen zien:

struct Thing {
    int id;
    ThingType type; // Enum: Tree, Dog, Player, etc.
    Vector3 position;
    float health;
    bool isAlive;
    // ... andere gedeelde data
}

AI houdt enorm van deze architectuur. Met één objecttype is er een enkele bron van waarheid. Als je 100 verschillende klassen hebt, moet de AI al die relaties begrijpen, wat de context vult en het redeneren bemoeilijkt. Met één objecttype is er geen ambiguïteit; de AI weet precies waar wijzigingen moeten worden aangebracht.

Bovendien maakt dit saving en loading triviaal. Serialisatie en deserialisatie zijn eenvoudig omdat er maar één type is. Je vermijdt complexe polymorfische deserialisatie en race-conditions bij het koppelen van objectbomen. Een game is dan simpelweg een grote lijst van Things.

Adresserbare State (The Command Pattern)

Als je één objecttype hebt, is het eenvoudig om wijzigingen in de state vast te leggen. Je kunt hier het Command Pattern implementeren voor extra voordeel.

Het idee is dat wijzigingen aan een object niet worden vastgelegd als een losse functieaanroep, maar als een (idealiter serialiseerbaar) object. Deze commando's gaan door een verwerkingsklasse die de wijzigingen aanbrengt.

Stel je voor: een speler voert acties uit die resulteren in commando's, wat leidt tot een eindstaat waarin een bug optreedt. Om dit te reproduceren, hoef je niet de speler te vragen wat hij deed; je kunt simpelweg de startstaat nemen en de geserialiseerde commando's "afspelen". Een AI-agent kan dit ook doen om te beoordelen hoe wijzigingen de state beïnvloeden.

Bovendien kunnen deze commando's in een teksteditor worden geschreven (of door de AI), en vervolgens worden afgespeeld in een draaiende instance van de game, zonder dat er gecompileerd hoeft te worden. Hoe meer je je game kunt aansturen zonder compilatie, hoe sneller de agentic loops kunnen draaien.

Datamanagement

Dit brengt me bij datamanagement. Met "data" bedoel ik de statische, operationele data die beschrijft hoe zaken zijn (bijv. attackDamage, walkSpeed).

Als je deze data direct in structs in je code schrijft, moet je bij elke wijziging hercompileren. Dit veroorzaakt vertraging voor de AI-agent. De betere manier is om alle data extern aan je game te houden. Wijzigingen in data zijn dan slechts een kwestie van het aanpassen van de databron en het opnieuw deserialiseren in de game.

Voor mijn project Cantata was dit de workflow. Niet-programmeurs kregen builds van de game en onze custom data-editor Depot. Ze konden wijzigingen maken in Depot, en de game-build verwerkte deze direct zonder dat ik nieuwe builds hoefde te maken. AI-agents kunnen ook worden getraind om jouw eigen tools te gebruiken, waardoor ze data-wijzigingen kunnen testen zonder constant de game te hoeven hercompileren.

Standalone Simulatie / Blackbox Architectuur

Vaak is de logica van een game vermengd met visuele state-transities en UI-calls. Hierdoor moet je de hele game (modellen, sounds, scenes) draaien om iets te testen of een bug te reproduceren.

Een betere aanpak is dat de simulatie van je game buiten de eigenlijke logica van de engine staat, letterlijk als een eigen DLL of library. De engine communiceert met de simulatie via commando's, vergelijkbaar met hoe clients en servers werken in web-programming.

De engine stuurt een commando naar de simulatie, en de simulatie reageert met de bijgewerkte states. De engine is "dom"; hij voert simpelweg uit wat de simulatie zegt.

Deze abstractie biedt enorme voordelen:

  1. Netwerkprogrammering: Wordt veel eenvoudiger omdat de engine alleen states consumeert; het maakt niet uit of die van een netwerk, een LLM of lokaal komen.
  2. Testing: Je kunt de simulatie testen zonder dat er een engine aan te pas komt. Je kunt tests draaien in pure C#-projecten met standaard tooling. AI houdt hiervan.

Dit concept heet Blackbox Game Architecture. Het stelt je in staat om je game te behandelen als een technisch object dat is geabstraheerd van de uiteindelijke spelerservaring.

Impactbeoordeling (Impact Assessment)

Hoe weten we of wijzigingen de game beïnvloeden zoals we willen? Dit is impactbeoordeling.

Een universeel punt van zorg voor alle games is performance. Traditionele profiling-tools zijn ontworpen voor menselijke consumptie (met call stacks en grafieken), wat AI lastig vindt. AI werkt het best met tekst.

Profiling-tools die raw captures als tekst kunnen dumpen, versnellen de AI-iteratie aanzienlijk. Een tool die ik aanbeveel is samply. Het kan captures naar de console dumpen, waardoor een AI-agent de output kan lezen en autonoom kan evalueren of zijn wijzigingen de performance hebben verbeterd.

Conclusie

Wanneer we al deze tools en technieken combineren, ontstaat er een flywheel: Deterministische input → Gestructureerde wijzigingen (zonder compilatie) → Verificatie in vacuüm → Impactbeoordeling (Profiling/Diffing).

Met een data-georiënteerde, binair-geïsoleerde versie van ontwikkeling kan een agentic turn deze hele loop in seconden doorlopen. Het maakt iteratie significant sneller dan een naïeve aanpak.

Als je dit herformuleert, zie je wat een game-driven harness is: het is je game zelf. Elke game is in staat tot dit alles, wat betekent dat elke game zijn eigen harness kan zijn.

***

Postscript Het was interessant om dit te schrijven na de presentatie. Ik sta achter veel van wat ik heb gezegd, maar ik ben momenteel een project aan het opleveren waar ik niets van dit alles heb gebruikt. Ik denk dat de schaal waar ik hier op mik grotere projecten zijn. Het project dat ik nu shipping is erg klein, dus ik had geen uitgebreide harness nodig. Echter, voor een grotere game waar ik aan werk, is dit absoluut relevant.