Skip to content

Instantly share code, notes, and snippets.

@tripzilch
Last active April 18, 2017 22:08
Show Gist options
  • Save tripzilch/10fc1af97830e515bcbb to your computer and use it in GitHub Desktop.
Save tripzilch/10fc1af97830e515bcbb to your computer and use it in GitHub Desktop.
Timing en de gameloop

Timing en de game-loop

lol, sam snapt mijn gameloop niet ofzo :P "die while loop is niet goed ofzo" stel je pc loopt vast dan zou ie oneindig veel updates kunnen krijgen

http://gameprogrammingpatterns.com/game-loop.html hmm ik zie toch echt dat ze t hier ook doen XD

Ah nice dus ik had het goed onthouden? :-) Volgens mij was dat ook precies het artikel waar ik dit uit geleerd heb.

Sam stelt wel een goede vraag hoor, en in theorie heeft ie deels gelijk. Bovendien zijn er ook nog andere situaties waarin het mis kan gaan. Dat betekent dat je bepaalde afwegingen moet maken:

  1. Hoe erg het is als dat gebeurt.
  2. Hoe onwaarschijnlijk het is dat dat kan gebeuren (en hoe zorg je daarvoor).
  3. Hoe belangrijk het is (gegeven 1 en 2) om deze situaties / excepties af te vangen, en hoe.

Anyway, we hebben het dus over een gameloop die er ongeveer zo uitziet: (in python/pseudocode)

while game_is_running:
	process_inputs()

    now = getTime()
    dt = min(now - game_now, MAX_DT)
    reps = 0
    while game_now < now:
        update(dt)
        game_now += dt
        reps++

    render()

(Deze is subtiel anders dan wat we zaterdag hebben gemaakt, volgens mij heb je de variabelen last_frame_time en game_dt eigenlijk niet nodig, dus heb ik het versimpeld)

1. Hoe erg het is

Kijk, als je pc vastloopt dan heb je toch al een probleem. Voor sommige soorten applicaties (bijvoorbeeld databases, filesystems, of hard real-time applicaties) is het belangrijk dat een programma zich dan nog steeds netjes en voorspelbaar gedraagt, of voorspelbaar afsluit, oid. Voor andere applicaties--zoals videogames--maakt dat niet uit en mogen ze gewoon crashen samen met de rest van het systeem dat vastloopt.

Voor nog meer achtergrondinformatie, Criteria for real-time computing op Wikipedia. Merk overigens op dat volgens de definitie van hard real-time een vastlopende computer hoe dan ook een total system failure is. Voor hard real-time boeit het geen drol meer wat er daarna gebeurt (omdat de kerncentrale, of de batterij van je Tesla toch al is ontploft, of de gebruiker van je software is overleden--denk pacemakers, automatische piloot, bemande diepzeevaartuigen, etc etc). Real-time systems engineering is een vak apart en een apart vak :) Heel pragmatisch. Videogames vallen onder de categorie soft real-time.

(NB: De middelste categorie "firm real-time" is volgens mij onzin, en valt gewoon onder "soft real-time". Waarschijnlijk verzonnen door iemand die de term "soft real-time" marketing-technisch niet mooi genoeg vond.)

2. Hoe (on)waarschijnlijk het is dat het misgaat

Ik zei zaterdag al, er zijn wel een paar eisen waar je gameloop aan moet voldoen zodat het niet mis loopt:

  • Een enkele call naar update(dt) moet "snel genoeg" zijn in verhouding tot de duur van een volledige process_inputs; while .. do update; render gameloop-iteratie. Als dat niet zo is dan werkt het averechts om update(dt) vaker aan te roepen om tijd in te halen. Hoe snel "snel genoeg" precies is, kan je eventueel uitrekenen.

  • Een enkele call naar update(dt) mag (gemiddeld) ook niet langer duren dan MAX_DT (wederom een reden dat het fijn is om de tijd in duidelijke units te hebben). Immers als het 0.05s kost om je game-state met 0.03s te updaten, dan raak je onherroepelijk achter, en is het onmogelijk om die tijd weer in te halen.

3. Wat je er tegen kan doen, en of dat nut heeft

Als je vrij zeker bent dat een enkele call naar update(dt) nooit meer dan een kleine fractie van MAX_DT aan tijd kost, en ook niet meer dan een bepaald percentage P van de duur van een volledige gameloop beslaat, dan zit je voor videogames in principe goed. Dit betekent dat de meeste tijd van je loop in de call naar render() wordt besteed. Je game zal dan alleen maar crashen als het systeem van de hele computer toch al instabiel aan het worden is (more or less).

Het percentage P kan je volgens mij in principe redelijk eenvoudig uitrekenen. Ik kan alleen nu ff niet bedenken hoe precies. Gelukkig denk ik dat het ook niet echt nodig is. Merk eerst op dat now = getTime() buiten de update(dt) while-loop staat. Dit betekent dat, zolang dt > 0, deze while-loop nooit oneindig lang kan herhalen.

Wat wel kan gebeuren is, als niet aan de genoemde voorwaarden wordt voldaan, het aantal repetities van deze binnenste while-loop steeds groter en groter wordt. Maar nooit oneindig. Desondanks, je game loopt dan wel vast.

Als je dit waarschijnlijk acht, en je wilt dit afvangen dan is daar een heel simpele manier voor: Stop of pauzeer de game zodra het aantal reps ridicuul hoog wordt. Hoe hoog is "ridicuul hoog"? Nouja, gegeven dat als het mis gaat, het aantal reps explosief omhoog gaat, kan je hier een echt hoog getal gebruiken.

In het artikel over Criteria for real-time computing staat iets geschreven over quality of service. Wat je eigenlijk moet doen, is bedenken wat precies nog een acceptabele quality of service is voor jouw game.

Stel je zegt, als de game op 10 frames per second draait, is het eigenlijk niet echt leuk meer om te spelen. Een frame duurt dan dus 1 / 10 == 0.1 seconde. Dat betekent dat now - game_now ook ongeveer 0.1 seconde is. Aangenomen dat MAX_DT beduidend kleiner is dan 0.1 seconde (wij hadden geloof ik 0.02 gekozen, toch?), dan wordt min(now - game_now, MAX_DT) dus gelijk aan MAX_DT. Je kan nu uitrekenen dat reps uitkomt op een waarde van 0.1 / MAX_DT, wat neerkomt op 0.1 / 0.02 == 5 iteraties van de while-loop.

Wanneer reps > 5 betekent het dus Danger Zone. Maar het is misschien een beetje te trigger-happy om gelijk te quitten als je even onder de 10fps dipt. Maar, wanneer het echt mis gaat omdat er simpelweg te weinig rekenkracht is, dan gaat het ieder frame ook steeds erger mis. Je kan dus bijvoorbeeld ook zeggen: bij vier keer zoveel, als reps > 20, dan is de shit wel serieus tegen de fan aan het vliegen. Een frame duurt dan ongeveer MAX_DT * 20 == 0.4 seconden (2.5 fps). Stel je wilt (WTSHTF) de speler niet blootstellen aan een game-freeze van langer dan 2 seconden, dan kan je bijvoorbeeld quitten als dat vijf keer achter elkaar gebeurt. Je kan nog verder kijken naar quality of service en bedenken dat choppy gameplay is ook niet cool is, dus bijvoorbeeld als dit vijf keer gebeurt binnen een periode van 10 seconden, is het ook end of story. Ik zou de logica hiervoor in een aparte functie bool check_responsiveness(reps) zetten, die dat bijhoudt, dan blijft je main gameloop lekker eenvoudig.

Dit soort oplossingen zijn soort van equivalent aan wat je browser ook doet als ie meldt dat een script unresponsive is geworden, of al een tijdje teveel resources opeet. Het ding is, je ontkomt er gewoon niet aan tenzij je precies weet op wat voor hardware je draait, en je exclusieve single-threaded control hebt, etc. Aan de andere kant, voor jouw Pong game zou ik me er ook weer niet al te druk over maken (je Raspberry Pi trekt het vast ook prima).

Epiloog, nog een paar dingetjes

In de een-na-laatste paragraaf van deze sectie staat dat het soms ook goed is om te zorgen dat je timestep dt niet te klein wordt, omdat update(dt) dan (vanwege overhead) misschien langer duurt dan dt seconden. Sterker nog, in deze sectie wordt uitgegaan van een constante waarde dt == MS_PER_UPDATE. Kan je ook doen.

Verder, in deze sectie staat ook nog iets over het extrapoleren van je gamestate door een lag parameter aan je render() call mee te geven. Voor de pseudocode hierboven geldt dan dat je lag kan uitrekenen (na de inner while-loop) als lag = game_now - now seconden. Merk op dat lag altijd kleiner is dan de dt voor dit frame. In het artikel roepen ze dan render(lag / dt) aan, deze parameter is dus altijd een getal tussen 0 en 1, die aangeeft "waar" (wanneer) je precies tussen je twee frames in zit. Persoonlijk zou ik het extrapoleren makkelijker vinden om te berekenen met gewoon lag zelf, dus render(lag). Dan is de waarde namelijk weer in seconden. Voor een spel als Pong zou dit betekenen dat de speler af en toe het balletje "in" of "voorbij" een batje ziet, terwijl dit natuurlijk niet zo gebeurt. Dus voor Pong misschien niet zo'n goed idee, maar voor ingewikkeldere game-engines, is het vaak wel beter dan het alternatief.

Wat je ook nog kan doen, gezien wij een variabele timestep dt gebruiken (in tegenstelling tot het artikel), is het volgende:

while game_is_running:
	process_inputs()

    now = getTime()
    dt = min(now - game_now, MAX_DT)
    reps = 0
    while game_now < now:
        update(dt)
        game_now += dt
        reps++
        
    if not check_responsiveness(reps):
        die_horribly('AAAAAaaaa')

    lag = game_now - now
    if lag > MAX_LAG:
        update(lag)
        game_now += lag    # game_now == now

    render()

Op deze manier zorg je ervoor dat je gamestate, na de inner while-loop, mogelijk nog een klein stapje verder updatet, en game_now precies uitkomt op het juiste tijdstip now. Ik heb hier nog een if-statement omheen gezet, zodat je niet onnodig vaak update aanroept als de lag heel erg klein is (dus op een zeer snelle computer).

Een variabele dt is alleen niet altijd deterministisch is voor iedere physics-engine, vandaar dat het artikel een kleine constante dt = MS_PER_UPDATE gebruikt. Uitproberen, hangt van je engine af.

@rreuvekamp
Copy link

I lost The Game.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment