Cześć.
Chciałbym trochę porozmawiać o pętlach stałokrokowych. Wiem, że taki temat już kiedyś był http://forum.pasja-informatyki.pl/144565/petla-stalokrokowa-tworzenie-gier ale tam autor pytania w ogóle nie rozumiał pętli stałokrokowej, a mi chodzi bardziej o różne typy pętli. Może od początku:
while(game)
{
update();
display();
}
Jak wszyscy wiedzą, taka pętla jest niepoprawna, ponieważ na szybkim komputerze postać będzie się poruszać szybko (albo po prostu pojawi się na drugim końcu mapy,), a na wolnym komputerze postać będzie się strasznie wlokła. W sumie do niedawna myślałem, że pętla, która teraz pokażę, w zupełności wystarczy (na przykładzie SFML-a, ale wszystkie nazwy są intuicyjne):
Clock clock;
clock.restart();
Time elapsedTime=Time::Zero;
Time updateRate=seconds(1.f/60.f); //dla 60 FPS-ów
while(game)
{
elapsedTime=clock.restart();
if(elapsedTime>updateRate)
{
elapsedTime-=updateRate;
update();
}
display();
}
Otóż taka pętla (według mnie) powinna już działać normalnie na każdym komputerze. No ale jednak tak nie jest. Gdy przesłałem koledze próbkę gry Pong, to na jego komputerze piłka latała bardzo szybko, to samo z paletką, którą się ją odbijało. Kiedy indziej sobie sprawdziłem na innym komputerze, ta sama sytuacja, tylko że tym razem na odwrót: wszystko było jakby w zwolnionym tempie. No i w sumie się zgadzało, bo komputer kolegi jest bardzo dobry, a drugi komputer jest tani, czyli słabszy. No ale czemu taka pętla i tak nie działa?
No więc zainteresowałem się sposobami na pętlę stałokrokową. Mam dwa źródła:
Kiedyś czytałem drugi link, a pierwszy znalazłem niedawno. W pierwszym są cztery typy pętli, z czego najlepszy ponoć jest ostatni, czyli "Stała prędkość gry niezależna od zmiennej wartości FPS":
const int TICKS_PER_SECOND=25;
const int SKIP_TICKS=1000/TICKS_PER_SECOND;
const int MAX_FRAMESKIP=5;
DWORD next_game_tick=GetTickCount();
int loops;
float interpolation;
bool game_is_running=true;
while(game_is_running)
{
loops=0;
while( GetTickCount()>next_game_tick&&loops<MAX_FRAMESKIP)
{
update_game();
next_game_tick+=SKIP_TICKS;
loops++;
}
interpolation=float( GetTickCount()+SKIP_TICKS-next_game_tick)/float(SKIP_TICKS);
display_game(interpolation);
}
Najbardziej mnie zaciekawiła ta "interpolacja", ponieważ gdy sprawdzałem mojego Ponga na dwóch różnych komputerach (tryb online multiplayer), to ruch drugiego gracza był przerywany, jakby ścinało. Przez to, że pobieranie danych trwa jednak wolniej niż aktualizowanie gry (więc w skrócie, interpolacja jest to przewidzenie gdzie powinien być dany obiekt w danej klatce, dzięki czemu gra jest płynniejsza, wydaje się, że ma się więcej FPS-ów). Tylko że nadal niezbyt rozumiem ogólną pętlę. Między innymi:
- czemu dzieli się akurat 1000 przez żądaną liczbę FPS-ów?
- na co jest zmienna loops?
- dlaczego MAX_FRAMESKIP wynosi akurat 5?
- no i ogólne działanie tej pętli.
Jeśli nie uważacie tej pętli za najlepszą (mi się podoba ten pomysł z interpolacją, szczególnie w grach online), to czy ta z drugiego źródła będzie lepsza?
float dt=0.0f; //czas od ostatniej aktualizacji
float lastUpdateTime=GetCurrentTime(); //czas ostatniej aktualizacji
//przykladowa funkcja GetCurrentTime() pobiera
//nam od systemu aktualny czas w sekundach
float accumulator=0.0f;
const float TIME_STEP=0.03; //krok czasowy, a zarazem czas trwania ramki
//fizyki w sekundach; tutaj 30 milisekund, czyli
//ok. 30 aktualizacji na sekundę
const float MAX_ACCUMULATED_TIME=1.0; //maksymalny czas zgromadzony w pojedynczym
//obiegu petli glownej
while(true)
{
dt=GetCurrentTime()-lastUpdateTime; //obliczenie czasu od ostatniej klatki
lastUpdate+=dt; //podmiana
dt=std::max(0,dt); //upewniamy sie, ze dt >= 0
accumulator+=dt;
accumulator=clamp(accumulator,0,MAX_ACCUMULATED_TIME); //zapobiegamy
//zbyt duzej ilosci aktualizacji w danym obiegu
//petli glownej
GrabInput(); //<-- zbieranie wejscia z klawiatury, myszki, sieci, itp.
while(accumulator>TIME_STEP)
{
UpdateGame(TIME_STEP); //<-- aktualizacja fizyki i logiki gry
accumulator-=TIME_STEP;
}
RenderGame(); //<-- wyswietlenie aktualnego stanu na ekranie
}
Ta pętla jest bardzo podobna do tej, która niby miała być normalna i powinna działać (czyli ta pętla stałokrokowa, którą pokazałem jako pierwszą). Więc w czym jest ona lepsza? A może to będzie to samo? Nie rozumiem, dlaczego na jednym komputerze taka pętla jest szybsza, a na innym wolniejsza (niby jest to wytłumaczone w pierwszym źródle, "Prędkość gry bazująca na zmiennej wartości wyświetlanych klatek - FPS").
Więc prosiłbym o ogólne wytłumaczenie dlaczego taka pętla jednak nie działa i jeśli lepiej używać innej pętli, to poproszę o wytłumaczenie jej działania. Dziękuję bardzo.