Tworzenie gier w JavaScript – Zaawansowany Game Loop

Prawie rok temu wrzuciłem tu posta o tworzeniu game loop’ów w grach JSowych. Bardzo dużo zmieniło się od tego czasu. Przede wszystkim nabrałem doświadczenia i nauczyłem się tego i owego. Innymi słowy, nie jestem już aż takim noobem jak kiedyś 🙂 .

Właśnie dlatego dziś wracam do tematu. Teraz, kiedy stworzyłem już kilka prostych gier, mogę powiedzieć co nieco w temacie 🙂 . W tym poście przedstawię bardziej zaawansowaną implementację pętli gry. Przyda się ona nie tylko w grach ale i we wszelkich animacjach i symulacjach.

Tworzenie gier w Javascript Game Loop

Przez jakiś czas do tworzenia gier używałem frameworka Phaser. Game loop w Phaserze był już zaimplementowany, więc nie musiałem się tym martwić. Jednak dla mojej wrześniowej gry z serii Gra Co Miesiąc, która napisałęm w TypeScript, musiałem napisać własne rozwiązanie. Dzięki temu, że wiem już co nieco o programowaniu gier, wiedziałem jakich błędów, które popełniałem kiedyś, unikać.

Nie jest to może idealna implementacja pętli gry, ale póki co nadaje się świetnie.

Bez zbędnego pisania, przejdę od razu do implementacji:

Ta podstawowa wersja nie jest specjalnie długa ani też mocno skomplikowana. W tym przypadku jednak ważniejsze jest zrozumienie, zarówno tego jak działa oraz dlaczego działa właśnie w ten sposób. W tym miejscu zaznaczę też, że pisanie odpowiednich pętli do gier czy symulacji to bardzo obszerny temat i na pewno nie zostanie wyczerpany w tym poście. Zachęcam to zgłębiania go na własną rękę. Post ten może służyć za preludium 😉 .

Przejdę przez cały kod dokładnie, linijka po linijce. Najpierw deklaruje trzy zmienne. Pierwsza lastFrame, przechowywać będzie dokładny czas (w milisekundach), kiedy narysowana została ostatnia klatka (początkowo równa zero). Kolejna zmienna to delta. Jej wartość to różnica czasów pomiędzy ostatnią klatką a aktualnie rysowaną. Domyślnie, delta również równa się zero. Wartość tych dwóch zmiennych będą nadpisywana z każdym obiegiem pętli. Ostatnia zmienna delta mogłaby być stałą, ponieważ to co przechowuje jest niezmienne. timestep to czas, co jaki gra ma się aktualizować. W tym wypadku 60 razy na sekundę. Docelowo, jest to tyle ile razy wywołuje się requestAnimationFrame. Nie jest to jednak gwarantowane, dlatego trzeba przedsięwziąć odpowiednei środki 🙂 ale o tym za chwilę.

Funkcja mainLoop przyjmuje jeden argument – timestamp. Argument ten jest automatycznie przekazywany przez wywołanie wewnątrz requestAnimationFrame. Wartość którą zawiera to dokładna data (co do milisekundy), wywołania tego obiegu rAF.

Na początku funkcji deklaruje zmienną numUpdateSteps i przypisuję jej wartość zero. Zmienna ta jest zabezpieczeniem na wypadek jakby generowanie klatek było zbyt czasochłonne, ale o tym za chwilę. Drugi krok, to dodanie do aktualnej wartości delta wyniku odejmowania wartości timestamp (data akutalnego wywołania rAF w milisekundach) i lastFrame (data poprzedniego wywołania rAF w milisekundach). Otrzymany wynik to różnica czasu poprzedniego i aktualnego rysowania klatki. W idealnej sytuacji powinna wynosić tyle co wartość zmiennej timestep, jednak nie zawsze tak jest 😉 .

Gdy delta jest już obliczona, przypisuję wartość timestamp do lastFrame, które będzie teraz czekać do kolejnego wywołania rAF.

Kolejny element mainLoop to pętla while. Wykonuje się ona tak długo, dopóki delta jest większa lub równa, timestep. Wewnątrz while wywołuję funkcję aktualizującą stan gry. Jako argument przekazuje mu wartość timestep. Dzięki temu każde wywołanie aktualizacji będzie miało taki sam efekt. Następnie odejmuję wartość timestep od delta.

requestAnimationFrame, powinno wykonywać się 60 razy na sekundę. Jednak nie zawsze tak jest. Wiele czynników wpływa na to ile czasu zajmie przygotowanie nowej klatki. Złożoność obliczeń aktualizacji, szybkość procesora czy dostępne ilości zasobów. Dzięki temu, że kontroluję ile razy w ciągu jednego rAF wykonuje się aktualizacja, mam pewność, że niezależnie od wyżej wymienionych czynników, gra będzie zawsze działać dokładnie tak samo. Jeżeli z jakiegoś powodu przygotowanie klatki zabierze dwa razy dłużej niż zwykle, stan gry zaktualizuje się dwa razy.

Na końcu pętli while sprawdzam, czy aby aktualizacji nie jest za dużo, jeżeli tak oznacza to, że coś poszło nie tak i zatrzymuję pętle. Na koniec wywołuję metodę draw stanu gry. Gdy wszystko jest gotowe, znów wywołuję rAF co powoduje, że pętla wraca do początku.

I to właściwie wszystko. Nawet jeżeli wprowadzanie takiej logiki wydaje się na początku zbędne, uwierzcie mi, jest kluczowe do poprawnego działania każdej gry.

Na koniec mogę zaprezentować też przykład użycia tego w naturze. Ta prosta animacja została stworzona przy użyciu pętli opisanej powyżej. Warto zerknąć w kod tego przykładu, jednak nie będę go tu opisywał w całości. Dla ciekawych, kod dostępny jest pod tym linkiem.

Na dziś to wszystko. Jeśli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *