Thumbnail Physics Engine Main Loop

Door: Thijs Zumbrink
15-12-2011 14:52

Ik was laatst weer wat aan het sleutelen aan TaZCrash, en realiseerde me dat mijn manier van timing in de physics engine niet helemaal netjes was. Ik heb het omgeschreven naar een nettere implementatie, maar dat heeft helaas ook een drawback.

Allereerst een korte introductie. De physics engine draait als een infinite loop, die aan de hand van een vooraf bepaalde framerate de physics van het spel berekent. De centrale functie hierin is physicsStep(double dt) die de nieuwe positionering van alle objecten in de wereld berekent, aan de hand van de parameter dt, de tijdsincrement waarmee gerekend wordt. Een hogere framerate betekent kleinere dt en daarom preciezere berekening.

Timing met sleep()

Om de beweging van objecten in de game wereld stabiel te houden is wat timing nodig. Mijn eerste aanpak is om de physics uit te voeren, te meten hoe lang dat geduurd heeft, en de rest van het frame te sleep()en. Deze aanpak heeft als voordeel dat de processor even rust krijgt voor andere zaken.

double timePerFrame = 1.0 / 30; // 30 frames per second

double prev = getTime();
while (true) {
physicsStep(timePerFrame);

double time = getTime();
double timeSpent = time - prev;
prev = time;

if (timeSpent > timePerFrame) {
std::cout << "[Game] Can't keep up!" << std::endl;
// Don't sleep, immediately continue
// The player will experience some "lag"
} else {
// Sleep for the rest of the frame
sleep(timePerFrame - timeSpent);
}
}


Echter, ik las dat sleep() geen garantie geeft over de maximum slaaptijd. Dus we kunnen niet zeker zijn dat we optijd terugkeren voor een nieuwe physics loop! In de praktijk is dat niet zo heel erg, maar ik vind het wel fijn om altijd een exacte staat te hebben.

Timing met een accumulator

Als we zekerder willen zijn van een exacte staat van de game wereld na physics berekeningen, kan een busy-wait in combinatie met een accumulator gebruikt worden. Hierin wordt niet meer gesleep()'d, maar we tellen gewoon de hoeveelheid vergane tijd totdat we genoeg tijd gewacht hebben om weer een physics step te doen.

Overigens: het is ook mogelijk om de accumulator niet te gebruiken en gewoon timeSpent mee te geven als time increment dt: physicsStep(timeSpent). Of physicsStep(accumulator). Dan heb je altijd directe berekening van je physics, ongeacht de timing. Echter, het heeft zo zijn voordelen om de tijd increment dt constant te houden. Allereerst brengt dit voordelen met zich mee in bijvoorbeeld multiplayer omgevingen of voor het opslaan van replays. Ook is het voordelig om de accumulator stapje voor stapje leeg te maken, dat voorkomt dat je een hele ruwe, grove physics berekening krijgt als er even lag voorkomt.

double timePerFrame = 1.0 / 30; // 30 frames per second
double accumulator = 0.0;

double prev = getTime();
while (true) {
double time = getTime();
double timeSpent = time - prev;
prev = time;
accumulator += timeSpent;

if (accumulator > 2*timePerFrame) {
std::cout << "[Game] Can't keep up!" << std::endl;
}

while (accumulator >= timePerFrame) {
physicsStep(timePerFrame);
accumulator -= timePerFrame;
}
}


Het nadeel van deze aanpak is dat de CPU (core) 100% belast wordt door de physics thread. Ookal is dat maar voor minder dan 5% nodig. Voorlopig houd ik gewoon beide implementaties en kan ik wisselen via een preprocessor constant. Ik bedenk nog of dit CPU probleem misschien op te lossen is en of er misschien nog betere oplossingen te vinden zijn.

Reacties
Log in of registreer om reacties te plaatsen.