Slow Roads intryguje zarówno graczy, jak i deweloperów, prezentując zaskakujące możliwości 3D w przeglądarce

Odkryj potencjał WebGL w nieskończonej, proceduralnej scenerii tej rekreacyjnej gry wyścigowej.

Wolne drogi to gra rekreacyjna z elementami przedstawiającymi niekończącą się scenerię i praktyczną obsługę. Wszystkie elementy są hostowane w przeglądarce jako aplikacja WebView. Dla wielu osób tak intensywne doświadczenie może wydawać się nietrafne w ograniczonym kontekście przeglądarki, a zmiana tego podejścia jest jednym z moich celów w tym projekcie. W tym artykule omówię niektóre techniki, które użyłem podczas trudnych działań w sieci.

Programowanie 3D w przeglądarce

Po opublikowaniu Powolnych dróg zaczęło się pojawiać w komentarzach: „Nie wiedziałem(-am), że jest taka możliwość w przeglądarce”. Jeśli podzielacie się z nami, z pewnością nie należysz do mniejszości. Zgodnie z ankietą State of JS z 2022 r. 80% deweloperów musi jeszcze poeksperymentować z WebGL. Wydaje mi się szkoda, że tak dużo potencjalnych możliwości można przeoczyć, zwłaszcza jeśli chodzi o gry w przeglądarce. Mam nadzieję, że Slow Roads uda mi się uwydatnić WebGL i zmniejszyć liczbę programistów, którzy nie zgadzają się na hasło „wydajny silnik gier JavaScript”.

Dla wielu użytkowników WebGL może wydawać się tajemniczy i skomplikowany, ale w ostatnich latach jego ekosystemy programistyczne w dużej mierze rozwinęły się i stworzyły niezwykle wydajne i wygodne narzędzia oraz biblioteki. Stosowanie interfejsu 3D w swojej pracy przez programistów interfejsów jest teraz łatwiejsze niż kiedykolwiek, nawet jeśli nie mają doświadczenia w grafice komputerowej. Three.js, czołowa biblioteka WebGL, służy jako podstawa wielu rozszerzeń, m.in. funkcji react-three-fiber, która wprowadza komponenty 3D do platformy React. Istnieją teraz również wszechstronne internetowe edytory gier, takie jak Babylon.js czy PlayCanvas, które oferują znajomy interfejs i zintegrowane łańcuchy narzędzi.

Pomimo ogromnej przydatności tych bibliotek ambitne projekty wiążą się z końcem ograniczeń technicznych. Sceptycy wobec koncepcji gier w przeglądarkach mogą podkreślić, że JavaScript jest jednowątkowy i ograniczony zasobami. Jednak zmierzenie się z tymi ograniczeniami odblokowuje ukrytą wartość – żadna inna platforma nie oferuje takiej samej natychmiastowej dostępności i masowej zgodności, jaka jest dostępna w przeglądarce. Użytkownicy każdego systemu z przeglądarką mogą rozpocząć grę jednym kliknięciem, bez konieczności instalowania aplikacji ani logowania się do usług. Nie wspominając o wygodzie deweloperów, którzy cenią sobie zaawansowane, wydajne platformy frontendu do tworzenia UI lub obsługi sieci w trybie wieloosobowym. Uważam, że te wartości sprawiają, że przeglądarka jest niezwykle doskonałą platformą zarówno dla graczy, jak i deweloperów. I jak pokazują Slow Roads, ograniczenia techniczne często wiążą się z problemami projektowymi.

Płynna akcja w Powolnych drogach

Głównymi elementami Slow Roads są szybkie poruszanie się i kosztowne generowanie krajobrazów, dlatego każda decyzja dotycząca projektowania jest uzależniona od potrzeby płynnego działania. Moją główną strategią było rozpoczęcie gry w prostym stylu, który pozwolił na wykorzystanie w architekturze silnika skrótów kontekstowych. Z drugiej strony oznacza to rezygnację z kilku przydatnych funkcji w celu osiągnięcia minimalizmu, ale w efekcie powstaje dostosowany, hiperoptymalnie zoptymalizowany system, który działa dobrze w różnych przeglądarkach i na różnych urządzeniach.

Oto zestawienie najważniejszych elementów, które sprawiają, że Powolne drogi nie są „nadmuchowane”.

Opracowywanie mechanizmu środowiska pod kątem rozgrywki

Mechanizm generowania środowiska to kluczowy element gry, dlatego nieunikniony jest kosztowny, więc w uzasadniony sposób pochłania on największą część budżetów na pamięć i moc obliczeniową. Ta metoda polega na planowaniu i rozdzielaniu intensywnych obliczeń na określony czas, aby nie zakłócać liczby klatek przy nagłych wzrostach wydajności.

Środowisko składa się z geometrycznych fragmentów, które różnią się rozmiarem i rozdzielczością (są klasyfikowane jako „poziomy szczegółowości” lub „LoDs”) w zależności od tego, jak blisko kamery będą one widoczne. W typowych grach z kamerą ruchomą muszą być stale wczytywane i wczytywane, aby dokładnie przedstawić otoczenie gracza, dokąd się uda. Może to być kosztowne i marnotrawstwo, zwłaszcza gdy środowisko jest generowane dynamicznie. Na szczęście w przypadku Powolnych dróg ten konwencja może zostać całkowicie zakłócona, ponieważ użytkownik musi pozostać na drodze. Szczegółowe geometrię można zarezerwować dla wąskiego korytarza sąsiadującego z trasą.

Diagram pokazujący, jak generowanie drogi z dużym wyprzedzeniem może umożliwić proaktywne planowanie i buforowanie środowiska w pamięci podręcznej.
Widok geometrii środowiska na Powolnych drogach wyrenderowany jako szkielet, wskazujący korytarze o wysokiej rozdzielczości otaczające drogę. Odległe fragmenty środowiska, których nigdy nie powinno być widoczne z bliska, są renderowane w znacznie niższej rozdzielczości.

Środek drogi jest generowany daleko przed pojawieniem się gracza, dzięki czemu można dokładnie przewidywać, kiedy i gdzie potrzebne są szczegóły środowiska. W efekcie powstał przejrzysty system, który może proaktywnie planować kosztowne prace, generując w każdej chwili tylko minimalną wymaganą ilość danych, a jednocześnie eliminuje niepotrzebne szczegóły. Ta technika jest możliwa tylko dlatego, że droga jest pojedynczą, nierozgałęziającą się ścieżką. Jest to dobry przykład tego, jak wiązać się ze skrótami architektonicznymi i kompromisami w rozgrywce.

Diagram pokazujący, jak generowanie drogi z dużym wyprzedzeniem może umożliwić proaktywne planowanie i buforowanie środowiska w pamięci podręcznej.
Na podstawie danych o pewnej odległości wzdłuż drogi można wstępnie wycofywać fragmenty środowiska i generować je stopniowo tuż przed ich potrzebą. Poza tym wszystkie fragmenty, które zostaną ponownie sprawdzone w najbliższej przyszłości, mogą być rozpoznawane i zapisywane w pamięci podręcznej, aby uniknąć niepotrzebnego ponownego generowania.

Przestrzeganie praw fizyki

Oprócz zadań obliczeniowych silnika środowiska należy symulacja fizyki. Gra Slow Roads wykorzystuje niestandardowy, minimalistyczny silnik fizyczny, który uwzględnia wszystkie dostępne skróty.

Najważniejsze jest to, aby uniknąć symulowania zbyt wielu obiektów i wejść do minimalnego, zenowego kontekstu przez zmniejszenie liczby takich elementów jak dynamiczne kolizje i obiekty możliwe do zniszczenia. Założenie, że pojazd pozostanie na drodze, oznacza, że kolizje z obiektami terenowymi można racjonalnie zignorować. Dodatkowo kodowanie drogi jako rozproszonej linii środkowej umożliwia stosowanie eleganckich trików do szybkiego wykrywania kolizji z nawierzchnią drogi i barierkami, a wszystko odbywa się w zależności od odległości od środka drogi. Jazda w terenie staje się droższa, ale to kolejny przykład uczciwego kompromisu dostosowanego do kontekstu rozgrywki.

Zarządzanie wykorzystaniem pamięci

To kolejne z zasobów ograniczonych do przeglądarki, dlatego ważne jest, aby rozważnie zarządzać pamięcią – nawet jeśli kod JavaScript jest czyszczony w sposób nieczytelny. Czasami łatwo to przeoczyć, ale zadeklarowanie nawet niewielkiej ilości nowej pamięci w pętli gry może spowodować poważne problemy podczas pracy z częstotliwością 60 Hz. Poza chłodzeniem zasobów użytkownika w kontekście, w którym prawdopodobnie wykonuje on wiele zadań jednocześnie, duże zbiory śmieci mogą zajmować nawet kilka klatek, co powoduje zauważalne zacinanie się. Aby tego uniknąć, pamięć pętli można wstępnie przydzielić w zmiennych klas w momencie inicjowania i ponownie wykorzystać w każdej klatce.

Widok profilu pamięci „przed i po” podczas optymalizacji bazy kodu Powolnych dróg, co wskazuje na znaczne oszczędności i zmniejszenie wskaźnika odpadów.
Chociaż ogólne wykorzystanie pamięci nie zmienia się, wstępne przydzielanie pamięci i recykling pamięci mogą znacznie zmniejszyć wpływ kosztownych odpadów.

Ważne jest również, aby cięższe struktury danych, takie jak geometrie i powiązane z nimi bufory danych, były zarządzane w sposób ekonomiczny. W nieskończonej liczbie gier, takich jak Slow Roads, większość elementów geometrycznych znajduje się na bieżni. Gdy w oddali znajdzie się stary element, jego struktury danych można przechowywać i ponownie wykorzystywać na potrzeby nadchodzącego świata, znanego jako łączenie obiektów.

Praktyki te pomagają w ustalaniu, na czym polegają priorytetowe działania w ramach usprawnienia, a jednocześnie wymagają pewnej prostoty kodu. W kontekstach o wysokiej wydajności warto pamiętać, że funkcje udogodnień dla dewelopera czasami działają na korzyść klienta. Na przykład metody takie jak Object.keys() czy Array.map() są niezwykle przydatne, ale łatwo zauważyć, że każda z nich tworzy nową tablicę dla swojej wartości zwracanej przez użytkownika. Zrozumienie mechanizmu działania takich czarnych pól może Ci pomóc doprecyzować kod i uniknąć podstępnych działań.

Skrócenie czasu wczytywania dzięki zasobom generowanym proceduralnie

Wydajność w czasie działania jest dla deweloperów gier najważniejszym pytaniem, ale wciąż obowiązują tu typowe wątpliwości dotyczące początkowego czasu wczytywania strony. W przypadku świadomego dostępu do ciężkich treści użytkownicy mogą wyrażać się bardziej cierpliwie, ale długie czasy wczytywania mogą negatywnie wpływać na ich wrażenia, jeśli nie uda się utrzymać użytkowników. Gry często wymagają dużych zasobów w postaci tekstur, dźwięków i modeli 3D. Należy je co najmniej starannie skompresować, gdy nie marnuje się szczegółów.

Procedury generowania zasobów po stronie klienta pozwalają też uniknąć długich transferów. Jest to bardzo przydatne w przypadku użytkowników, którzy korzystają z wolnego połączenia, i zapewnia deweloperowi większą bezpośrednią kontrolę nad układem gry – nie tylko na wstępnym etapie wczytywania, ale także podczas dostosowywania poziomów szczegółów do różnych ustawień jakości.

Porównanie ilustrujące sposób, w jaki jakość generowanej proceduralnie geometrii przy powolnych drogach może zostać dynamicznie dostosowywana do potrzeb użytkownika w zakresie działania.

Większość elementów geometrycznych w przypadku Powolnych dróg jest generowana proceduralnie i jest uproszczona, a niestandardowe narzędzia do cieniowania łączą wiele tekstur, aby uwydatnić szczegóły. Wadą jest to, że te tekstury mogą być ciężkie, choć w tym przypadku istnieją dodatkowe możliwości oszczędności. Metody takie jak stochastyczne tekstury pozwalają uzyskać więcej szczegółów w przypadku małych tekstur źródłowych. Bardzo możliwe jest również generowanie tekstur w całości na kliencie za pomocą takich narzędzi jak texgen.js. To samo dotyczy audio, przy czym interfejs Web Audio API umożliwia generowanie dźwięku z węzłami audio.

Dzięki zaletom zasobów proceduralnych wygenerowanie środowiska początkowego zajmuje średnio tylko 3, 2 sekundy. Aby w pełni wykorzystać mały rozmiar pobieranego z góry, nowi użytkownicy mogą korzystać z prostego ekranu powitalnego, który odkłada kosztowne uruchomienie sceny do momentu, gdy trzeba wyrazić na to zgodę. Pełni też funkcję wygodnego bufora w przypadku sesji porzuconych, minimalizując zmarnowany transfer zasobów wczytywanych dynamicznie.

Histogram czasów wczytywania pokazujących szczytowy wzrost w ciągu pierwszych 3 sekund, który odpowiada ponad 60% użytkowników, a następnie gwałtowny spadek. Histogram pokazuje, że ponad 97% użytkowników widzi czas wczytywania krótszy niż 10 sekund.

Elastyczne podejście do późnej optymalizacji

Bazę kodu dla Slow Roads zawsze uważałem za eksperymentalną i dlatego jej projektowanie wymaga teraz bardzo zmysłu. W przypadku pracy ze złożoną i szybko ewoluującą architekturą systemową trudno jest przewidzieć, gdzie mogą wystąpić ważne wąskie gardła. Skup się na szybkim, a nie czytelnym wdrożeniu oczekiwanych funkcji, a potem na optymalizacji systemów tam, gdzie jest to naprawdę ważne. Narzędzie profilu wydajności dostępne w Narzędziach deweloperskich w Chrome jest nieocenione na tym etapie i pomogło mi zdiagnozować poważne problemy z wcześniejszymi wersjami gry. Twój czas jako programisty jest cenny, więc nie poświęcaj go na rozpatrywanie problemów, które mogą okazać się nieistotne lub zbędne.

Monitorowanie wrażeń użytkowników

Podczas wykonywania tych czynności należy zadbać o to, aby gra działała zgodnie z oczekiwaniami. Możliwość korzystania z szerokiej gamy możliwości sprzętowych to podstawowy aspekt tworzenia gier, ale gry internetowe mogą być kierowane na znacznie szerszą gamę urządzeń, na przykład na wysokiej klasy komputery i starsze urządzenia mobilne. Najprostszym sposobem na rozwiązanie tego problemu jest zaoferowanie ustawień pozwalających dostosować najbardziej prawdopodobne wąskie gardła w bazie kodu – zarówno w przypadku zadań wymagających dużej mocy procesora, jak i GPU – zgodnie z informacjami uzyskanymi przez program profilujący.

Profilowanie na własnym komputerze może jednak objąć niewiele, dlatego warto w jakiś sposób zacieśnić pętlę informacji zwrotnych dla użytkowników. W przypadku Powolnych dróg przeprowadzam proste analizy, które podają informacje o wydajności oraz czynnikach kontekstowych, takich jak rozdzielczość ekranu. Te statystyki są wysyłane do podstawowego backendu węzła przy użyciu socket.io, wraz z wszelkimi pisemnymi opiniami przesłanymi przez użytkownika za pomocą formularza w grze. Na początku analityka wychwytywała wiele ważnych problemów, które można złagodzić przez proste zmiany w interfejsie, takie jak wyróżnianie menu ustawień w przypadku wykrycia stale niskiej liczby FPS lub ostrzeganie użytkownika o konieczności włączenia akceleracji sprzętowej, jeśli wydajność jest wyjątkowo niska.

Wolne drogi przed Tobą

Nawet po wykonaniu tych wszystkich czynności spora część graczy nadal musi grać na niższych ustawieniach, zwłaszcza tych korzystających z lekkich urządzeń, które nie mają GPU. Chociaż zakres dostępnych ustawień jakości zapewnia względnie równomierną dystrybucję wydajności, tylko 52% graczy osiąga wyniki powyżej 55 FPS.

Macierz zdefiniowana przez ustawienie odległości oglądania w stosunku do ustawień szczegółów. Pokazuje średnią liczbę klatek na sekundę przy różnych połączeniach. Rozkład jest stosunkowo równomierny z przedziału od 45 do 60, przy czym 60 to cel zapewniający dobrą skuteczność. Użytkownicy z niskimi ustawieniami zwykle uzyskują mniejszą liczbę klatek na sekundę niż te przy wysokich ustawieniach, co podkreśla różnice w możliwościach sprzętowych klienta.
Dane te są w pewnym stopniu zniekształcone przez użytkowników, którzy korzystają z przeglądarki z wyłączoną akceleracją sprzętową, co często powoduje sztucznie obniżoną wydajność.

Na szczęście jest wiele sposobów na obniżenie wydajności. Oprócz dodawania kolejnych sztuczek renderowania w celu ograniczenia zapotrzebowania na GPU mam nadzieję w najbliższym czasie poeksperymentować z pracownikami internetowymi, aby w najbliższym czasie generować równolegle generowanie środowiska. W przyszłości być może zajdzie potrzeba zastosowania do bazy kodu standardu WASM lub WebGPU. Umożliwi mi to tworzenie bogatszych i bardziej zróżnicowanych środowisk, co będzie długofalowym celem w dalszej części projektu.

Wraz z rozwojem hobbystycznego Slow Roads udało się pokazać, jak zaskakująco zaawansowane, wydajne i popularne mogą być gry przeglądarkowe. Jeśli udało mi się rozbudzić Twoje zainteresowanie WebGL, pamiętaj, że technologia „Wolna droga” stanowi zaledwie płytki przykład jej potencjału. Zdecydowanie zachęcam do zapoznania się z prezentacją Three.js, a osoby zainteresowane tworzeniem gier internetowych są mile widziane na stronie webgamedev.com.