Pętla czasu rzeczywistego [PL]

Gry działają w specyficzny sposób, który odróżnia je od większości aplikacji innego rodzaju. Polega na on zawłaszczaniu tak dużej części zasobów komputera, jak to tylko jest możliwe. Dotyczy w szczególności czasu, w trakcie którego gra wykonuje obliczenia aktualizujące jej stan oraz wyświetla grafikę. O ile programy użytkowe reagują głównie na akcje wykonywane przez użytkownika (jest to tzw. model zdarzeniowy), o tyle gry działają w sposób ciągły i “aktywny”, wykorzystując każdą wolną chwilę na generowanie kolejnych klatek.
Taki samolubny sposób działania jest jednak jak najbardziej poprawny. Przecież uruchomiona gra przykuwa całą uwagę użytkownika, który w zasadzie nie zajmuje się w jej trakcie innymi czynnościami. Analogicznie sama gra może (i powinna) wyciągać od systemu operacyjnego tyle zasobów, ile tylko zdoła. Najczęściej tylko w ten sposób możliwe jest wyświetlanie skomplikowanej grafiki trójwymiarowej i animacji czy liczenie złożonych symulacji fizycznych. Nie bez znaczenia jest też inny oczywisty fakt: gry zazwyczaj działają w trybie pełnoekranowym, przesłaniając wszystkie inne aplikacje.

Ten specyficzny tryb działania wymaga innego podejścia niż w przypadku programów użytkowych. Gry są bowiem aplikacjami czasu rzeczywistego i działają nie od zdarzenia do zdarzenia, ale przez cały czas – w pętli. W tym artykule zamierzam pokazać praktyczne konsekwencje tego faktu i przedstawić prawidłowe sposoby sterowania przebiegiem gry. Zajmiemy się zatem:

  • postacią pętli czasu rzeczywistego
  • uaktualnianiem stanu gry w zależności od upływającego czasu
  • mierzeniem efektywności działania gry (czyli między innymi liczeniem ilości wyświetlanych klatek na sekundę)

U czytelnika zakładam pewną znajomość języka C++. Przykłady kodów są aczkolwiek krótkie i nie ma w nich żadnych zaawansowanych konstrukcji językowych. Wykorzystywaną platformą jest w większości czyste API Windows; niepotrzebne są nam szczegóły bibliotek graficznych jak DirectX czy OpenGL.
Artykuł jest generalnie przeznaczony dla początkujących programistów gier i osób aspirujących do tego miana.

Ogólna postać pętli czasu rzeczywistego

Programy użytkowe czekają na system operacyjny, aby powiadomił je o zachodzących zdarzeniach, z czego najbardziej interesujące są te pochodzące od urządzeń wejścia – klawiatury i myszki. Gry na to nie czekają, gdyż powinny renderować kolejne klatki tak szybko, jak to tylko możliwe. A ponieważ wymaga to ciągłego “pętlenia się”, mogą również pobierać informacje o wejściu w bardziej bezpośredni sposób.
I tak właśnie się to dzieje. Typowo jeden obieg pętli czasu rzeczywistego polega na wykonaniu trzech czynności:

  1. Sprawdzeniu, czy zaszły jakieś zdarzenia od (zmienił się stan) urządzeń wejściowych
  2. Wykonania obliczeń dla nowego stanu gry w oparciu o działanie użytkownika oraz czas, jaki upłynął od poprzedniej klatki
  3. Rysowania, co oznacza np. renderowanie sceny 3D lub wyświetlanie dwuwymiarowych sprite‘ów

Spróbujemy przyjrzeć się każdemu z tych etapów.

Reakcja na zdarzenia

Jeśli programujemy w czystym Windows API, to doskonale wiemy, że za fasadą programowania sterowanego zdarzeniami kryje się tak naprawdę pętla – zwana tutaj pętlą komunikatów (message loop). W najprostszej i najczęściej wykorzystywanej postaci wygląda ona następująco:

  1. MSG msg;
  2. while (GetMessage(&msg, 0, 0, 0))
  3. {
  4.    TranslateMessage (&msg);
  5.    DispatchMessage (&msg);
  6. }

co w skrócie tłumaczy się jako: pobierz komunikat o zdarzeniu, przetłumacz go (szczegół techniczny potrzebny komunikatom od klawiatury) i przekaż do odpowiedniego okna – czyli obsłuż zdarzenie.
Wszystko wygląda OK, ale dla nas ta pętla ma poważny feler. Otóż funkcja GetMessage nie tylko pobiera komunikat zdarzenia. Jeżeli żaden nie jest dostępny, funkcja ta poczeka na niego – i może ona czekać bardzo, bardzo długo. W tym czasie gra nie będzie wykonywała żadnych obliczeń ani rysowania, zatem gracz może wręcz uznać, iż się zawiesiła! Pętla w powyższej postaci jest więc dla nas zupełnie nieprzydatna.

Potrzebujemy bowiem sposobu na określenie, czy w kolejce czeka na nas jakieś nieobsłużone zdarzenie. Jeżeli odpowiedź jest pozytywna, wówczas powinniśmy się nim zająć. W przeciwnym wypadku mamy wolną rękę i możemy zająć się obliczeniami i renderingiem (czyli ciekawszymi rzeczami :)) W pseudokodzie idea ta wygląda następująco:

  1. while (!NalezyKonczyc())
  2. {
  3.    if (ZaszloZdarzenie())      ObsluzZdarzenie();
  4.    else
  5.    {
  6.       UaktualnijStanGry();
  7.       Rysuj();
  8.    }
  9. }

W wersji dla Windows API odpowiednią funkcją sprawdzającą warunek z powyższej instrukcji if jest PeekMessage. Jeżeli w kolejce komunikatów dla aplikacji (ściślej: wątku) mamy chociaż jedno zdarzenie, funkcja pobiera je; w przeciwnym wypadku informuje nas o zastanym braku. Ważne jest to, że funkcja nigdy na nic nie czeka i od razu zwraca sterowanie z powrotem do programu.
Ostatecznie prawidłowa postać szkieletu pętli komunikatów czasu rzeczywistego dla Windows wygląda następująco:

  1. MSG msg;
  2. msg.message = WM_NULL;  // "pusta" wiadomość
  3. while (msg.message != WM_QUIT)
  4. {
  5.    if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
  6.    {
  7.       TranslateMessage (&msg);
  8.       DispatchMessage (&msg);
  9.    }
  10.    else
  11.    {  /* Licz i rysuj */ }
  12. }

Analogicznie będzie zresztą wtedy, gdy korzystamy z innych środowisk czy bibliotek. W SDL na przykład użyjemy SDL_PollEvent do sprawdzania, czy jakieś zdarzenie zaszło. Najważniejsze jest, aby nasza pętla nie czekała bezczynnie na jego pojawienie się, lecz aktywnie to sprawdzała – a w międzyczasie wykonywała obliczenia związanie z rysowaniem kolejnej klatki.

Aktualizacja stanu gry

Rzeczone obliczenia polegają na uaktualnieniu stanu gry w oparciu o upływający czas. Może to obejmować bardzo wiele różnego rodzaju kalkulacji, począwszy od animacji, przez zmianę pozycji obiektów poruszających się ze stałą prędkością, wykrywanie i odpowiedzi na kolizje, aż po skomplikowane symulacje fizyczne z użyciem mechaniki ciała sztywnego lub giętkiego.
Wszystkie algorytmy mają – a przynajmniej powinny mieć – pewną cechę wspólną. Jest nią wspomniana zależność od czasu. Ażeby ją osiągnąć, musimy jednak być świadomi jego upływu.

Pomiar upływającego czasu

Zarówno biblioteka standardowa C++, jak i np. Windows API oferują funkcje służące do uzyskania aktualnego czasu. Nie chodzi nam jednak o datę i godzinę. Chociaż można by teoretycznie używać takich funkcji do pomiaru upływającego czasu, to praktyce ich dokładność jest zbyt mała – przykładowo, standardowa funkcja time z C++ zwraca czas zaledwie z sekundową precyzją.
Zamiast takich “zegarowych” funkcji powinniśmy użyć czegoś innego. Nie interesuje nas bowiem konkretna chwila w czasie, tylko różnica między dwiema takimi chwilami. Wówczas nie ma znaczenia, czy czas mierzymy zgodnie z powszechnie przyjętym kalendarzem, czy też zaczynając od 1 stycznia 1970 roku (data uniksowa), czy może od… uruchomienia komputera. Grunt, żeby uzyskiwać wartości z rozsądną dokładnością.

Minimalną precyzją, na jaką możemy sobie pozwolić, są milisekundy. W celu jej osiągnięcia możemy użyć funkcji Windows API o nazwie GetTickCount (albo analogicznej SDL_GetTicks z SDL). Zwraca ona liczbę milisekund, jakie upłynęły od rozruchu systemu. Ponieważ rezultat jest liczbą 32-bitową, teoretycznie po niecałych 50 dniach wystąpi “przekręcenie licznika”. Możemy chyba jednak swobodnie założyć, że przy sławetnej stabilności systemu Windows takie wyniki byłyby wyjątkowym ewenementem :)
Uzbrojeni w powyższa wiedzę możemy już dodać do naszej pętli pomiar czasu trwania jednego jej przebiegu:

  1. float dt;
  2. DWORD dwTicks = GetTickCount(), dwNewTicks;
  3. while (msg.message != WM_QUIT)
  4. {
  5.    if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
  6.    {  /* Przetworzenie zdarzenia */ }
  7.    else
  8.    {
  9.       // pobranie nowego czasu (i upewnienie się, że jest dodatni)
  10.       // i obliczenie różnicy w sekundach
  11.       dwNewTicks = GetTickCount();
  12.       dt = dwNewTicks > dwTicks ? (dwNewTicks - dwTicks) / 1000.0f : 0.0f;
  13.       dwTicks = dwNewTicks;
  14.  
  15.       /* Obliczenia (oparte o dt) i rysowanie */
  16.    }
  17. }

Jak widać nie ma tu żadnej magii: po prostu wywołujemy GetTickCount w każdym cyklu, zapisujemy uzyskaną wartość i odejmujemy od niej poprzednią. Po przeliczeniu na sekundy w zmiennej dt otrzymujemy czas trwania obliczeń i rysowania dla poprzedniej klatki.

Bądźmy dokładniejsi

Według MSDN GetTickCount oferuje nam dokładność milisekundową, chociaż w praktyce jest ona nawet mniejsza (i podobno wynosi ok. 18 milisekund). Tak czy inaczej, jest ona często zbyt mała. A jeśli nawet chwilowo nie jest, to warto wiedzieć, że możemy uzyskać precyzyjniejsze rezultaty :)

W Windows API posłużą nam do tego dwie funkcje: QueryPerformanceFrequency i QueryPerformanceCounter. Służą one do obsługi tzw. zegara wysokiej częstotliwości. Dawniej był on dostępny tylko na niektórych komputerach; obecnie każdy sensowny sprzęt posiada taki licznik, który umożliwia osiągnięcie mniej więcej mikrosekundowej (10-6) dokładności.
Aby skorzystać z tej precyzji, musimy wpierw pobrać częstotliwość tego sprzętowego zegara. Czynimy to funkcją QueryPerformanceFrequency:

  1. LARGE_INTEGER uFreq;
  2. bool bUseQPC = (QueryPerformanceFrequency (&uFreq) != 0);

Możliwy jest oczywiście ten rzadki przypadek, gdy zegar wysokiej częstotliwości jest niedostępny; wówczas wspomniana funkcja zwróci FALSE. W przeciwnym wypadku zapisze nam ona częstotliwość zegara w podanej zmiennej – czyli liczbę jego “tyknięć” na sekundę. Typ LARGE_INTERGER to zaś nic innego jak prosta unia o rozmiarze 8 bajtów, umożliwiająca interpretację zapisanej w niej wartości jako liczby 64-bitowej (pole QuadPart) lub dwóch liczb 32-bitowych (pola HighPart i LowPart).

Gdy już znamy częstotliwość, możemy użyć drugiej z funkcji – QueryPerformanceCounter – do pobrania aktualnej wartości zegara. Wygodnie jest ją sobie opakować, aby rezultat był zwracany od razu w sekundach:

  1. float GetSecs()
  2. {
  3.    if (!bUseQPC)   return GetTickCount() / 1000.0f;
  4.    else
  5.    {
  6.       LARGE_INTEGER uTicks;
  7.       QueryPerformanceCounter (&uTicks);
  8.       return (float)(uTicks.QuadPart / (double)uFreq.QuadPart);
  9.    }
  10. }

Jak widać dodatkowej pracy jest niewiele, a w zamian otrzymujemy możliwość dokładnego pomiaru czasu także wtedy, kiedy w ciągu jednej sekundy udaje nam się generować więcej niż 1000 klatek.

Obliczenia zależne od czasu

Przypomnijmy, że w wyniku tego całego mierzenia poznaliśmy czas, jaki upłynął od ostatniego obiegu naszej pętli (zapisaliśmy go w zmiennej o nazwie dt). Na co nam jednak taka informacja? Otóż jest ona absolutnie niezbędna, aby poprawnie zaprogramować całą mechanikę gry – tak, aby na każdym sprzęcie działała ona tak samo.

Weźmy na przykład jakiś obiekt, który ma się poruszać ze stałą prędkością. Mogłoby się wydawać, że dla osiągnięcia takiego efektu wystarczy w każdej klatce dodawać do jego pozycji taką samą liczbę pikseli.
Nic bardziej błędnego. Przy takim rozwiązaniu faktyczna prędkość obiektu byłaby zależna od szybkości komputera, czyli częstotliwości generowania kolejnych klatek. Jeśli za 10 lat uruchomiono by naszą grę na cztery-pięć razy szybszym sprzęcie, wszystko poruszałoby się w niej odpowiednio szybciej. Tego typu problemy sprawiają starsze gry (jeszcze z czasów DOS-a), w których dopuszczono się podobnego niedopatrzenia. Na współczesnych maszynach działają one tysiące razy szybciej niż powinny, przez co są zupełnie niegrywalne.

Jest tylko jeden sposób na zapewnienie, że gra będzie działała w stałym i takim samym tempie na każdym komputerze. Należy wszystko, co ma się zmieniać wraz z upływającym czasem, programować właśnie w oparciu o ten czas. W wymienionym wyżej przykładzie oznacza to ustalenie prędkości obiektu nie w pikselach na klatkę, lecz na sekundę, i obliczanie jego nowej pozycji w oparciu o interwał czasu, który upłynął:

  1. // pseudokod
  2. void CObject::Update(float dt)
  3. {
  4.    m_Position += m_Velocity * dt;
  5. }

W tym przypadku oznacza to po prostu pomnożenie wartości różnicy czasu (dt) przez wektor prędkości i dodanie wyniku do wektora położenia – zgodnie ze znanym wzorem kinematycznym droga = prędkość * czas, znanym ci jeszcze ze szkoły. Dla innych rodzajów ruchów będzie to oczywiście wyglądało inaczej. Zasada zależności od czasu dotyczy jednak także innych aspektów gry, jak np. animacji (wyświetlanie kolejnych jej klatek) i ogólnie wszystkiego, co jest związane z działaniem gry.

Efektywność pętli, czyli mierzenie wydajności

Obecnie wiemy już, jak wygląda główna pętla typowej gry, dlaczego należy w niej zawsze pobierać aktualny czas i obliczać jego różnicę oraz dlaczego powinniśmy tę właśnie różnicę wykorzystywać w trakcie uaktualniania stanu gry. To aczkolwiek nie jedyny sposób na jej wykorzystanie. Skoro już bowiem mierzymy upływający czas, to możemy pokusić się o prostą ocenę efektywności naszego kodu.

Wartość FPS i różne jej rodzaje

Najprostszą i najpopularniejszą metodą, znaną także wszystkim graczom, jest częstotliwość renderowania klatek– czyli popularnie FPS (frames per second, nie mylić z gatunkiem strzelanek). Większość gier nawet w wersjach wydaniowych umożliwia wyświetlanie tej wartości po wciśnięciu odpowiedniej kombinacji klawiszy lub wyborze jakiejś opcji. Zwykle licznik pojawia się wtedy w którymś z rogów ekranu.
W jaki sposób możemy sami osiągnąć coś takiego? To zależy, jakiego rodzaju wartość FPS chcielibyśmy pokazać. Istnieją bowiem przynajmniej trzy jej rodzaje:

  • chwilowe FPS, wyliczone tylko w oparciu o ostatnią klatkę
  • średnie FPS, obliczone na podstawie szybkości generowania klatek próbkowanej przez pewien krótki czas (np. sekundę)
  • całkowite FPS, przeliczone dla całego czasu działania gry i wszystkich wyrenderowanych klatek

Wartość chwilową wyliczyć zdecydowanie najprościej – to odwrotność czasu liczenia poprzedniej klatki, a więc po prostu 1 / dt. Zmienia się ona więc bardzo często, zatem w praktyce nie moglibyśmy jej po prostu wyświetlać, bo liczba ta byłaby nie do odczytania. Należałoby zatem odświeżać ją rzadziej, co nie jest już takie łatwe. Gra jest przy tym niewarta świeczki, gdyż chwilowe FPS jest niemiarodajne – za bardzo podlega losowym wahaniom wynikłym na przykład z opóźnień w dostępie do stron pamięci czy działania programu szeregującego zadania (procesy i wątki) systemu operacyjnego.
Z kolei jeśli przez moment rozważylibyśmy wartość całkowitego FPS, to też powinniśmy ją odrzucić. Jeśli bowiem wyświetlalibyśmy liczbę skalkulowaną na podstawie całego czasu działania gry, to naturalne jest, że z czasem zmieniałaby się ona coraz mniej. W końcu mogłoby nawet dojść do tego, że obraz na ekranie wyraźnie klatkuje, a licznik FPSów niewzruszenie pokazuje coś w okolicach 50-60 – co byłoby oczywistą bzdurą.
Widać więc, że w tym przypadku najlepsza jest równowaga i najlepiej sprawdza się średnie FPS. Dobrze odzwierciedla ono bieżącą szybkość rysowania, a jednocześnie nie podlega losowym nieregularnościom. Jego wartość zmienia się zazwyczaj co sekundę (chociaż możemy ustalić inaczej), więc nie ma też problemu z jej odczytaniem.

Zresztą obliczenie średniego FPS też nie sprawia specjalnego kłopotu. Należy po prostu zliczać kolejne klatki oraz upływający czas; kiedy przekroczy on sekundę, wystarczy podzielić przez siebie obie te liczby – i już :) Odpowiednio zmodyfikowana główna pętla będzie wyglądała mniej więcej tak:

  1. // zmienne
  2. float fTime, fNewTime, dt;
  3. float fFPS, fFPSTime;
  4. unsigned uFrames;
  5. MSG msg;
  6.  
  7. // pętla
  8. fTime = GetSecs();
  9. fFPSTime= 0.0f;
  10. uFrames = 0;
  11. msg.message = WM_NULL;
  12. while (msg.message != WM_QUIT)
  13. {
  14.    if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
  15.    {
  16.       TranslateMessage (&msg);
  17.       DispatchMessage (&msg);
  18.    }
  19.    else
  20.    {
  21.       // obliczamy czas generowania ostatniej klatki
  22.       fNewTime = GetSecs();
  23.       dt = fNewTime - fTime;
  24.       fTime = fNewTime;
  25.  
  26.       /* Obliczenia i rysowanie */
  27.  
  28.       // ewentualna aktualizacja wartości FPS
  29.       ++uFrames;
  30.       fFPSTime += dt;
  31.       if (fFPSTime >= 1.0f)   // czy minęła sekunda?
  32.       {
  33.          // tak - aktualizacja FPS i zaczynamy liczyć klatki od początku
  34.          fFPS = uFrames / fFPSTime;
  35.          uFrames = 0;
  36.          fFPSTime = 0.0f;
  37.       }
  38.    }
  39. }

To, co zrobimy z wyliczoną wartością zmiennej fFPS, zależy już tylko od nas. Zwyczaj jednak nakazuje, aby się nią chwalić – przynajmniej wówczas, gdy jest ona odpowiednio wysoka :)

Osobliwości FPS #1 – Pionowa synchronizacja

Ku naszemu zdziwieniu może się jednak zdarzyć tak, że nawet dla zupełnie pustej sceny wartość FPS nie przekracza pewnej magicznej granicy, równej zwykle 60 lub 75. Otóż nie ma w tym nic dziwnego, a odpowiedzialna za ten stan rzeczy jest tak zwana pionowa synchronizacja obrazu, czyli w skrócie v-sync.
Najogólniej mówiąc jest to dostrojenie częstotliwości prezentacji kolejnych klatek do częstotliwości odświeżania monitora. Mechanizm ten został pomyślany dla monitorów kineskopowych (CRT), w których kolejne piksele są “wystrzeliwane” na luminofor poczynając od lewego górnego rogu, a skończywszy na prawym dolnym. Odpowiedzialne za to działko elektronowe potrzebuje jednak krótkiej chwili na powrót do pozycji początkowej po wyświetleniu całego obrazu. Moment ten jest idealny, by zastąpić zawartość pamięci wideo nową klatką. W przeciwnym wypadku mogłoby się zdarzyć tak, że zobaczylibyśmy na ekranie fragmenty dwóch osobnych ramek: w dolnej części następną, a w górnej jeszcze poprzednią. Zjawisko to jest zwane szarpaniem (tearing), jako że fragmenty te mogłyby być w widoczny sposób przesunięte w czasie i gdyby rzecz powtarzała się w każdej klatce, obserwowalibyśmy nieprzyjemne ‘rozdarcie’ obrazu.
Podobne zjawisko występuje w monitorach LCD i związane jest z odświeżaniem pikseli przez matrycę ciekłokrystaliczną.

W praktyce pionowa synchronizacja może się sprawdzać zwłaszcza w grach o szybkiej akcji. Na czas testowania jest jednak wysoce niepożądana, gdyż uniemożliwia nam zobaczenie prawdziwej wartości FPS (zwykle 60 lub 75 Hz), która będzie ograniczona przez częstotliwość odświeżania monitora. Dlatego też powinniśmy wiedzieć, jak można wyłączyć v-sync. Większość kart graficznych umożliwia to na poziomie sterownika, lecz każde porządne graficzne API również to potrafi. I tak:

  • W DirectX należy w tym celu podać stałą D3DPRESENT_INTERVAL_IMMEDIATE w polu PresentationInterval struktury D3DPRESENT_PARAMETERS, którą podajemy do funkcji CreateDevice celem stworzenia urządzenia. Z kolei wartość D3DPRESENT_INTERVAL_DEFAULT powoduje włączenie synchronizacji.
  • W OpenGL trzeba posłużyć się rozszerzeniem WGL_EXT_swap_control i wywołać wchodzącą w jego skład funkcję wglSwapIntervalEXT z parametrem 0 dla wyłączniea v-sync lub 1 dla włączenia. (Oczywiście stosują się tutaj wszystkie prawidła korzystania z roszerzeń OpenGL: trzeba więc sprawdzić jego obecność przez glGetString(GL_EXTENSIONS) oraz pobrać adres wspomnianej funkcji przez wglGetProcAddress).
  • W SDL pionowa synchronizacja jest domyślnie wyłączona. Można ją kontrolować wywołaniem SDL_GL_SetAttribute z flagą SDL_GL_SWAP_CONTROL.

Po wyłączeniu synchronizacji mamy pewność, że zobaczymy prawdziwą wartość FPS naszej gry.

Osobliwości FPS #2 – Nieliniowość

Dla pustej sceny wartość ta jest najczęściej bardzo duża – przynajmniej rzędu setek. W pierwszej chwili można się jednak zdziwić, gdyż wraz z dodaniem kilku obiektów prawie na pewno drastycznie się ona zmniejszy. Nie będzie to jednak znaczyć, że kod naszej gry jest strasznie nieefektywny. Wystarczy bowiem kontynuować rozbudowywanie sceny, by w końcu przekonać się, że w pewnym momencie dodawanie nowych obiektów nie wpływa już w widoczny sposób na częstotliwość generowania klatek.

Dlaczego tak się dzieje? Musimy pamiętać, że wartość FPS to w przybliżeniu odwrotność czasu generowania ostatniej klatki (lub odpowiednia średnia). Ten zaś jest mniej więcej proporcjonalny do liczby obiektów, jakie rysujemy. Pomijając współczynniki i ewentualne składniki stałe można więc uznać, że dla X obiektów na scenie wartość FPS wyraża się jako:

FPS(X) = 1 / X

Wykres funkcji FPS
Wykres funkcji FPS (autor: Adam Sawicki “Reg”)

Ta funkcja jest oczywiście nieliniowa i stąd wynikają wspomniane “dziwne” zjawiska. Otóż jeśli zwiększymy jej argument o stałą (czyli wstawimy trochę nowych obiektów) to zmiana FPS będzie zależała od tego, jaka była jego pierwotna wartość. Stąd zwiększenie liczby obiektów z 5 do 10 będzie skutkowało znacznie większym spadkiem częstotliwości klatek niż ze 105 do 110, co odpowiada temu, iż wraz z dodaniem pierwszych obiektów FPS spada gwałtowanie, a później się stabilizuje.

Czy możemy coś na to poradzić? Z matematyką się oczywiście nie wygra :) Powinniśmy być więc świadomi tej własności licznika FPS i traktować go z lekkim przymrużeniem oka. Do poważniejszego profilowania prawdopodobnie lepiej nadaje się sam czas generowania ostatniej klatki, ewentualnie potraktowany podobnym uśrednieniom jak w ostatnim przykładzie. Można bowiem swobodnie założyć, że zależy on liniowo od ilości renderowanej geometrii (lub liczby wywołań funkcji rysujących), zatem nie podlega tak “nieintuicyjnym” zmianom jak FPS.

Podsumowanie

W tym artykule postarałem się odpowiedzieć na pewne często przewijające się pytania na temat ogólnego sposobu działania gier. Mam nadzieję, że okaże się on pomocny w unikaniu typowych pomyłek, jakie zdarzają się początkującym, oraz posłuży jako uzupełnienie wiedzy dla tych bardziej zaawansowanych.

Be Sociable, Share!
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.