Intense.js – framework dla gier HTML5

Na studiach byłem zafascynowany możliwościami HTML5 i chciałem pokazać, że nadaje się on nawet do tworzenia gier. Stworzyłem w tym celu własny framework i przyszedł czasy aby wypuścić go na szerokie wody. Niech inspiruje!

Studia były dla mnie okresem w którym fascynowałem się zarówno nowymi możliwościami „webu” jak i czasem w którym byłem zapalonym graczem. Praca magisterska, która mogłaby połączyć oba zagadnienia była dla mnie więc nie tylko ciekawym wyzwaniem, ale także i niesamowitą przyjemnością. Wymyśliłem sobie, że wykonam framework do gier w paradygmacie ECS (ang. Entity-Component-System), który mniej więcej opisałem we wstępie o A-Frame. Framework nazwałem intense.js, ponieważ miał on przede wszystkim być dla gier multiplayer i takich w których rozgrywka może być „intensywna”.



Praca okazała się sukcesem, a ja sam byłem z siebie niezwykle dumny, ponieważ w krótkim czasie nauczyłem się na prawdę wiele. Z perspektywy czasu patrzę jednak na całość z lekkim przymrużeniem oka, bo projekt był pisany jak wielki monolit, a przecież dzielenie kodu w JSie było już możliwe za sprawą chociażby require.js. Mimo wszystko postanowiłem podzielić się nim ze światem na githubie, bo być może ktoś mądrzejszy ode mnie wyciągnie z niego coś ciekawego. Framework można znaleźć pod tym linkiem. Inspiracją były dla mnie biblioteki Flashowe, które wyprzedzały na tamten czas HTML5 zarówno możliwościami jak i dojrzałością. Świetnym przykładem frameworka ECS jest Ash Framework, który jak widzę obecnie nie jest już rozwijany. Dzisiejszy wpis jest więc dla mnie swoistym wehikułem czasu i chciałbym podzielić się z Wami wiedzą, którą na tamten czas zebrałem. Będą to fragmenty mojej pracy magisterskiej będącej wstępem do implementacji przyjętego rozwiązania. Zapraszam do przeszłości 🙂

Metodyka programowania w oparciu o komponenty

W trakcie prac projektowych nad frameworkiem powstał problem zaoferowania programiście przystępnej składni, która ukrywałaby skomplikowane mechanizmy pod dużą warstwą abstrakcji a także wprowadzała wygodną architekturę do rozszerzania możliwości, bez ingerencji w kluczowe elementy kodu. Podczas analizy wzorców projektowych do tworzenia gier, autor znalazł rozwiązanie tego problemu w postaci metodyki nazywanej programowaniem w oparciu o komponenty (ang. Entity component system). Jest to stosunkowo nowy paradygmat programowania, stosowany najczęściej w przypadku projektowania gier, który nie posiada jednoznacznie zdefiniowanej architektury. Istnieje zarówno wiele różnych sposobów na zaprojektowanie architektury komponentowej, jak i sposobów implementacji wybranego podejścia (http://entity-systems.wikidot.com/es-approaches).

Genezą powstania nowej metodyki był problem zarządzania dużą ilością obiektów w grach. Współcześnie większość projektów programistycznych w tym gier komputerowych, tworzonych jest za pomocą paradygmatu obiektowego. Zaletą takiego rozwiązania jest jasny podział obowiązków w kodzie i możliwość ponownego wykorzystania skonstruowanych elementów programu w innych częściach kodu. Jednostki w grach tworzone są za pomocą obiektów będących instancją klas, które definiują ich właściwości oraz zachowanie. Aby dany obiekt zajmował się tylko jednym określonym zadaniem, tworzona jest hierarchia klas, gdzie wspólne elementy kodu umieszczane są w klasach bazowych w hierarchii dziedziczenia. Pozwala to na niepowielanie wspólnych funkcjonalności i tworzenie specjalizowanych obiektów, które hermetyzują prywatne właściwości i udostępniają metody do komunikacji z innymi obiektami. Model ten sprawdza się w przypadku aplikacji a także prostych gier. Wraz ze wzrostem poziomu skomplikowania gry, metodyka obiektowa może spowodować potrzebę całkowitej reorganizacji kodu. Jest to związane z rolą jaką pełnią obiekty w tradycyjnych aplikacjach w przeciwieństwie do obiektów stosowanych w grach. Obiekty w aplikacjach posiadają zazwyczaj jasno określone role, które bardzo rzadko są zmieniane w trakcie działania, lub też jedynie w niewielkim stopniu. Z kolei w przypadku gier, obiekty mogą bardzo często zmieniać w trakcie działania nie tylko właściwości, ale również swoje zachowania, zmieniając zupełnie przypisaną początkową rolę. Przykładowo tworząc grę z gatunku fantasy, można zdefiniować główne klasy bazowe dla obiektów statycznych i dynamicznych, niezbędne do współdzielenia potrzebnych właściwości i metod. W przypadku gdy w trakcie działania obiekt statyczny będzie chciał uzyskać zachowanie obiektu dynamicznego, programista nie ma możliwości przecięcia hierarchii dziedziczenia w taki sposób, aby nie powielać już raz użytego kodu. Próbą rozwiązania tego problemu jest stworzenie wielu bardziej sprecyzowanych klas bazowych, lub też zupełna rezygnacja z dziedziczenia na rzecz kompozycji wraz ze zwiększoną ilością potrzebnych atrybutów i metod dla obiektów. Oba rozwiązania powodują problem w znalezieniu proporcji pomiędzy ilością powielanych informacji a wielkością tworzonych obiektów. Obiekty zawierające zbyt wiele funkcjonalności, które często mogą nie być w ogóle wykorzystywane dla danej instancji to tzw. „boskie obiekty” (ang. god objects) i są antywzorcem programowania.

Założenia metodyki ECS

W przeciwieństwie do programowania obiektowego, podejście oparte o komponenty skupia się na danych a nie na obiektach i z tego względu jest często porównywane do paradygmatu zorientowanego na dane (ang. Data-Oriented Design). Metodyka oparta o komponenty pozwala na dynamiczną manipulację zachowaniem obiektów w trakcie wykonywania programu i pozwala na szybką modyfikację elementów gry ze względu na jasny podział zadań. Całość oparta jest o następujące pojęcia:

  • Encja – nazywana również jednostką (ang. entity), reprezentuje pojedynczy obiekt w grze. Może to być zarówno obiekt statyczny, jak i dynamiczny. Encja nie posiada żadnych właściwości ani metod, to co ją identyfikuje i nadaje charakter to zbiór komponentów z danymi. Stanowi to swego rodzaju „pojemnik” na komponenty, który może być utożsamiany z instancją klasy w programowaniu obiektowym. W wielu architekturach encja jest prostym obiektem posiadającym unikalne pole za pomocą którego można ją zidentyfikować. Istnieją także podejścia oparte na relacyjnych bazach danych, w których encja jest po prostu identyfikatorem numerycznym.
  • Komponent – jest prostym obiektem definiującym dane. Zbiór komponentów składa się na stan encji. Komponenty nie powinny posiadać zachowań, jednakże w wielu podejściach umieszcza się w nich metody dostępowe do danych i metody służące obróbce tych danych. W architekturach zbliżonych do relacyjnych baz danych, komponenty stanowią krotkę w tabeli, której unikalnym kluczem jest encja.
  • System – definiuje logikę dla encji zawierającej określone komponenty. Odseparowanie danych od zachowania jest największą różnicą w stosunku do programowania obiektowego. W metodyce opartej o komponenty logika programu znajduje się w systemach, które z kolei operują na danych dostarczanych przez komponenty. Jest to możliwe dzięki braku enkapsulacji danych w komponentach. Każdy system realizuje określone zadanie i operuje jedynie na tych encjach, które posiadają wymaganą grupę komponentów przez system. W niektórych podejściach, aby ułatwić operacje na grupach komponentów wprowadza się dodatkową strukturę zwaną węzłem (ang. node). W takiej strukturze komponenty posiadają ułatwioną możliwość wymiany danych. Istnieją także frameworki komponentowe, w których pojęcie systemu nie występuje a logiką zajmują się komponenty. Architektury oparte na relacyjnych bazach danych identyfikują system jako tabelę, zawierającą wiersze (komponenty) o unikalnych identyfikatorach (encje).

Za pomocą powyższego podziału możliwa jest dynamiczna zmiana zachowania obiektu poprzez zmianę jego komponentów. Metodyka oparta o komponenty ułatwia również zarządzanie główną pętlą gry, która przekazuje całą mechanikę gry do odpowiednich systemów. Ze względu na brak dokładnego sprecyzowania wyglądu architektury, istnieje wiele podejść do realizacji tego problemu.

Hybrydowa natura intense.js

Podstawowym założeniem tworzonego frameworka było połączenie architektury opartej o komponenty z wygodą pisania programowania obiektowego. Język JavaScript, który jest podstawą frameworka okazał się do tego celu idealny, ze względu na swoją prototypową naturę. Framework intense zachowuje podstawowe założenia metodyki opartej o komponenty i udostępnia programiście szybkie mechanizmy do manipulacji obiektami. Jest więc frameworkiem hybrydowym, ponieważ łączy dwie metodologie w jeden, spójny interfejs programistyczny.

Cały framework został zamknięty w funkcję natychmiastową, aby zapewnić hermetyczne wnętrze i chronić funkcje prywatne przed niewłaściwym wykorzystaniem. W przypadku środowiska przeglądarki, cała funkcjonalność jest widoczna w przestrzeni intense, rejestrowanej jako globalna zmienna. Ta sama przestrzeń jest wykorzystywana jako moduł w przypadku środowiska node.js. Programista może wykorzystać metodę expose aby w szybki sposób zarejestrować moduły w przestrzeni globalnej. Framework intense składa się z następujących modułów:

  • intense.utils – przestrzeń nazw funkcji dodatkowych, wspierających podstawowe operacje na obiektach.
  • intense.debug – moduł zawierający funkcje wspierające logowanie zdarzeń i wykrywanie etapów działania frameworka.
  • intense.controls – moduł odpowiedzialny za obsługę klawiatury, myszy oraz ekranu dotykowego w przypadku urządzeń mobilnych.
  • intense.support – moduł wspierający wykrywanie funkcjonalności przeglądarek.
  • intense.init – metoda inicjalizująca framework.
  • intense.game – konstruktor gry.
  • intense.assetManager – manager zewnętrznych zasobów.
  • intense.drawing – moduł funkcji wspierających rysowanie.
  • intense.engine – moduł sterujący główną pętlą gry.
  • intense.poolManager – moduł zarządzającymi pulą obiektów.
  • intense.io – moduł połączeniowy do obsługi web sockets oraz wywołań ajaxowych.
  • intense.systemManager – manager obsługi systemów.
  • intense.componentManager – manager obsługi komponentów.
  • intense.entityManager – manager obsługi encji.
  • intense.eventManager – manager obsługi zdarzeń.

Oprócz powyższych modułów, intense w swojej przestrzeni udostępnia programiście także następujące struktury:

  • intense.fps – licznik szybkości rysowania gry.
  • intense.cfg – przestrzeń dla zmiennych konfiguracyjnych.
  • intense.keyDown – alias dla metody modułu intense.controls.keyDown sprawdzającej czy został wciśnięty dany klawisz.
  • intense.version – metoda zwracająca numer wersji frameworka.
  • intense.EntitySet – alias dla konstruktora intense.entityManager.EntitySet, tworzącego zbiór encji.
  • intense.system – konstruktor systemów a także metoda zwracająca istniejący system.
  • intense.renderSystem – konstruktor systemów typu „render”.
  • intense.netSystem – konstruktor systemów typu „net”.
  • intense.component – konstruktor komponentów a także metoda zwracająca istniejący komponent.
  • intense.entity – konstruktor encji a także metoda zwracająca istniejącą encję.
  • intense.expose – metoda udostępniająca moduły w globalnej przestrzeni nazw przeglądarki.

Rozszerzanie możliwości frameworka odbywa się za pomocą komponentów i systemów, bez ingerencji w pozostałe moduły. Kod frameworka używa tzw. trybu ścisłego (ang. strict mode), który wprowadza zgodność z przyszłymi wersjami języka JavaScript i zapobiega używaniu newralgicznych instrukcji, które mogą negatywnie wpłynąć na wydajność aplikacji.

Powrót do rzeczywistości

Wspaniale było ponownie zanurzyć się w dawny projekt, który nadal napawa mnie ogromną dumą i chciałbym w kolejnych wpisach pokazać kolejne jego elementy wraz z dokładniejszym opisem poszczególnych części. W końcu trochę się w tej pracy rozpisałem 😉 Zachęcam wszystkich aby odkrywali swojego stare projekty, wracali do przeszłości i nie bali się chwalić swoimi małymi odkryciami. A nuż taki projekt stanie się furtką do nowych wyzwań? Któż to wie, czasem warto zaryzykować!