Architektura kodu w grze platformowej. Opisuję najważniejsze podstawy [OZDGD #4]

Powiedzenie, aby nie oceniać książki po okładce, ma szczególne zastosowanie przy tworzeniu gier. Nigdy bowiem nie wiemy, co siedzi pod maską danej produkcji i choć jako graczy niespecjalnie nas to obchodzi, to potencjalni pracodawcy, z całą pewnością zajrzą nam do kodu przy pierwszej lepszej okazji. Dlatego też architektura kodu w grach w Unity musi być dobrze przemyślana, a poszczególne klasy i metody rozwinięte tak, żeby stać się naszą wizytówką.
Architektura kodu w grze platformowej. Opisuję najważniejsze podstawy [OZDGD #4]

Gra platformowa 2D w Unity Od Zera do Game-Developera [OZDGD]

W ramach serii Od Zera do Game-Developera, w której to chcę zachęcić innych do zainteresowania się nie tylko grami, ale też sferą ich produkcji, powstały już następujące teksty:

Niniejszy tekst jest kontynuacją serii, w ramach to której tworzę pełnoprawną, ale prostą platformową grę 2D:

Architektura kodu platformowej gry 2D w Unity

Proces tworzenia gry jest dynamiczny, więc wiele może zmienić się w toku pracy, ale dobrze nakreślić sobie na początku pierwotne założenia projektu, aby nie utknąć na danym etapie i zawsze wiedzieć, co powinniśmy robić w następnej kolejności. Jak więc do tego podejść? Niestety w toku tej serii nie uda mi się zrobić z nikogo początkującego programisty w C#, którym aktualnie sam bym się nazwał, ale zapewne zdołam podrzucić wam inspirację co do tego, jak podzielić swoje skrypty tak, żeby miały sens i przedstawiały sobą pewien poziom zaawansowania. 

Zacznijmy więc od tego, aby określić, co znajdzie się w naszej grze i tym samym, co powinniśmy potraktować obiektowo, czyli tak, jak C# przykazał. Tak więc, pewne jest, że w naszej grze znajdzie się zarówno główny bohater sterowany przez gracza, jak i przeciwnicy, którzy będą temu graczowi rzucać wyzwanie. Co te obiekty będą miały ze sobą wspólnego? Sporo, dlatego można uznać je za ogólnie pojęte postaci. Mamy więc pewność, że w grze znajdzie się Player, Enemy i tym samym Character, co oznacza, że możemy wyjść z założeniem stworzenia trzech interfejsów, czyli swoistych “przepisów” na dane klasy: IPlayer, IEnemy oraz ICharacter, z czego ten trzeci interfejs będzie gromadził funkcje wspólne zarówno dla postaci gracza, jak i przeciwników.

Najważniejsze mamy już za sobą, więc czas wziąć się za elementy, które będą “dodatkami”, bez których gra się nie uda. Mowa przykładowo o klasie elementów w świecie gry, które będzie można zebrać, czyli Collectibles, a jako że gracz będzie mógł (potencjalnie) zbierać nie tylko jeden rodzaj znajdziek, warto postarać się o stworzenie interfejsu (ICollectibles), który to posłuży nam do wskazywania w kodzie, że gracz może zbierać różne znajdźki (inne klasy) z dostępem do interfejsu ICollectibles. Więcej o tym podejściu możecie znaleźć tutaj:

Na znajdźkach jednak się nie skończy, bo ważnym elementem gry będzie też udźwiękowienie (Audio), sposób wyboru różnych scen (SceneManager) jak i forma oddziaływania poszczególnych elementów na siebie, co możemy umieścić w klasie odpowiadającej za interakcje między graczem, a przeciwnikami oraz zarządzaniem tokiem rozwoju ogólnej rozgrywki (GameManager). Ostatnim elementem będzie ogólnie pojęte UI, czyli interfejs gracza, który to obejmie implementację głównego menu (MainMenu), menu w grze (InGameMenu) służącego również jako ekran zwycięstwa lub porażki oraz interfejsu bezpośrednio w grze, który to będzie wskazywał, jak wiele przeciwników musimy pokonać na danym poziomie i jak wiele nutek musimy jeszcze zebrać, żeby ten poziom zakończyć. Nie możemy też zapominać o sekcji wyświetlającej aktualny poziom życia gracza, co finalnie zapewnia nam taką oto architekturę:

Jeśli z kolei idzie o sposób nawiązywania połączenia między klasami, to w tak małym projekcie najlepiej będzie wykorzystać prosty w użyciu wzorzec projektowy Singleton dla najważniejszych i występujących pojedynczo w grze klas (np. Player, Sound Manager). W nim klasa ma stworzoną tylko jedną ogólnie dostępną instancję w programie, która zapewnia do niej prosty dostęp. Czy to najlepszy sposób? Zapewne nie, ale czy idealny dla tego typu projektu? Moim zdaniem tak. 

Powyżej możecie obejrzeć, jak w toku produkcji podejdę do dzielenia poszczególnych skryptów. Najwięcej uwagi poświęcę z oczywistych względów postaci gracza, która to doczeka się łącznie trzech klas – menadżera, kontrolera oraz zbiorku danych w postaci SO (Scriptable Objectu). Podczas gdy pierwsza klasa doczeka się instancji Singletona i będzie komunikować się z pozostałymi Managerami, kontroler będzie odpowiadał za operacje związane wyłącznie z postacią głównego bohatera w środowisku gry, a dane do sterowania nią będzie pobierał z pliku SO, w którym to znajdą się wszystkie potrzebne parametry. 

Z racji planu implementacji kilku przeciwników, ci będą tworzeni z wykorzystaniem dziedziczenia, co ułatwi mi zarówno implementację logiki dla gracza, jak i interfejsu użytkownika (UI – User Interface). Czy tak właśnie wszystko uda się stworzyć i czy będę trzymał się tej rozpiski za wszelką cenę? Zdecydowanie nie. W ostatnich latach zrozumiałem, że choć plan jest podstawą, to na tym etapie nie można przewidzieć wszystkiego, co trzeba będzie wprowadzić do gry. Innymi słowy, finalny kształt rozpiski architektury kodu może się znacząco różnić, ale taki jest to już urok programowania – w toku pisania możemy dostrzec schematy i mechanizmy, które będziemy mogli lepiej rozpisać, zmieniając nawet podstawę, o którą się opieraliśmy. Przykładowo już teraz mam wątpliwości, czy aby na pewno interfejsy będą konieczne w tym projekcie w takiej oto formie, ale poważne decyzje zapadną dopiero w momencie pisania kodu, czyli w następnym artykule z tej serii Od Zera do Game-Developera.