W tym poście chciałbym zawrzeć część ważniejszych koncepcji związanych z tworzeniem gier.
Właściwie jest to podsumowanie moich przemyśleń z kilku lat praktyki w pisaniu gier.
Zatem do dzieła!
- Podział gry na moduły.
Pisząc grę czy to inne aplikacje, ważne jest to, aby jasno wydzielić granicę poszczególnych dziedzin tj. Jako osobny moduł należy wydzielić Widok(to co gracz widzi), Kontrole(to co gracz może zrobić w grze, przyjmowanie danych z klawiatury, ekranu dotykowego i wstępne jego przetwarzanie) oraz Logikę(to jak gra działa).
- Zasada tworzenia wąskich punktów powiązania:
Między tymi modułami należy zastosować taki interfejs żeby umożliwiał wykonywanie tylko tego co jest niezbędne i pobieranie tego co jest potrzebne. W taki sposób że Kontroler nigdy nie zmieni bezpośrednio pozycji jakiegoś przedmioty, tylko powiadomi o tym Mechanikę. Natomiast Widok będzie mógł co najwyżej pobrać aktualne pozycje obiektów w świecie gry, ale nigdy na nich nic nie wykona.
- Zasada maksymalnej dekompozycji: mówi nam że z tych 3 modułów należy jeszcze podzielić elementy na składowe. Aby tak uprościć projekt jak to możliwe, ale nie bardziej niż to potrzebne.
- Zasada nie wszystko na raz: Należy tak tworzyć aplikacje aby w ciągu pewnych skoków czasowych(np. 3 dnich, tygodnia, dwóch tygodni) wypuścić chociaż developersko wersję którą można skompilować i wykazać elementy które się zrobiło. Dlatego też ważnym jest że nie wolno nam rozpocząć pracy od tworzenia tylko samej mechaniki, lub widoku i robić ją w całości. Na całych tych trzech głównych warstwach należy działać w cyklu sprintu(skoku). Dzięki tej zasadzie nasza aplikacja rozwija się w sposób naturalny, a efekty są widoczne natychmiast po wykonaniu zadania.
- Zanim napiszesz coś, najpierw to zaprojektuj: Ważnym elementem przed przystąpieniem do pisania czegokolwiek jest przedstawienie go chociażby w formie na sucho na papierze i przemyślenie zasady jego działania. Już na tym etapie możemy wychwycić pierwsze błędy. Dodatkowo jeśli uznamy że nie jest to dobre rozwiązanie będziemy mogli je szybko zmienić. Testując to od razu na projekcie tracimy dużo więcej czasu na wprowadzenie naszego systemu do istniejącej infrastruktury, często ją modyfikując a późniejsza zmiana może być jeszcze trudniejsza, lub też demotywująca. Pamiętajmy o tym że jeśli chcemy coś przetestować to warto działać na kopi aplikacji a nie na oryginalnym kodzie. Polecam tutaj prace na repozytoriach Git czy Svn.
- Nie pisz wszystkiego sam: W pisaniu gier ważnych jest wiele kwestii, których zaimplementowanie może zająć wiele czasu(dni, tygodni, miesięcy). Nie trać więc czasu na poruszanie tych wszystkich kwestii. Lepiej nauczyć się wzorców, języka i zasad ogólnych a potem wykorzystać gotowe rozwiązania innych osób i zaadaptować je na własne potrzeby. Przy dobrej bibliotece, szybciej napisze się własny adapter niż zaimplementuje chociaż część jej funkcjonalności. Także nie ma sensu pisać własnego silnika do gry lub obsługi sieci, jeśli zależy nam na utworzeniu wersji dystrybucyjnej gry a nie na udostępnianiu własnego silnika. Tak samo z kolizjami i fizyką, lepiej użyć gotowych rozwiązań jak np. Box2D.
Co prawda exp leci, ale exp naleciałby ci także jakbyś robił tylko to co lubisz i sprawia ci przyjemność. Choć nie powiem pewne tematy są niezbędne, ale lepiej je douczać się na bieżącą niż ryć w nich przez spory okres czasu bo to może po prostu zniechęcić.
- Czytelność kodu: dbaj o to by twój kod był czytelny. Zasada jest taka że 10 razy więcej będziesz czytał to co napisałeś niż w istocie będziesz pisał. Wiadomo przy starcie projektu wszystko można zrobić szybko, bo jesteśmy w stanie zapanować nad całą infrastrukturą. Jednak wraz ze wzrostem ilości klas, pakietów czy też funkcji w końcu dochodzimy do momentu że pisanie kodu to przeprawa przez bagno, czego skutkiem będzie niemal na pewno albo porzucenie projektu, albo jego gruntowna przebudowa. Żeby uniknąć tego zdarzenia, warto zawsze przeglądać swój kod, zwłaszcza ten zrobiony poprzedniego dni i popoprawiać w nim to co jest źle. Jest to dobra idea bo w pamięci masz to co robiłeś, a przy tym przez noc mogłeś ułożyć sobie lepsze spojrzenie na to co napisałeś.
Jest to praktycznie fundamentalna zasada tego że projekt może się utrzymać i być rozwijany. Nie tylko przez ciebie ale także inne osoby z zespołu.
Ogólnie jeśli chodzi o zasady dotyczące czytelności polecam książkę: Czysty kod. Podręcznik dobrego programisty. Wyd. Helion. Zwłaszcza pierwsze rozdziały powinny być lekturą obowiązkową.
Poniżej zamieszczę bardziej techniczne zagadnienia, związane z implementacją podstawowych rozwiązań. Proponuję także zapoznanie się z wzorcami projektowymi w celu pogłębienia idei tam zawartych.
- Podział modułów na (pod)moduły: czyli dalsza dekompozycja struktury gry.
O ile w drobnej produkcji być może udałoby się utworzyć nam projekt gry w podziale tylko na 3 moduły(pakiety) to jednak system gier jest na tyle skomplikowaną architektura że należy podzielić go na dalsze części:
- Widok: tutaj sprawa jest dosyć skomplikowana, bo w sporej mierze zależy od wykorzystywanej przez nas technologii. W Swingu będzie inaczej niż w LibGDX, albo jMoneky, czy konsoli. Tutaj powinniśmy dostosować się do wykorzystywanej przez nas biblioteki i platformy. Zwłaszcza że w tym wypadku będziemy jedynie korzystać z danych pobieranych z logiki a nie będziemy na nich nic wykonywać. Zalecam oczywiście podział na Stage na które mogą się składać różne sceny np. w libGDX. Czy też na różne JFramy. Złą praktyką jest stosowanie instrukcji if-else lub switchy do rysowania widoku, nawet w konsoli. Stage najlepieć jest zmieniać poprzez podmienianie widoku na odpowiedni Stage, lub tworzenie nowej instancji Stage(JFrame) czy podmiana innych komponentów.
- Kontroler: tutaj też bywa różnie. Ogólnie jest połączony z widokiem poprzez listenery, które nasłuchują kliknięcia w przycisk, dotknięcia palcem w ekran czy wciśnięci klawisza. Najważniejsze jest to że nie ważne jak i co będziesz prezentował użytkownikowi, kontroler ma zawsze wyrzucić obiekt zdarzenia zawierający wynik działania operacji użytkownika. To znaczy że, jeśli użytkownika kręci suwakiem to nie koniecznie po każdym milimetrze powinniśmy generować zdarzenie zmienionoWartośćSuwaka, tylko być może warto poczekać aż użytkownik puści przycisk myszy czy palec i wtedy je wysłać. Jest to istotne w szczególności kiedy użytkownik ma Widok i Kontroler na swoim komputerze a wysyła dane przez sieć do serwera(Architektura klient-serwer). Możemy tutaj rozróżnić pod moduły do obsługi różnych wejść(klawiatura, joystick, ekran, kontrolki widoku itp.).
- Logika: serce naszej aplikacji. Tutaj możemy wyróżnić 4 główne pod moduły:
Physic: moduł implementujący fizykę świata gry. Np. kolizje, grawitacje, prędkość obiektów, siłę hamowania, wielkość obrażeń zadanych obiektowi gry, np. statkowi w zależności od rodzaju pocisku.
Mechanics: zadaniem mechaniki będzie nadzorowanie obiektów gry a także ich tworzenie, przenoszenie. Zapewnić ma abstrakcje(pewien poziom magii), której nie oczekujemy od fizyki, a która musi być w grze. Np. Utworzenie obiektu pocisku i nadaniu mu prędkości i kierunku początkowego, czy teleportowanie statku w inne miejsce. Reprezentować ma takie stany, których nie można przedstawić za pomocą fizyki, a bynajmniej takie do której wymagany byłby bardzo zaawansowany system fizyczny. Tutaj także mieści się AI i Pathfinding.
World: jest to baza danych obiektów w grze. Obiekty tutaj się znajdujące to głównie struktury przechowujące informacje o obiektach w grze. np. Gracz(jako obiekt reprezentujący postać gracza w świecie gry), Statek, Plansza, Pole. Dodatkowo World to także system zbierający i przetwarzający informacje z świata gry(przetwarza ale nie modyfikuje ich).
Exception: moduł ten nie jest naturalnie powiązany z poprzednimi 3 modułami. Zadaniem tego modułu jest przechwytywanie wyjątków. Np. rozłączenie się gracza, czy opuszczenie przez niego rozgrywki. W zależności od sytuacji może on zdecydować o zakończeniu gry(istnienia pokoju), bądź wprowadzić pewne zmiany do jego świata np. zastąpić gracza ludzkiego, graczem komputerowym lub odwrotnie, pod gracza komputerowego wprowadzić gracza ludzkiego.
1. System zdarzeń.
Komunikację w swojej grze warto oprzeć na mechanizmie zdarzeń i listenerów. Należy unikać sztywnego podczepiania zależności, utrudnia to proces rozbudowy i modyfikacji.
3. Dekompozycja modułu Mechaniki(Logiki):
W przypadku większości projektu, logiki nie da się napisać w postaci jednego modułu. Warto wykonać wtedy podział na niezależne od siebie moduły wykonujące konkretne operację. Np. Obliczające fizykę, obsługujące kolizję, tworzące nowe obiekty, odczytujące dane wejściowe, AI. Im bardziej skomplikowana mechanika tym więcej takich głównych modułów powinniśmy wyciągać i tworzyć z nich osobne systemy. Zasadą jaką powinniśmy się tutaj kierować powinna być taka że moduł przeniesiony do innej sceny po dopięciu powinien sam sobie radzić bez potrzeby dorzucania ani jednego innego modułu.
4. Podział klas na obiekty i struktury.
Co prawda w Javie wszystko jest obiektem, więc tutaj lepszym przykładem byłby C#, gdzie mamy i klasy i struktury. Nie mniej podział ten możemy określić sami, także nie jest tutaj wymagana specjalna pomoc języka. Wystarczy pamiętać że w sytuacji kiedy chcemy utworzyć klasy reprezentujące obiekty na mapie, lub samą mapę najlepiej wykonać to w postaci struktury, czyli tak aby posiadała ona jak najwięcej zmiennych określających jej stan, a jak najmniej metod działających na nich(najlepszym rozwiązaniem byłoby posiadanie jedynie setterów i getterów). Natomiast klasy pomocnicze, służące np. do przenoszenia obiektów czy zliczania wrogów na mapie powinny udostępniać jak najmniej zmiennych a jak najwięcej funkcji. Złą praktyką jest ich mieszanie. tworzenie tak zwanych klas hybrydowych. Ich użyteczność wraz z rozrostem aplikacji będzie się pomniejszać. W końcu dojdziemy do sytuacji gdzie dodanie jakiej zmiennej albo metody będzie pociągało modyfikacje w wielu miejscach, czego powinniśmy unikać. Należy pisać tak kod, aby modyfikacji ulegały tylko najbliższe danej klasie klasy. Jest to zastosowanie zasady max 2 poziomy w dół(i górę).
5. Tworzenie adapterów.
Jak już pisałem warto abyśmy korzystali z gotowych rozwiązań, tak by nie trzeba było koła wymyślać od nowa. Jednak w wielu wypadkach każda biblioteka ma specyficzne zachowanie i swoją strukturę. Nie zawsze może nam ona odpowiadać a czasami możne się nawet kłócić z innymi bibliotekami jakie używamy. W takim wypadku pisanie kodu szybko się przerodzi w niezrozumiałą plątaninę micro-adaptacyjnych metod co w końcu doprowadzi do chaosu. Czego musimy się usilnie wystrzegać jeśli chcemy doprowadzić projekt do końca. Warto dlatego już za wczasu przygotować klasy osłonowe które przygotują daną bibliotekę do użytku w naszym kodzie bez potrzeby dostrajania się do jej funkcjonowania. Oczywiście inną zaletą będzie to że w każdej chwili będziemy mogli podpiąć pod używaną dotychczas bibliotekę inną, a sama logika aplikacji nie będzie wymagała jakiś szczególnych zmian.
6. Fabryka prefabów
Tworzenie obiektów w grze powinniśmy oddelegować do konkretnej klasy. Dzięki temu tworzymy obiekt budowniczego, którego możemy łatwo podmieniać stosując interfejs. Dodatkowo mamy możliwość zrobienia szybkiego loadera scen z plików.