Als in een multiplayergame twee spelers in dezelfde ruimte staan en de een de vijand recht vooruit ziet terwijl de ander hem drie stappen naar links ziet, ligt het probleem bijna altijd in de laag van game state-synchronisatie. De "wereldstaat" die de server en de clients delen, drijft na verloop van tijd uit elkaar, en die afwijking wordt zichtbaar. In dit artikel leg ik uit hoe we consistentie tussen clients opbouwen: het autoritatieve servermodel, tick-gebaseerde updates en interpolatie, praktisch toegelicht.
Wat is state, en waarom moet die worden gesynchroniseerd?
Vanuit het oogpunt van een game is state de momentopname van de wereld op een bepaald moment: de posities, snelheden, levenspunten en inventarissen van spelers, het gedrag van NPC's, open deuren, gevallen voorwerpen. In een singleplayergame leeft dit beeld in één geheugengebied en is geen enkele inconsistentie mogelijk. In een netwerkgame houdt echter elke client zijn eigen kopie van de state bij, en de server zijn eigen kopie. Omdat de lichtsnelheid eindig is, kunnen deze kopieën nooit op hetzelfde moment worden bijgewerkt; ons doel is niet om inconsistentie te elimineren, maar om die klein en kortstondig genoeg te houden om onopgemerkt te blijven.
Synchronisatie lost twee kernvragen op: wiens woord geldt als "waar", en hoe vullen we de tussenliggende frames in? Het eerste is een kwestie van autoriteit, het tweede van interpolatie.
De autoritatieve server: één bron van waarheid
De basis van moderne multiplayerarchitectuur is het model van de autoritatieve server. Hier is de enige bron van waarheid de server. Wanneer een client een toets indrukt, zegt hij niet "ik bevind me nu op positie X"; hij stuurt een input zoals "ik wil vooruit bewegen". De server verwerkt die input in zijn eigen simulatie, berekent het resultaat en zendt de bijgewerkte state terug naar alle clients.
Het grootste voordeel van deze aanpak is cheatbestendigheid. De client kan niet rechtstreeks een state schrijven zoals "de levenspunten van de vijand zijn 0" of "ik zit in de muur"; hij geeft alleen een intentie aan, en de server beslist. Hetzelfde principe geldt voor MMORPG-servers zoals Metin2: schade, drops en positievalidatie gebeuren aan de serverkant, anders kan een client die zijn eigen geheugen aanpast alles breken.
- Client → server: stuurt alleen input/commando's (bewegingsrichting, aanval, voorwerp gebruiken).
- Server: draait de simulatie, past botsingen en regels toe.
- Server → clients: zendt de nieuwe state-snapshots uit.
Tick-gebaseerde simulatie
De server laat de wereld niet continu vooruitgaan, maar met vaste tussenpozen. Elke stap heet een tick. Een server van 20 ticks/s werkt de wereld 20 keer per seconde bij (elke 50 ms). De reden voor een vaste tick is determinisme: wanneer dezelfde inputs in dezelfde volgorde met dezelfde tijdstap worden verwerkt, komt het resultaat op elke machine identiek uit.
// Serverlus met vaste tijdstap (vereenvoudigd)
const double TICK_RATE = 20.0; // ticks per seconde
const double DT = 1.0 / TICK_RATE; // 0,05 s
double accumulator = 0.0;
double previous = now_seconds();
while (running) {
double current = now_seconds();
accumulator += current - previous;
previous = current;
while (accumulator >= DT) {
process_inputs(); // client-inputs toepassen
step_world(DT); // fysica + regels
accumulator -= DT;
}
broadcast_snapshot(); // state naar clients sturen
}
De tickfrequentie is een evenwichtsoefening. Een hoge tick (bijv. 60) geeft vloeiendere, preciezere gameplay maar verhoogt de kosten aan bandbreedte en CPU. Snelle shooters vereisen een hoge tick; voor een MMO zijn 10–20 ticks meestal ruim voldoende.
Snapshots, delta's en bandbreedte
De hele wereld bij elke tick naar elke client sturen is verspilling. Er zijn twee gangbare optimalisaties:
- Deltacompressie: stuur alleen de velden die zijn veranderd ten opzichte van de vorige snapshot. Voor een stilstaande NPC stroomt er geen data.
- Interessegebied (area of interest): stuur een speler alleen de entiteiten binnen zijn zicht-/invloedsbereik. Hij hoeft niets te weten van een gevecht aan de andere kant van de map. Op MMO-schaal is dit onmisbaar.
Je moet ook alleen de velden serialiseren die relevant zijn voor het netwerk (positie, rotatie, animatiestatus, levenspunten) in plaats van allemaal. Positie kwantiseren in een begrensd bereik (bijv. 16 bits) in plaats van die op volle precisie te sturen, verkleint de pakketten aanzienlijk.
Latentie verbergen: interpolatie en voorspelling
Zelfs als snapshots maar 20 keer per seconde binnenkomen, wil de speler vloeiende beweging zien op een 144 Hz-scherm. Er zijn twee technieken om de gaten op te vullen.
Interpolatie (voor externe spelers): de client bemonstert de positie soepel tussen de laatste twee ontvangen snapshots. Daarvoor rendert hij bewust een kleine buffer (bijv. 100 ms) achter, zodat hij altijd twee echte datapunten heeft om tussen over te gaan.
// Lineaire interpolatie tussen twee snapshots
Vec2 lerp(const Vec2& a, const Vec2& b, float t) {
return { a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t };
}
// t: verhouding van het rendermoment tussen de twee snapshottijden (0..1)
Voorspelling aan de clientkant (voor je eigen speler): je eigen personage op het serverantwoord laten wachten geeft vervelende input lag. In plaats daarvan past de client de input lokaal toe op hetzelfde moment dat hij die naar de server stuurt. Wanneer de officiële state van de server binnenkomt, is er geen verschil als de voorspelling klopte; klopte ze niet, dan corrigeert reconciliatie dit: de client spoelt terug naar de door de server bevestigde state en speelt de nog niet bevestigde inputs opnieuw af. Bij een verschil zie je een kleine "teleport"; om dat te verzachten wordt de correctie over een paar frames vermengd.
Bronnen van inconsistentie en praktische tips
- Pakketverlies: over UDP kunnen snapshots verloren gaan. Gebruik een betrouwbaar kanaal/bevestigingsmechanisme voor kritieke gebeurtenissen (dood, voorwerp oppakken); probeer continue data zoals positie niet betrouwbaar te versturen, want de volgende snapshot brengt al verse data.
- Klokverschuiving: client- en serverklokken lopen uiteen. Voorzie snapshots van een server-ticknummer/timestamp en interpoleer tegen die tijd in plaats van de wandklok.
- Float-niet-determinisme: floating-pointresultaten kunnen licht variëren tussen compilers/platforms. Als je volledig determinisme nodig hebt, overweeg dan fixed-point rekenkunde; de meeste autoritatieve servers hebben dat niet nodig omdat de bron van waarheid enkelvoudig is.
- Testen: injecteer kunstmatige latentie en pakketverlies tijdens de ontwikkeling (met
tc netemop Linux). Een systeem dat goed aanvoelt bij 200 ms latentie is vlekkeloos bij 20 ms.
Veelgestelde vragen
Moet ik TCP of UDP gebruiken?
Voor snelle, positie-intensieve games geniet UDP doorgaans de voorkeur, omdat de gegarandeerde geordende levering van TCP nieuwe pakketten laat wachten terwijl een oud verloren pakket opnieuw wordt verzonden (head-of-line blocking), wat de latentie opblaast. Voor turn-based of latentietolerante games is TCP ruim voldoende. Veel engines bouwen hun eigen betrouwbaarheidslaag bovenop UDP.
Hoe hoog moet ik de tickfrequentie instellen?
Dat hangt af van de gameplay. In shooters waar reactietijd cruciaal is, zijn 30–64 ticks gangbaar; in MMO's en strategiespellen volstaan 10–20 ticks en zijn ze beter schaalbaar. Beslis door eerst het spelgevoel te meten en daarna de serverkosten.
Vormt voorspelling aan de clientkant een cheatrisico?
Nee, want voorspelling is slechts een visuele schatting; de uiteindelijke beslissing wordt altijd op de autoritatieve server genomen. Als de lokale voorspelling van de client in strijd is met de server, geldt de state van de server en wordt de client gecorrigeerd.
Bouw je een multiplayerserver? Laten we samen de autoritatieve architectuur, het tickontwerp en de strategieën om latentie te verbergen plannen, afgestemd op het genre van je project. Neem contact met me op en laten we bespreken wat je nodig hebt.