System aktorów gry, cz. 1

Każda gra ma jakichś aktorów – może to być postać gracza, pocisk, kamera, beczka, potwór albo każdy obiekt, który odgrywa jakaś rolę. Gdy piszemy coś prostego – kolejny klon snake’a czy tetrisa – możemy założyć że aktorów będzie na tyle mało, że nie potrzebujemy żadnej wyższej abstrakcji do zarządzania nimi. Jednak (nie)stety w życiu każdego wytrwałego kodera gier przychodzi czas na „coś grubszego”, wtedy w miarę sprawne zarządzanie aktorami może powodować bóle głowy. Kończymy teraz pisać Kapturka, który jest projektem zmiennym niczym pogoda – spróbuję opisać w jaki sposób poradziliśmy sobie z jego złożonością. Piszę od razu, żeby nie było niedomówień – to nie jest duża gra (nawet nie jest średnia), jednak czasami sprawiała nam problemy.

Zacznę od klasy Entity (jednostka, aktor, obiekt, nazywaj jak tam chcesz). Od razu widać, że obiekt tej klasy przeważnie ma pozycję i przeważnie może się rysować, a gdy się rysuje to przeważnie ma jakąś bitmapową animację. Pozycja to 2 floaty (x i y, bo robimy w 2D), a animacja to co najmniej 1 float i jeden pusty wektor na klatki, czyli podejrzewam że nie więcej niż 16 bajtów. Zaokrąglimy teraz ostro w górę – powiedzmy niech każdy goły aktor ma 64 bajty. Zauważ że jednostek wcale nie będzie aż tak dużo – pewnie nie więcej niż 20k (nie możemy przecież animować ich zbyt wiele, bo liczba klatek spadłaby gwałtownie na łeb, na szyję). Proste mnożenie 64B*20000=1280000B czyli coś około 1,2MB (poprawcie mnie jeśli się pomyliłem – nie umiem mnożyć). Niewiele za wygodę wpakowania wszystkich typów jednostek w jedną klasę.

Zaraz, zaraz, ale po co w ogóle wszystko obsługiwać jedną klasą? Ma to bardzo poważną zaletę – omijamy dziedziczenie, które przeważnie przy wielu klasach jest strzałem w stopę – bardzo trudno użyć go tak, żeby jednocześnie nie kopiować kodu pomiędzy różnymi klasami, nie obciążać obiektów zbędnymi danymi czy kodem oraz nie robić z kodu spaghetti. Funkcjonalność obiektom jednak i tak trzeba dodać, z pomocą przychodzą komponenty.

Komponenty to nic innego jak obiekty z których składać się będzie obiekt. Na przykład komponentem może być Animacja, Ciało kolizyjne, Ikona lub bardziej wyszukane rzeczy jak na przykład System Particli Ognia (wtedy możemy podpalać aktorów :D) czy Zabijacz Gracza, który po zetknięciu z graczem uruchamia mu funkcję kill. U nas bazowy komponent składał się z 3 rzeczy:

  1. Referencji do aktora, którego jest częścią.
  2. Funkcji step/update/work/animate która zmieniała stan aktora (np jego pozycję, animację, dane komponentu).
  3. Funkcji draw/render która wyświetlała dodatkowe sprajty powiązane z komponentem.

Aktor ma tablicę/wektor wskaźników na swoje komponenty, które animuje, rysuje i kasuje przy własnej śmierci. Koncept jest bardzo prosty, ale dzięki niemu możemy tworzyć obiekty w locie. Potrzebujemy obiektu który emituje gwiazdki, śpiewa hymn Ukrainy i lata w kółko – nic prostszego, wystarczy zakodować i zmontować odpowiednie komponenty. Niestety nie jest to rozwiązanie w pełni uniwersalne, np gdy chcemy by postać biegała i skakała równocześnie, to nie da się jej złożyć z komponentów Biegacz i Skoczek, ponieważ obaj modyfikują pozycję i animację i nie wiedzą o sobie (przynajmniej – nie powinny). W tym wypadku trzeba zrobić BiegoSkoczka – nie ma innej rady, dziedziczenie też by przecież nie pomogło.

Teraz troszkę kodu, same nagłówki które dadzą pogląd na całą sprawę z trochę większej odległości:

class Entity
{
public:
    pos2 position;
    Animation animation;
    std::vector<Component*> components;

    void step(double delta)
    {
        animation.step(delta);

        for(int i=0; i<components.size(); i++)
            components[i]->step(delta);
    }

    void draw()
    {
        animation.draw();

        for(int i=0; i<components.size(); i++)
            components[i]->draw();
    }

    ~Entity()
    {
        for(int i=0; i<components.size(); i++)
            delete [] components[i];
    }
};

class Component
{
public:
    Entity &owner;

    Component(Entity &_owner):owner(_owner)
    {
        owner.components.push_back(this);
    }

    virtual void step(double delta) {}
    virtual void draw() {}
};

Nagłówki te pisałem „na kolanie”, ale chyba są poprawne. Celowo oszczędziłem sobie gettery i settery, żeby nie zaciemniać kodu.

To tyle na dzisiaj, w następnej notce opiszę jak zrobić do tego fajny manager, dzięki któremu będziemy mogli po prostu złożyć obiekty i o nich zapomnieć. Polecam samemu wpisać w google „Game Object Component” i poszukać więcej info o tej ciekawej technice. Póki co – do następnego wpisu, który mam nadzieję pojawi się niebawem – szlifuję już pióro.


Mniej = więcej

Już szósty miesiąc robimy Kapturka. No dobra, w sumie to piąty – przez pierwsze 30 dni nie zakodowaliśmy nawet wyświetlenia okienka bo czekaliśmy aż designerzy w końcu spiszą jak to widzą. Czy był to czas stracony? Teraz myślę że mogliśmy spokojnie poczekać jeszcze z 2 miesiące, zająć się pożyteczniejszymi rzeczami bo prawie wszystko z pierwotnych założeń poleciało do kosza. Większość kodu została usunięta, część ostro zmieniona i teraz kilka rozwiązań wygląda głupio, ale nie chcemy tego pisać od nowa. Napiszę tak – kodu powstało tyle, że jeszcze dodatkowo 2 gry można było z tego zrobić…

Ale nie tylko chodzi o ilość skasowanego kodu. Gorsze były ograniczenia i powiązania – nigdy nie ma tak, że ficzery dodaje się bez żadnych kosztów – zawsze w jakimś miejscu może się okazać że trzeba użyć „brzydkiej sztuczki” by coś zrobić, a jak tych sztuczek jest wiele (bo innych rozwiązań nie ma) to robi się nieciekawie… Nie muszę pisać, że im więcej kodu tym się go gorzej konserwuje i zabiera to coraz więcej czasu.

Przyczyny? Dla designerów to był pierwszy projekt, nawet teraz nie wiedzą jak ma to dokładnie wyglądać (!). Komunikacja też często u nas leżała i kwiczała, ale na szczęście co 2 tygodnie od początku wypuszczaliśmy małe demka obrazujące co mamy już zrobione co pozwoliło wychwycić dużo nieścisłości we wczesnych fazach projektu. Leczenie? Niestety nie ma – zmiany teamu nie pomoże, bo w Polsce mało jest osób które się tym potrafią  zajmować. Lepiej zostać przy ludziach którzy się powoli uczą, niż zmieniać zespoły jak rękawiczki – dopiero po jakimś czasie będzie widać czy nic z nich nie będzie, czy może potrzebują czasu.

Moja rada jest taka: nie zaczynać pisać zbyt szybko, lepiej na początku zrobić sobie wolne. Kod powinien się robić coraz prostszy, a to nie jest możliwe przy ciągle zmieniających się wymaganiach (nie mamy nieograniczonego czasu). Następny projekt zaczynam od 3 miesięcznych wakacji…


ctrl+z

Edytor bez operacji cofania jest baaardzo irytujący. Normalne jest to, że czasami klikniemy myszą w złym miejscu – jesteśmy tylko ludźmi. Na szczęście wymyślono mechanizm do radzenia sobie z takim problemem – cofanie. Spróbuję pokazać że zakodowanie takiego ficzera we własnej aplikacji jest dosyć proste.

W naszym edytorze mamy bazę danych na której możemy wykonywać jakieś akcje. Każdy obiektowo-myślący programista zauważy, że mamy 2 główne klasy reprezentujące pogrubione słowa. Baza danych to u mnie obecny stan edytora (rozmieszczenie obiektów, parametry tych obiektów itp.), akcja zaś czynność która może zmienić stan bazy danych.

Klasa akcji może wyglądać tak:

class Action {
    protected:
        DataBase &db;
    public:
        Action(DataBase &_db): db(_db) {}
        virtual void undo() = 0;    // confij akcję
        virtual void redo() = 0;    // powtórz akcję
};

Teraz potrzebujemy stosu takich akcji (można użyć np. tego z STLa) na który będziemy odkładali wykonane polecenia.

std::stack<Action*> actionsStack;

Dlaczego trzymamy wskaźniki? Bo użyjemy tu polimorfizmu i stworzymy np. taką klasę:

class ActionAdd: public Action {
    private:
        Object obj;
    public:
    ActionAdd(DataBase &_db, Object &_obj) : Action(_db), obj(_obj) {}
    void undo() {
        db.objects.erase(obj);
    }
    void redo() {
        db.objects.insert(obj);
    }
}

Wykonywanie akcji wygląda tak:

actionsStack.push(new ActionAdd(db, obj));
actionsStack.top()->redo();

A cofanie:

if(!actionsStack.empty()) {
    actionsStack.top()->undo();
    delete actionsStack.top();
    actionsStack.pop();
}

Powtarzanie (ctrl+y) działa podobnie.

Proste, nie?


Nie szata zdobi…

Ktoś kiedyś powiedział że nie szata zdobi człowieka. Porównując powyższy i poprzedni screen z Kapturka widać że to przysłowie nie odnosi się do gier. A na pewno nie w obecnych czasach.  Tak naprawdę nikt nie spojrzy nawet na grę która wygląda jak kilkuminutowa zabawa dziecka w Paintcie. Dzisiaj zamieszczam screen w prawdzie z gry bez nowych ficzerów, ale za to z nową grafiką żeby kapturkowi fani (aż 5 osób, bo teraz tyle developerów liczy team) mogli nasycić wzrok naszym minimalistycznym stylem.

Podoba się?


To się rusza!

Już prawie 4 miesiące developujemy kod Kapturka a według planu będziemy go jeszcze robić troszkę ponad 2 miesiące. Jak wygląda stan na dzień dzisiejszy? Cóż, projekt ciągle się zmienia, nie mieliśmy na początku spójnej wizji jak ma to wyglądać, ludzie przychodzą i odchodzą, a nasz koordynator (jak większość zespołu) w gamedevie dopiero raczkuje – wszystkie te czynniki skutecznie opóźniają projekt. Stale walczymy z tymi przeciwnościami, dlatego już coś się rusza:

  1. Można chodzić i skakać.
  2. W grze są takie obiekty jak:
    1. platformy (ruchome/statyczne)
    2. drabiny
    3. sloty na przedmioty (i same przedmioty oczywiście)
    4. dźwignie
    5. przeciwnicy (3 rodzaje)
    6. obiekt sterowany przez skrypt Lua
  3. Mamy edytor cząsteczek (+ system cząsteczek do niego).
  4. Własny, szybki moduł do renderowania (działamy na SFML który nie grzeszy wydajnością).
  5. Działają ruchome warstwy tekstur.
  6. Mamy moduł do lokalizacji.
  7. Szkielet edytora też się powoli robi.
  8. Rdzeń systemu zagadek zaimplementowany.

Ogólnie projekt idzie w dobrą stronę i jest (mała) szansa że się wyrobimy do wakacji z kodem. Trochę nam jeszcze brakuje:

  1. Edytor – niestety nie pisaliśmy jeszcze czegoś takiego (a przynajmniej nie o takich gabarytach) a wydaje się to nawet ważniejszą częścią projektu niż sam kod gry.
  2. System muzyki/dźwięku – co tu dużo pisać, nawet nie myśleliśmy jeszcze o tym jak ma to wyglądać.
  3. Skrypty – mniej więcej wiemy jak to zrobić, ale jeszcze nie zakodziliśmy potrzebnych funkcjonalności.
  4. Efekty – jakiś postprocess trzeba na pewno dodać w niektórych fragmentach.

i podejrzewam że jeszcze dużo, dużo więcej… Od jutra zabieram się do roboty (mieliśmy tydzień przerwy świątecznej).

Trochę wstyd zamieszczać taki screen, ale uznałem że „lepszy rydz niż nic”. Dlatego to tak wygląda bo nie mamy osoby która ogarnęłaby grafikę – stale się nam graficy zmieniają. Wszystko to oczywiście prowizorka.


Grunt to czytelność

Zagadka – co robi ten kod z obiektem shape:

shape.Rectangle(0, 0, 100, 100, sf::Color(255,255,255));

Poprawna odpowiedź: nic. Kilkanaście tygodni temu straciłem dobrą godzinę zastanawiając się „czemu nic się nie wyświetla”. Na pierwszy rzut oka wygląda tak, jakby funkcja Rectangle robiła z obiektu shape biały prostokąt o wymiarach 100×100. Niestety, to byłoby przecież zbyt proste – kod (jak sama nazwa wskazuje) powinien być maksymalnie nieczytelny. Co zamiast tego robi funkcja Rectangle? Dokumentacja na to:

Create a shape made of a single rectangle.

Tak naprawdę nic to nie rozjaśniło. Burza mózgów, wyrywanie włosów, przeklinanie kompilatora aż tu nagle coś mnie natchnęło żeby zobaczyć deklarację tej funkcji:


static Shape Rectangle (float P1X, float P1Y, float P2X, float P2Y, ...)

Dlaczego niby static?! Bo ta funkcja zamiast ustawiać obiekt na rzecz którego jest wywoływana zwraca tak ustawiony obiekt. Powinno się jej używać w ten sposób:

shape = shape.Rectangle(0, 0, 100, 100, sf::Color(255,255,255));

Co jest winne straconego czasu w tej sytuacji? Interfejs biblioteki. Można było temu zaradzić przynajmniej dwoma sposobami:

  • robiąc funkcje globalną CreateRectangle
  • robiąc funkcję składową w klasie Shape i nazwać ją np. SetAsRectangle

Jaki z tego morał? Nazwy funkcji mają ogromne znaczenie. Chyba że ktoś lubi debugger.

PS: Jeszcze powiedziałbym coś o tworzeniu niepotrzebnego obiektu tymczasowego przez funkcję Rectangle, ale już odpuszczę to tej biednej klasie…


Prawie Wolfram

Prawie :). Na laboratoria z programowania w języku C++ robimy projekty zaliczeniowe, pierwszym był prosty kalkulator mogący liczyć wartość wyrażenia typu 3^3*2/(5-11)!. Można było zrobić ten projekt kilkoma sposobami: albo użyć narzędzi yacc/bison albo zakodować wszystko ręcznie (tu prowadzący zasugerował użycie ONP) albo jakiś miks poprzednich dwóch sposobów. Wybrałem opcję „wszystko robię sam” (bo nie chciało mi się czytać manuala yacc’a i bison’a), ale za to z możliwością liczenia całek. W kalkulatorze działają operatory +,-,*,/,!,^ oraz sporadycznie unarny minus, są też jakieś podstawowe funkcje typu sin, ln oraz stałe PI i E. Screena zamieszczam poniżej:

Takie nic a cieszy :). Źródła + binarki tutaj. Starałem się to dość dobrze skomentować w nadziei że prowadzący zerknie w kod. Nie spojrzał, 2h na marne…