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

Odkryj możliwości WebGL dzięki nieskończonej, generowanej proceduralnie scenerii w tej grze wyścigowej.

Slow Roads to gra wyścigowa, w której nacisk położono na nieskończoną ilość generowanych proceduralnie krajobrazów. Wszystko to hostowane jest w przeglądarce jako aplikacja WebGL. Dla wielu osób tak intensywne wrażenia mogą wydawać się nie na miejscu w ramach ograniczonego kontekstu przeglądarki. W związku z tym jednym z moich celów w ramach tego projektu było zmienić takie nastawienie. W tym artykule omówię niektóre techniki, których użyłem do pokonania przeszkody związanej ze skutecznością, aby pokazać często pomijany potencjał 3D w internecie.

Tworzenie w 3D w przeglądarce

Po wydaniu aplikacji Slow Roads w opiniach często pojawiał się ten sam komentarz: „Nie wiedziałem, że to możliwe w przeglądarce”. Jeśli podzielasz to zdanie, nie jesteś w żaden sposób w mniejszości. Według ankiety Stan JS w 2022 r. (w języku angielskim) 80% programistów nie eksperymentowało jeszcze z WebGL. Uważam, że to wielka szkoda, że tak wiele możliwości może zostać zmarnowanych, zwłaszcza w przypadku gier w przeglądarce. Mam nadzieję, że dzięki Slow Roads WebGL stanie się bardziej popularny, a liczba deweloperów, którzy zniechęcają się na hasło „wysoko wydajny silnik gier JavaScript”, zmniejszy się.

WebGL może wydawać się tajemniczy i skomplikowany, ale w ostatnich latach jego ekosystemy programistyczne znacznie się rozwinęły, tworząc bardzo wydajne i wygodne narzędzia oraz biblioteki. Deweloperzy front-endu mogą teraz łatwiej niż kiedykolwiek wcześniej stosować w swojej pracy interfejs 3D, nawet bez wcześniejszego doświadczenia w grafice komputerowej. Three.js, najpopularniejsza biblioteka WebGL, stanowi podstawę wielu rozszerzeń, w tym react-three-fiber, która wprowadza komponenty 3D do frameworka React. Dostępne są też kompleksowe edytory gier działające w przeglądarce, takie jak Babylon.js czy PlayCanvas, które oferują znajomy interfejs i zintegrowane zestawy narzędzi.

Pomimo niezwykłej przydatności tych bibliotek ambitne projekty są jednak ograniczone przez ograniczenia techniczne. Ci, którzy są sceptyczni co do gier w przeglądarce, mogą zwrócić uwagę, że JavaScript jest jednowątkowy i ograniczony pod względem zasobów. Jednak po usunięciu tych ograniczeń można uzyskać ukrytą wartość: żadna inna platforma nie oferuje takiej natychmiastowej dostępności i kompatybilności z wielu urządzeniami, jak przeglądarka. Użytkownicy na dowolnym systemie z możliwością korzystania z przeglądarki mogą rozpocząć odtwarzanie jednym kliknięciem, bez konieczności instalowania aplikacji i logowania się w usługach. Nie wspominając o tym, że deweloperzy mogą korzystać z wygodnych, niezawodnych frameworków front-endowych do tworzenia interfejsu użytkownika lub obsługi sieci w trybach wieloosobowych. Te wartości, moim zdaniem, sprawiają, że przeglądarka jest tak doskonałą platformą zarówno dla graczy, jak i dla deweloperów. Jak pokazuje Slow Roads, ograniczenia techniczne często można zredukować do problemu z projektem.

Uzyskiwanie płynnego działania w przypadku wolnych dróg

Główne elementy Slow Roads wymagają szybkiego ruchu i drogiego generowania krajobrazu, więc każda decyzja projektowa była podyktowana potrzebą płynnego działania. Moją główną strategią było rozpoczęcie od uproszczonego projektu rozgrywki, który umożliwiał tworzenie kontekstowych skrótów w ramach architektury silnika. Z drugiej strony, w drodze do minimalizmu musieliśmy zrezygnować z niektórych przydatnych funkcji, ale w efekcie otrzymaliśmy spersonalizowany, wysoce zoptymalizowany system, który działa bez zarzutu w różnych przeglądarkach i na różnych urządzeniach.

Poniżej znajdziesz zestawienie najważniejszych elementów, które sprawiają, że Slow Roads jest lekka.

Dostosowanie silnika środowiska do rozgrywki

Silnik generowania środowiska jest kluczowym elementem gry, więc nieuchronnie jest drogi i słusznie pochłania największą część budżetu na pamięć i przetwarzanie. Trik polega na zaplanowaniu i rozłożeniu na czas wykonywania obciążających obliczeń, aby nie przerywać płynności animacji przez nagłe wzrosty wydajności.

Środowisko składa się z płytek geometrii o różnym rozmiarze i rozdzielczości (zwanych „poziomami szczegółów” lub LoD), w zależności od tego, jak blisko obiektów znajduje się kamera. W typowych grach z kamerą o wolnym poruszaniu się różne poziomy szczegółowości muszą być stale wczytywane i wyłączane, aby szczegółowo odwzorować otoczenie gracza, niezależnie od tego, gdzie się on znajduje. Może to być kosztowna i nieefektowna operacja, zwłaszcza gdy samo środowisko jest generowane dynamicznie. Na szczęście tę konwencję można całkowicie odwrócić w przypadku jazdy po drogach wolnobieżnych dzięki oczekiwaniu kontekstowemu, że użytkownik powinien pozostać na drodze. Zamiast tego szczegółową geometrię można zarezerwować dla wąskiego korytarza bezpośrednio przy trasie.

Diagram pokazujący, jak wygenerowanie drogi z dużym wyprzedzeniem może umożliwić planowanie i zapisywanie w pamięci podręcznej generowania środowiska z wyprzedzeniem.
Geometrya otoczenia w Slow Roads w postaci siatki, wskazująca korytarze z geometryą w wysokiej rozdzielczości wzdłuż drogi. Odległe części środowiska, które nigdy nie powinny być widoczne z bliska, są renderowane w znacznie niższej rozdzielczości.

Środek drogi jest generowany z dużym wyprzedzeniem, co pozwala na dokładne przewidywanie, kiedy i gdzie będą potrzebne szczegóły środowiska. W efekcie otrzymujemy wydajny system, który może aktywnie planować kosztowne zadania, generując tylko minimalną ilość potrzebnych w danym momencie zasobów i nie marnując wysiłku na szczegóły, które nie będą widoczne. Ta technika jest możliwa tylko dlatego, że droga jest pojedynczą ścieżką bez rozgałęzień. Jest to dobry przykład kompromisu w grze, który uwzględnia skróty w architekturze.

Diagram pokazujący, jak wygenerowanie drogi z dużym wyprzedzeniem może umożliwić planowanie i zapisywanie w pamięci podręcznej generowania środowiska z wyprzedzeniem.
Dzięki temu, że patrzymy na pewną odległość wzdłuż drogi, fragmenty środowiska mogą być generowane stopniowo tuż przed tym, jak będą potrzebne. Dodatkowo wszystkie fragmenty, które zostaną ponownie przetworzone w najbliższej przyszłości, mogą zostać zidentyfikowane i zarchiwizowane, aby uniknąć niepotrzebnej regeneracji.

Wymagający wobec praw fizyki

Drugim czynnikiem, który powoduje duże zapotrzebowanie na moc obliczeniową, jest symulacja fizyki. Slow Roads używa niestandardowego, minimalnego silnika fizyki, który wykorzystuje wszystkie dostępne skróty.

Najważniejsze jest unikanie symulowania zbyt wielu obiektów. W tym celu warto stosować minimalizm i nie tworzyć dynamicznych kolizji ani obiektów niszczalnych. Założenie, że pojazd pozostanie na drodze, oznacza, że kolizje z obiektami poza drogą można z pewnym prawdopodobieństwem zignorować. Dodatkowo kodowanie drogi jako rzadkiej linii środkowej umożliwia stosowanie sprytnych trików do szybkiego wykrywania kolizji z powierzchnią drogi i porębami, a wszystko to na podstawie sprawdzania odległości od środka drogi. Jazda w terenie staje się wtedy droższa, ale jest to kolejny przykład sprawiedliwego kompromisu dostosowanego do kontekstu rozgrywki.

Zarządzanie wykorzystaniem pamięci

Ponieważ jest to kolejny zasób ograniczony przez przeglądarkę, pamięć należy zarządzać ostrożnie, mimo że kod JavaScript jest usuwany. Łatwo o tym zapomnieć, ale zadeklarowanie nawet niewielkich ilości nowej pamięci w pętli gry może doprowadzić do poważnych problemów przy działaniu z częstotliwością 60 Hz. Oprócz pochłaniania zasobów użytkownika w kontekście, w którym prawdopodobnie wykonuje on wiele zadań jednocześnie, gromadzenie dużej ilości danych może zająć kilka klatek, co powoduje zauważalne zacinanie. Aby tego uniknąć, pamięć pętli można wstępnie przydzielić w ramach zmiennych klasy podczas inicjalizacji i wykorzystywać ponownie w każdym klatce.

Porównanie profilu pamięci przed i po optymalizacji kodu źródłowego aplikacji Slow Roads, które pokazuje znaczne oszczędności i obniżenie częstotliwości usuwania niepotrzebnych danych.
Mimo że ogólne wykorzystanie pamięci ulega niewielkim zmianom, wstępna alokacja i korzystanie z pamięci pętli może znacznie zmniejszyć wpływ kosztownych operacji sprzątania.

Bardzo ważne jest też, aby cięższe struktury danych, takie jak geometrie i powiązane z nimi bufory danych, były zarządzane ekonomicznie. W grach generowanych w nieskończoność, takich jak Slow Roads, większość geometrii istnieje na rodzaju bieżni – gdy stary element zniknie w dalszej części świata, jego struktury danych można zapisać i ponownie wykorzystać do tworzenia kolejnych elementów świata. Jest to wzór projektowania znany jako zbiornik obiektów.

Te metody pomagają priorytetowo traktować wydajne wykonywanie kodu kosztem jego prostoty. W przypadku aplikacji o wysokiej wydajności należy pamiętać, że funkcje ułatwiające korzystanie z aplikacji czasami korzystają z zasobów klienta na korzyść dewelopera. Na przykład metody takie jak Object.keys() czy Array.map() są bardzo przydatne, ale łatwo można przeoczyć, że każda z nich tworzy nową tablicę dla zwracanej wartości. Poznanie działania takich czarnych skrzynek może pomóc Ci w udoskonaleniu kodu i uniknięciu niespodziewanego spadku wydajności.

Skracanie czasu wczytywania za pomocą generowanych proceduralnie zasobów

Chociaż wydajność w czasie działania powinna być głównym problemem deweloperów gier, nadal obowiązują zwykłe założenia dotyczące początkowego czasu wczytywania strony internetowej. Użytkownicy mogą być bardziej wyrozumiali, gdy świadomie otwierają treści o dużej objętości, ale długi czas wczytywania może negatywnie wpływać na wrażenia użytkowników, a w efekcie – na ich utrzymanie. Gry często wymagają dużych zasobów w postaci tekstur, dźwięków i modeli 3D. Należy je przynajmniej starannie skompresować w miejscach, w których można zredukować szczegóły.

Alternatywnie, można generować zasoby w sposób proceduralny po stronie klienta, aby uniknąć czasochłonnych przesyłań. Jest to ogromna zaleta dla użytkowników z wolnymi połączeniami. Pozwala też deweloperowi na większą kontrolę nad tym, jak skonstruowana jest gra, nie tylko na etapie początkowego wczytywania, ale też w zakresie dostosowywania poziomów szczegółów do różnych ustawień jakości.

Porównanie pokazujące, jak jakość generowanej proceduralnie geometrii w Slow Roads może być dynamicznie dostosowywana do potrzeb użytkownika w zakresie wydajności.

Większość geometrii w Slow Roads jest generowana proceduralnie i uproszczona, a niestandardowe shadery łączą wiele tekstur, aby uzyskać szczegóły. Wadą jest to, że te tekstury mogą być dużymi zasobami, ale i w tym przypadku istnieją możliwości oszczędności, np. za pomocą metody losowego teksturowania, która pozwala uzyskać większą szczegółowość z małych tekstur źródłowych. W najbardziej ekstremalnym przypadku można też wygenerować tekstury całkowicie po stronie klienta za pomocą narzędzi takich jak texgen.js. To samo dotyczy dźwięku, ponieważ interfejs Web Audio API umożliwia generowanie dźwięku za pomocą węzłów audio.

Dzięki komponentom generowanym proceduralnie wygenerowanie początkowego środowiska zajmuje średnio tylko 3, 2 sekundy. Aby w pełni wykorzystać niewielki rozmiar początkowego pliku do pobrania, na początku wyświetla się prosta strona powitalna, która wita nowych użytkowników i opóźnia kosztowne inicjowanie sceny do momentu naciśnięcia przycisku potwierdzającego. Jest to też wygodny bufor dla sesji, które zostały przerwane, co pozwala zminimalizować marnowanie transferu zasobów wczytywanych dynamicznie.

Histograma czasu wczytywania, która pokazuje wyraźny szczyt w ciągu pierwszych 3 sekund, obejmujący ponad 60% użytkowników, a następnie gwałtowny spadek. Z histogramu wynika, że ponad 97% użytkowników widzi czas wczytywania poniżej 10 sekund.

Elastyczne podejście do późnej optymalizacji

Zawsze uważałem, że kod źródłowy Slow Roads jest eksperymentalny, dlatego zastosowałem bardzo elastyczne podejście do rozwoju. Praca z kompleksową i bardzo szybko rozwijającą się architekturą systemu może utrudniać przewidywanie, gdzie mogą wystąpić ważne wąskie gardła. Należy skupić się na szybkim wdrażaniu funkcji, a nie na ich czystości, a następnie na optymalizowaniu systemów tam, gdzie jest to naprawdę ważne. Profilowanie wydajności w Narzędziach deweloperskich w Chrome jest nieocenione na tym etapie i pomogło mi zdiagnozować niektóre poważne problemy w wcześniejszych wersjach gry. Twój czas jako dewelopera jest cenny, dlatego nie trać go na rozważanie problemów, które mogą okazać się nieistotne lub zbędne.

Monitorowanie wrażeń użytkowników

Podczas wdrażania tych wszystkich sztuczek ważne jest, aby upewnić się, że gra działa zgodnie z oczekiwaniami. Dostosowanie się do różnych możliwości sprzętowych jest podstawą każdej gry, ale gry internetowe mogą być kierowane na znacznie szersze spektrum urządzeń, obejmujące zarówno najwyższej klasy komputery, jak i urządzenia mobilne sprzed 10 lat. Najprostszym sposobem jest udostępnienie ustawień dostosowania najbardziej prawdopodobnych wąskich gardeł w kodzie źródłowym (zarówno w przypadku zadań wymagających dużej mocy obliczeniowej GPU, jak i CPU).

Profilowanie na własnym komputerze może być niewystarczające, dlatego warto w jakiś sposób uzyskać opinie użytkowników. W przypadku Slow Roads korzystam z prostych statystyk, które podają informacje o wydajności wraz z czynnikami kontekstowymi, takimi jak rozdzielczość ekranu. Te dane analityczne są wysyłane do podstawowego zaplecza Node za pomocą socket.io wraz z wszelkimi opiniami pisemnymi przesłanymi przez użytkownika za pomocą funkcji w grze. Na początku te dane analityczne wykrywały wiele ważnych problemów, które można było rozwiązać za pomocą prostych zmian w UX, takich jak wyróżnienie menu ustawień, gdy wykryto stale niskie FPS, lub ostrzeżenie, że użytkownik może potrzebować włączenia akceleracji sprzętowej, jeśli wydajność jest szczególnie niska.

Drogi o niskiej prędkości

Nawet po zastosowaniu wszystkich tych środków nadal znaczna część graczy musi korzystać z niższych ustawień – głównie na urządzeniach o małej mocy obliczeniowej, które nie mają karty graficznej. Chociaż zakres dostępnych ustawień jakości prowadzi do dość równomiernego rozkładu wydajności, tylko 52% graczy osiąga ponad 55 FPS.

Definiowana przez ustawienia odległości widoku w porównaniu z ustawieniami szczegółowości matryca, która pokazuje średnią liczbę klatek na sekundę uzyskaną przy różnych kombinacjach. Rozkład jest dość równomierny w zakresie od 45 do 60, przy czym 60 to wartość docelowa dla dobrej skuteczności. Użytkownicy z niskimi ustawieniami mają zwykle niższą liczbę klatek na sekundę niż użytkownicy z wysokimi ustawieniami, co pokazuje różnice w możliwościach sprzętu klienta.
Te dane są nieco zafałszowane przez użytkowników, którzy korzystają z przeglądarki z wyłączonym przyspieszeniem sprzętowym, co często powoduje sztucznie niską wydajność.

Na szczęście nadal istnieje wiele możliwości oszczędzania na wydajności. Oprócz dodania kolejnych trików do renderowania, aby zmniejszyć obciążenie procesora graficznego, mam nadzieję, że w najbliższym czasie będę mógł eksperymentować z workerami internetowymi w ramach równoległego generowania środowiska. Ostatecznie może się okazać, że trzeba będzie włączyć do kodu źródłowego WASM lub WebGPU. Każda ilość miejsca, jaką uda mi się zwolnić, pozwoli na tworzenie bogatszych i bardziej zróżnicowanych środowisk, co będzie stałym celem do końca projektu.

W ramach projektu hobbystycznego Slow Roads udało nam się w spełnieniu pokazać, jak zaskakująco rozbudowane, wydajne i popularne mogą być gry w przeglądarce. Jeśli udało mi się wzbudzić Twoje zainteresowanie WebGL, wiedz, że technologia Slow Roads to tylko jeden z mniejszych przykładów jej możliwości. Gorąco zachęcam do zapoznania się z prezentacją Three.js. Osoby zainteresowane tworzeniem gier internetowych mogą dołączyć do społeczności na stronie webgamedev.com.