@Crow: jeśli Ci śmiga to co masz to raczej problem został rozwiązany. Nie będę też komentował tego co próbowałeś i co odrzuciłeś, bo nie widziałem wszystkich testowanych przez Ciebie kodów. Przejdę od razu do rzeczy.
Zainteresowała mnie też w twoim kodzie jedna rzecz, mianowicie oczekiwanie na klatkę. Jak coś takiego zrealizować?
Wszystko znajdziesz w module, którego kod Ci podałem – metoda WaitForNMI
służy właśnie do oczekiwania na kolejną klatkę, tak aby gra wykonywała stałe 60fps, generując kolejne klatki równo co ~16,7ms, wspierając dynamicznie zmieniające się obciążenie.
Załóżmy, że narysowałem sobie klatkę, zajęło mi to 8 milisekund (czyli do kolejnej musi upłynąć jeszcze drugie tyle).
Żeby wiedzieć ile czasu trwało renderowanie klatki, należy pobrać czas dwa razy – przed renderowaniem i po nim – a następnie obliczyć różnicę. Nie możesz tutaj bazować na czasie systemowym, bo częstotliwość jego odświeżania wynosi na oko 64Hz, czyli skokami co mniej więcej 16ms. Dlatego potrzebne są funkcje QPF
i QPC
, aby móc obliczyć dokładny czas w jakim przetwarzano klatkę.
To oznacza, że nie mogę tak po prostu przeskoczyć do rysowania kolejnej klatki, tylko muszę odczekać […]
Tak, i do tego celu możesz użyć Sleep
. Ale żeby go użyć, musisz wiedzieć ile zajęło generowanie klatki i ile milisekund pozostało do odczekania. To są proste obliczenia – podałem je niżej.
Pytanie jak długo mam czekać? Nie wiem przecież, ile czasu zajmie rysowanie kolejnej klatki, może znowu 8ms, a może tym razem np. 17?
To trzeba obliczyć dynamicznie. Dzięki temu bez względu na to jak długo trwa generowanie klatki, za każdym razem dostaniesz odpowiednią liczbę milisekund do odczekania. Im dłużej trwało generowanie klatki, tym mniej czasu trzeba odczekać.
W tym drugim wypadku (żeby nie było opóźnień), taką klatkę w ogóle powinno się pominąć (frame skipping) […]
Wszystko zależy od efektu jaki Cię interesuje. Mój platformer jest prywitywny z założenia i wszelkie lagi mają być widoczne, jeśli takowe wystąpią. Każda klatka jest renderowana w oknie – nieważne jak długo była generowana. Natomiast jeśli generowanie trwało za długo (dłużej niż ~16.7ms) to kolejną klatkę generuje się od razu – bez oczekiwania.
To dosyć złożone i nie do końca wiem, jak się za to zabrać.
To dość proste – trzeba tylko poczekać aż trybiki zaskoczą o co w tym wszystkim chodzi. :]
Wyjaśnię Ci dokładnie jak to wygląda w moim platformerze – innej gierki nie robiłem aby się wzorować.
Zacznijmy od tego, że generowanie klatek zajmuje różną ilość czasu – raz jest to np. 4ms (podczas animacji), raz 12ms (podczas właściwej rozgrywki na poziomie). Czas ten jest zależny od skomplikowania renderowania oraz od mocy obliczeniowej procesora (akceleracja sprzętowa nie jest wspierana, więc wszystko przetwarza centralna jednostka).
Aby wiedzieć ile czasu trwało generowanie klatki, najpierw należy wiedzieć ile ”ticków” potrafi procesor wykonać w czasie jednej sekundy. Ta wartość jest stała i pobiera się ją za pomocą funkcji QPF
. Teraz aby wiedzieć ile ticków zajęło generowanie klatki, należy pobrać stan licznika przed generowaniem i po nim – to robi się za pomocą QPC
. Mając dwa stany licznika, należy obliczyć różnicę tych wartości – wynikiem jest liczba ticków wykorzystanych na generowanie klatki.
W tym momencie mamy dwie liczby – liczbę ticków wykonywanych w czasie jednej sekundy oraz liczbę ticków wykorzystanych na generowanie bieżącej klatki. Aby wiedzieć ile ticków przeznaczonych jest na wygenerowanie jednej klatki, należy wartość pobraną za pomocą QPF
podzielić przez framerate.
Wiemy już ile ticków przeznaczonych jest dla jednej klatki, wiemy też ile ticków zajęło generowanie – teraz pozostało obliczyć jaki procent ticków został wykorzystany (procent obciążenia), a ile nie (czas do odczekania). To zwykłe mnożenie na krzyż z jedną niewiadomą – proste do wykonania.
Skoro już wiemy jaki procent ticków pozostał do odczekania, należy go przeliczyć na milisekundy. Skąd je wziąć? To proste – ticki przeliczyliśmy na procentaż i możemy go wykorzystać do obliczeń. Wiemy też ile milisekund przeznaczonych jest dla jednej klatki – to liczba milisekund w jednej sekundzie (czyli 1000
) podzielona przez framerate (czyli przez 60
). Zmiennoprzecinkowy wynik takiego dzielenia to 16.666(6)
i jego mnożymy przez procentaż. Wynikiem jest liczba milisekund do odczekania – w postaci floata, dlatego trzeba go zaokrąglić.
No, to by było na tyle. Teraz czas na pseudokod.
Najpierw pobierzmy liczbę ticków wykonywanych w czasie jednej sekundy i policzmy ile ich przypada na generowanie jednej klatki:
Kopiuj
var
TicksPerFrame: Int64;
QueryPerformanceFrequency(TicksPerFrame);
TicksPerFrame := TicksPerFrame div 60;
W głównej pętli gry trzeba pobrać stan licznika przed i po generowaniu:
Kopiuj
var
TicksBegin, TicksEnd, TicksUsed: Int64;
repeat
QueryPerformanceCounter(TicksBegin);
QueryPerformanceCounter(TicksEnd);
TicksUsed := TicksEnd - TicksBegin;
until Terminated;
Zmienna TicksUsed
zawiera liczbę wykorzystanych ticków na generowanie klatki. Teraz musimy policzyć jaki procent ticków pozostał niewykorzystany:
Kopiuj
var
PercentUnused: Single;
PercentUnused := 1 - TicksUsed / TicksPerFrame;
W zmiennej PercentUnused
mamy procentaż w postaci liczby zmiennoprzecinkowej. Teraz ten procentaż należy wykorzystać do obliczenia milisekund, które gra musi odczekać do następnej klatki. Tę liczbę milisekund należy wykorzystać w procedurze Sleep
:
Kopiuj
var
WaitTime: Integer;
WaitTime := Max(Trunc(1000 / 60 * PercentUnused), 0);
Sleep(WaitTime);
Ten sposób pozwoli utrzymać średnio 60fps bez względu na obciążenie. A jeśli to osiągnie 100% lub więcej to odczuwalne będą lagi. Lagi te nie wpłyną w żaden sposób na obliczenia, bo jedyne co spowodują to opóźnienie generowania kolejnej klatki.
Ten sposób wykorzystuję w swojej grze (jeszcze ”w trunku”) i z testów wynika, że działa dość stabilnie. Jeśli nie chcesz używać procedury Sleep
, która ma niestety ograniczenia związane z niedokładnością pracy, to zawsze możesz bazować wyłącznie na licznikach ticków i zżerać moc CPU. To da Ci super stabilny klatkaż, jednak kosztem dużego zużycia mocy procesora.
Jeśli chcesz to mogę wrzucić przykład implementacji klasy takiego zegara – będzie to nieco zmodyfikowana wersja klasy zegara z mojego platformera.