Bazując na informacjach, które czytałem odnośnie Direct'a 2D (tego API używam), V-SYNC działa tam po prostu tak, że po zakończeniu renderowania danej klatki, blokuje wątek CPU obsługujący render. Następnie, we właściwym monecie, zostaje on wybudzony przez odpowiedni Notify Event
wysłany przez GPU (co jest oczywiście powiązane z częstotliwością odświeżania monitora).
No to SDL robi dokładnie tak samo. Żeby mieć V-Sync i jednocześnie obsługiwać logikę z większą częstotliwością niż odświeżanie ekranu, logika musi być wykonywana w osobnym wątku, a renderowanie w głównym, czyli tym, który jest blokowany przez V-Sync.
Nie wykluczone, że w twojej implementacji to działa, ale jednak większość źródeł odradza takie podejście, nazywając je naiwnym i na dłuższą metę problematycznym.
Problematyczne to będzie, jak się ten mechanizm wykorzysta do celów, do których się nie nadaje. W przypadku gier, których logika i renderowanie nie są wymagające, sprawdza się wyśmienicie i działa bezbłędnie na każdej maszynie, wedle założeń.
Dlatego właśnie szukam "profesjonalnego" podejścia, tzn. takiego, którego używa się w "prawdziwym" gamedevie. Niestety, zaskakująco trudno dokopać się do jakichś konkreteów na ten temat.
Bo może szukasz czegoś, co po prostu nie istnieje? Nie potrzebujesz żadnych materiałów na ten temat, bo sam możesz taki mechanizm zaklepać. Wystarczy wziąć pod uwagę właściwości silnika i specyfikę wymagania jakie masz (i jakich my nie znamy, boś ich nie podał) i na tej podstawie dobrać odpowiednie rozwiązanie.
Powyższe nie rozwiązuje jednak problemu zarządzania mocą obliczeniową CPU, bo tam też wątek śmiga ile fabryka dała...
Oczywiście, że rozwiązuje, bo do tego celu został stworzony i moje gierki są tego dowodem. Fairtris działa w 60fps lub 50fps (w zależności od wybranego regionu w ustawieniach rozgrywki), zużywając raptem 3-4% mocy obliczeniowej jednego rdzenia CPU — reszta jest do dyspozycji innych procesów i wątków.
Jeśli logikę przetwarzasz w wątku „ile fabryka dała”, to się nie dziw, że zjadasz całą dostępną moc rdzenia. Chcesz oszczędzić moc obliczeniową? Wykorzystaj coś w rodzaju Sleep
i oddaj systemowi moc, której nie potrzebujesz.
Jeżeli twoim zdaniem oparcie odświeżania logiki gry na Sleep()
jest dobrym pomysłem, to zupełnie nie ogarniasz...
Mam wrażenie, że sam nie rozumiesz. Masz dosłownie trzy wyjścia:
- nie używasz
Sleep
— wątek zżera całą dostępną moc, framerate jest zmienny,
- używasz zmiennego
Sleep
— wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest stały,
- używasz stałego
Sleep
— wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest zmienny.
Innej opcji nie ma i nie będzie.
Ale ja nie chcę 200 klatek, tylko możliwości własnoręcznego ustawienia limitu odświeżania, z opcją precyzyjnego odmierzania interwałów między iteracjami […]
Ustalenie własnego limitu klatek na sekundę to nic innego jak odpowiednie obliczanie interwału dla Sleep
. Im dłuższy Sleep
, tym mniej klatek będzie renderowanych w czasie jednej sekundy. Dokładnie to dzieje się w Deep Platformerze — w trybie debug
mogę zmieniać limit od 1fps to 60fps, im mniej klatek tym więcej mocy jest oszczędzanej. Tyle że platformer działa zgodnie z NES-em, czyli nie używa delty.
[…] i bez niepotrzebnego busy waitingu, zjadającego moc obliczeniową CPU.
Nie da się zrobić precyzyjnego odmierzania czasu bez busy waitingu pod Windows, bo żaden systemowy mechanizm wstrzymywania pracy wątku nie zapewnia mikrosekundowej precyzji, która była by wystarczająca — koniec kropka. Bazowanie na samym Sleep
pozwoli osiągnąć w miarę stabilny klatkaż, ale tylko w miarę stabilny — rozrzut będzie pomiędzy 58fps a 62fps. Wiem, bo probowałem wielokrotnie na przestrzeni ostatnich lat i po prostu się nie da, bo Sleep
jest nieprecyzyjny. A innego, bardziej precyzyjnego jego odpowiednika nie ma w Windows.
Chcesz się przekonać? Pobierz Fairtrisa, zaremuj busy waiting, skompiluj i odpal. Dostaniesz to:
Ma być sztywne 60fps, ale niedokładność Sleep
(tu: SDL_Delay
) powoduje wahania. Przy czym Fairtris ma wybitnie prosta logikę, więc w każda klatka generowana jest w niemal identycznym czasie. W przypadku gdy logika będzie wykonywana od kilku do kilkunastu milisekund, w zależności od danej sceny, wahania mogą być i będą większe. Nieważne czy podzielisz proces na kilka wątków, czy będziesz używał jednego.
A jeśli chodzi o deltę, to nie jest ona w żaden sposób problematyczna. Jeśli gra działa stabilnie i ma zapas mocy obliczeniowej, wykonujesz jeden update logiki per klatka. Jeśli zdarzy się turbo-lag, logikę aktualizujesz w pętli, małymi skokami, tak aby wyeliminować błędy fizyki (np. przechodzenie przez ściany).
Upraszczając, chcesz mieć 60fps, czyli na jedną klatkę przypada delta w rozmiarze 1/60, czyli ~0.0166
. Tę deltę używasz jako mnożnik do obliczania ruchu obiektów. Zdarza się lag, dostajesz deltę o wartości 0.200
, czyli lag trwał 1/5 sekundy, czyli 200ms. Jeśli takiej delty użyjesz jako mnożnika, obiekt może przeniknąć przez ścianę. Temu trzeba zaradzić.
Delta jest zbyt duża, więc w pętli odejmujesz ~0.0166
od obecnej delty i wykonujesz logikę dotąd, aż obecna delta będzie mniejsza niż ~0.0166
. Kończysz pętlę, a pozostąłą deltę używasz jeszcze raz do jeszcze jednej aktualizacji logiki. W ten sposób lag nie zepsuje Ci silnika. Coś a la poniższe:
//metoda aktualizacji logiki
procedure UpdateLogic(ADelta: Single);
begin
// tu używasz delty jako mnożnika
// delta nigdy nie jest zbyt duża
end;
// metoda generowania klatki
procedure Update(ADelta: Single);
begin
// nie ma laga
if ADelta < 1 / 60 {+ malutki margines} then
UpdateLogic(ADelta)
else
// jest lag
begin
repeat
UpdateLogic(1 / 60);
ADelta -= 1 / 60;
until ADelta < 1 / 60;
UpdateLogic(ADelta);
end;
end;
To jest bardzo mocno uproszczona wersja, tylko do zobrazowania sytuacji. Wyżej użyłem wartości 60
, co da 60fps. Jeśli zamiast literału użyjesz zmiennej przechowującej limit klatkażu, będziesz mógł tym limitem manipulować, a kod się do niego zastosuje.