Writing a Game Engine from Scratch – Part 1: Messaging

author
20 minutes, 5 seconds Read

Następujący wpis na blogu, o ile nie zaznaczono inaczej, został napisany przez członka społeczności Gamasutras.
Wyrażone myśli i opinie należą do piszącego, a nie do Gamasutry lub jej firmy macierzystej.

Część 1 – Wiadomości
Część 2 – Pamięć
Część 3 – Pamięć podręczna danych
Część 4 – Biblioteki graficzne

Żyjemy we wspaniałych czasach dla deweloperów. Z tak ogromną ilością świetnych silników klasy AAA dostępnych dla każdego, tworzenie prostych gier może być tak proste jak przeciągnij i upuść. Wydaje się, że w dzisiejszych czasach nie ma już żadnego powodu, by pisać silnik. A z powszechnym sentymentem, „Pisz gry, nie silniki”, dlaczego miałbyś to robić?

Ten artykuł jest przede wszystkim skierowany do samotnych programistów i małych zespołów. Zakładam pewne obycie z programowaniem obiektowym.

Chcę dać ci pewien wgląd w podejście do rozwoju silnika i użyję prostego fikcyjnego silnika aby to zilustrować.

Po co pisać silnik?

Krótka odpowiedź brzmi: Nie rób tego, jeśli możesz tego uniknąć.

Życie jest zbyt krótkie by pisać silnik dla każdej gry (zaczerpnięte z książki 3D Graphics Programming by Sergei Savchenko)

Obecny wybór doskonałych silników takich jak Unity, Unreal czy CryEngine jest tak elastyczny jak tylko można mieć nadzieję i może być użyty do stworzenia prawie każdej gry. Dla bardziej specjalistycznych zadań, istnieją oczywiście bardziej wyspecjalizowane rozwiązania, takie jak Adventure Game Studio lub RPG Maker, by wymienić tylko kilka. Nawet koszt silników klasy komercyjnej nie jest już argumentem.

Zostało już tylko kilka niszowych powodów do napisania własnego silnika:

  • Chcesz się nauczyć jak działa silnik
  • Potrzebujesz pewnych funkcjonalności, które nie są dostępne lub dostępne rozwiązania są niestabilne
  • Wierzysz, że możesz to zrobić lepiej / szybciej
  • Chcesz zachować kontrolę nad rozwojem

Wszystkie te powody są całkowicie uzasadnione i jeśli to czytasz, prawdopodobnie należysz do jednego z tych obozów. Moim celem nie jest zagłębianie się w długą debatę „Którego silnika powinienem użyć?” lub „Czy powinienem napisać silnik?” i wskoczę w nią od razu. Więc, zaczynajmy.

Jak nie napisać silnika

Czekaj. Najpierw mówię ci, żebyś go nie pisał, a potem wyjaśniam, jak go zawieść? Świetny wstęp…

Anyways, there are alot of things to concider before even writing a single line of code. Pierwszy i największy problem każdego kto zaczyna pisać silnik gry może być sprowadzony do tego:

Chcę zobaczyć trochę rozgrywki tak szybko jak to możliwe!

Jednakże im szybciej zdasz sobie sprawę, że minie dużo czasu zanim zobaczysz coś interesującego, tym lepiej będzie ci się pisało silnik.

Zmuszanie twojego kodu do pokazania jakiejś formy grafiki lub rozgrywki tak szybko, jak tylko możesz, tylko po to, aby mieć jakieś wizualne potwierdzenie „postępu”, jest twoim największym wrogiem w tym momencie. Weź. Swój. Time!

Nawet nie myśl o zaczynaniu od grafiki. Prawdopodobnie przeczytałeś już wiele poradników i książek na temat OpenGL / DirectX i wiesz jak wyrenderować prosty trójkąt lub sprite. Możesz pomyśleć, że krótki wycinek kodu Rendering małej siatki na ekranie jest dobrym miejscem do rozpoczęcia. To nie jest.

Tak, twój początkowy postęp będzie niesamowity. Heck, możesz być uruchomiony wokół małego poziomu w First-Person w ciągu zaledwie jednego dnia kopiując-wklejając fragmenty kodu z różnych tutoriali i Stack Overflow. Ale gwarantuję Ci, że 2 dni później skasujesz każdą linijkę tego kodu. Co gorsza, możesz nawet zniechęcić się do pisania Silnika, ponieważ nie jest to motywujące widzieć coraz mniej.

Drugim dużym problemem, z którym borykają się programiści podczas pisania Silników jest feature creep. Każdy chciałby napisać świętego Graala silników. Każdy chce tego idealnego silnika, który może zrobić wszystko. Strzelanki z perspektywy pierwszej osoby, taktyczne gry RPG, jakkolwiek to nazwać. Ale faktem jest, że nie potrafimy. Jeszcze. Wystarczy spojrzeć na wielkie nazwiska. Nawet Unity nie jest w stanie idealnie zaspokoić każdego gatunku gier.

Nawet nie myśl o pisaniu silnika, który może zrobić więcej niż jeden gatunek za pierwszym razem. Nie rób tego!

Gdzie właściwie zacząć pisanie silnika

Pisanie silnika jest jak konstruowanie prawdziwego silnika do samochodu. Kroki są dość oczywiste, zakładając, że wiesz nad jaką grą (lub samochodem) pracujesz. Oto one:

  1. Podkreśl dokładnie, do czego twój silnik musi być zdolny ORAZ do czego twój silnik nie musi być zdolny.
  2. Zorganizuj potrzeby w Systemy, których twój silnik będzie wymagał.
  3. Zaprojektuj swoją idealną Architekturę, która powiąże wszystkie te Systemy razem.
  4. Powtarzaj kroki 1. – 3. tak często jak to możliwe.
  5. Koduj.

Jeśli (= jeśli i tylko jeśli) poświęcisz wystarczająco dużo czasu i wysiłku na kroki 1. – 4. i projekt gry nie zmieni się nagle z horroru na automat do gier (czytaj: Silent Hill), kodowanie będzie bardzo przyjemnym przedsięwzięciem. Kodowanie nadal będzie dalekie od łatwego, ale całkowicie do opanowania, nawet przez samotnych programistów.

To jest powód, dla którego ten artykuł jest głównie o krokach 1. – 4. Pomyśl o kroku 5. jako o „Wypełnianiu pustych miejsc. 50.000 LOC of Blanks”.

Najbardziej kluczową częścią tego wszystkiego jest Krok 3. Tutaj skoncentrujemy większość naszych wysiłków!

Krok 1. Określ Potrzeby i Nie Potrzeby

Wszystkie te kroki mogą wydawać się dość trywialne na początku. Ale tak naprawdę nie są. Możesz pomyśleć, że Krok 1 procesu tworzenia silnika do gry First Person Shooter może być sprowadzony do tego:

Muszę załadować poziom, broń graczy, kilku wrogów z AI. Gotowe, przechodzimy do kroku 2.

Gdyby tylko to było takie proste. Najlepszym sposobem na przejście do Kroku 1 jest przejście przez całą grę Click-by-Click, Action-by-Action od kliknięcia ikony na pulpicie, do naciśnięcia klawisza Exit po zwinięciu Credits. Zrób listę, dużą listę tego, czego potrzebujesz. Zrób listę tego, czego na pewno nie potrzebujesz.

To prawdopodobnie będzie wyglądało tak:

Uruchamiam grę, a ona przechodzi bezpośrednio do Menu Głównego. Czy w menu będzie użyty obraz statyczny? Scena cięta? Jak będę sterował Menu Głównym, Mysz? Klawiatura? Jakiego rodzaju elementów GUI potrzebuję dla Menu Głównego? Przyciski, formularze, paski przewijania? Co z muzyką?

A to są tylko makro-rozważania. Wejdź tak szczegółowo, jak to tylko możliwe. Decyzja, że potrzebujesz przycisków jest dobra i słuszna, ale zastanów się również, co dany przycisk może zrobić.

Chcę, aby przyciski miały 4 stany, Up, Hover, Down, Disabled. Czy będę potrzebował dźwięku dla przycisków? Co z efektami specjalnymi? Czy będą animowane w stanie bezczynności?

Jeśli twoja Lista Potrzeb i Nie Potrzeb zawiera tylko około 10 pozycji do końca Menu Głównego, zrobiłeś coś źle.

Na tym etapie, to co robisz to symulacja silnika w twoim mózgu i spisanie tego co musi być zrobione. Krok 1 stanie się jaśniejszy z każdą iteracją, nie martw się, że coś przeoczyłeś za pierwszym razem.

Krok 2. Zorganizuj potrzeby w Systemy

Więc, masz swoje listy rzeczy, których potrzebujesz i nie potrzebujesz. Nadszedł czas, aby je uporządkować. Oczywiście rzeczy związane z GUI, takie jak przyciski, trafią do jakiegoś systemu GUI. Rzeczy związane z renderowaniem trafią do Systemu Grafiki / Silnika.

Ponownie, tak jak w przypadku Kroku 1, decyzja o tym co gdzie trafi będzie bardziej oczywista w drugiej iteracji, po Kroku 3. Dla pierwszego przejścia, pogrupuj je logicznie jak w powyższym przykładzie.

Najlepszym źródłem informacji na temat „co gdzie idzie” i „co robi co” jest bez wątpienia książka Game Engine Architecture autorstwa Jasona Gregory.

Zacznij grupować funkcjonalności. Zacznij myśleć o sposobach ich łączenia. Nie potrzebujesz Camera->rotateYaw(float yaw) i Camera->rotatePitch(float pitch), jeśli możesz je połączyć w Camera->rotate(float yaw, float pitch). Utrzymuj to w prostocie. Zbyt wiele funkcjonalności (pamiętaj, feature creep) zaszkodzi ci później.

Pomyśl o tym, która funkcjonalność musi być publicznie widoczna, a która musi rezydować tylko wewnątrz samego Systemu. Na przykład, twój Renderer musi posortować wszystkie przezroczyste sprite’y przed rysowaniem. Funkcja do sortowania tych sprite’ów nie musi być jednak ujawniona. Wiesz, że musisz posortować przezroczyste sprite’y przed rysowaniem, nie potrzebujesz jakiegoś zewnętrznego systemu, który by ci to powiedział.

Krok 3. Architektura (Lub, rzeczywisty artykuł)

Możemy równie dobrze zacząć artykuł tutaj. To jest interesująca i ważna część.

Jedną z najprostszych możliwych architektur twojego silnika jest umieszczenie każdego systemu w klasie i posiadanie Głównej Pętli Gry wywołującej ich podprogramy. Może to wyglądać mniej więcej tak:

while(isRunning)
{
Input->readInput();
isRunning = GameLogic->doLogic();
Camera->update();
World->update();
GUI->update();
AI->update();
Audio->play();
Render->draw();
}

Na początku wydaje się to całkiem rozsądne. Masz wszystkie swoje podstawy, wejście -> przetwarzanie wejścia -> wyjście.

I rzeczywiście, to wystarczy dla prostej gry. Ale będzie to bolesne w utrzymaniu. Powód tego powinien być oczywisty: Zależności.

Każdy System musi w jakiś sposób komunikować się z innymi Systemami. Nie mamy żadnego sposobu, aby to zrobić w naszej powyższej pętli gry. Dlatego przykład jasno wskazuje, że każdy System musi mieć jakieś odniesienie do innych Systemów, aby zrobić cokolwiek sensownego. Nasze GUI i Game Logic muszą wiedzieć coś o naszym wejściu. Nasz Renderer musi wiedzieć coś o naszej Logice Gry, aby wyświetlić cokolwiek sensownego.

Prowadzi to do tego architektonicznego cudu:

Jeśli to pachnie jak Spaghetti, to jest to Spaghetti. Zdecydowanie nie o to nam chodzi. Tak, to jest łatwe i szybkie do zakodowania. Tak, będziemy mieli akceptowalne wyniki. Ale nie jest to możliwe do utrzymania. Zmień mały kawałek kodu gdzieś i może to mieć niszczycielski wpływ na wszystkie inne systemy bez naszej wiedzy.

Ponadto, zawsze będzie istniał kod, do którego wiele systemów potrzebuje dostępu. Zarówno GUI jak i Renderer muszą wykonywać wywołania Draw lub przynajmniej mieć dostęp do jakiegoś Interfejsu, który to dla nas obsłuży. Tak, moglibyśmy po prostu dać każdemu Systemowi uprawnienia do wywoływania funkcji OpenGL / DirectX bezpośrednio, ale skończymy z mnóstwem nadmiarowości.

Moglibyśmy to rozwiązać poprzez zebranie wszystkich funkcji rysowania wewnątrz Systemu Renderingu i wywoływanie ich z systemu GUI. Ale wtedy Rendering System będzie miał specyficzne funkcje dla GUI. Te nie mają miejsca w Rendererze i dlatego są sprzeczne z Krokiem 1 i 2. Decyzje, decyzje.

Więc pierwszą rzeczą jaką powinniśmy rozważyć jest podzielenie naszego silnika na warstwy.

Engine Lasagne

Lasagna jest lepsza niż Spaghetti. Przynajmniej pod względem programistycznym. Trzymając się naszego przykładu Renderera, to czego chcemy, to wywoływanie funkcji OpenGL / DirectX bez wywoływania ich bezpośrednio w systemie. To pachnie jak Wrapper. I w dużej części tak jest. Zbieramy całą funkcjonalność rysowania wewnątrz innej klasy. Klasy te są nawet bardziej podstawowe niż nasze Systemy. Nazwijmy te nowe klasy Framework.

Pomysł za tym jest taki, aby wyabstrahować wiele niskopoziomowych wywołań API i uformować je w coś dostosowanego do naszej gry. Nie chcemy ustawiać Vertex Buffer, ustawiać Index Buffer, ustawiać tekstur, włączać tego, wyłączać tamtego tylko po to, aby wykonać proste wywołanie rysowania w naszym Renderer System. Umieśćmy wszystkie te niskopoziomowe rzeczy w naszym Frameworku. Tę część frameworka nazwę po prostu „Draw”. Dlaczego? Cóż, wszystko co robi, to przygotowuje wszystko do rysowania, a następnie rysuje. Nie dba o to, co, gdzie i dlaczego rysuje. To jest pozostawione Rendererowi System.

To może wydawać się dziwne, chcemy szybkości w naszym silniku, prawda? Więcej warstw abstrakcji = mniejsza prędkość.

I miałbyś rację, gdyby to były lata 90-te. Ale my potrzebujemy łatwości w utrzymaniu i możemy żyć z ledwo zauważalną utratą prędkości dla większości części.

Jak więc powinien być zaprojektowany nasz Draw Framework? Mówiąc najprościej, jak nasze własne małe API. SFML jest tego świetnym przykładem.

Ważne rzeczy o których należy pamiętać:

  • Dbaj o dobrą dokumentację. Jakie mamy funkcje? Kiedy można je wywołać? Jak są wywoływane?
  • Utrzymaj to proste. Proste funkcje jak drawMesh(Mesh* oMesh) lub loadShader(String sPath) sprawią, że będziesz zadowolony na dłuższą metę.
  • Zachowaj funkcjonalność. Nie bądź zbyt szczegółowy. zamiast drawButtonSprite, miej drawSprite funkcję i pozwól wywoływaczowi zająć się resztą.

Co zyskujemy? Wiele:

  • Musimy tylko raz skonfigurować nasz Framework i możemy go używać w każdym systemie, którego potrzebujemy (GUI, Renderer….)
  • Możemy łatwo zmienić bazowe API, jeśli się na to zdecydujemy, bez przepisywania każdego systemu. Przejście z OpenGL na DirectX? Nie ma problemu, wystarczy przepisać klasę Frameworka.
  • Dzięki temu kod w naszych systemach jest czysty i zwarty.
  • Dobrze udokumentowany interfejs oznacza, że jedna osoba może pracować nad frameworkiem, podczas gdy druga pracuje w warstwie systemowej.

Prawdopodobnie skończymy z czymś takim:

Moja zasada kciuka dotycząca tego co wchodzi do frameworka jest raczej prosta. Jeśli potrzebuję wywołać zewnętrzną bibliotekę (OpenGL, OpenAL, SFML…) lub mam Struktury Danych / Algorytmy, których potrzebuje każdy system, powinienem zrobić to we Frameworku.

Mamy teraz zrobioną pierwszą warstwę Lasagny. Ale wciąż mamy tę wielką kulę Spaghetti nad nią. Zajmijmy się tym teraz.

Messaging

Wielki problem jednak pozostaje. Nasze systemy nadal są ze sobą połączone. Nie chcemy tego. Istnieje wiele sposobów radzenia sobie z tym problemem. Zdarzenia, Komunikaty, klasy abstrakcyjne z wskaźnikami funkcji (jakże ezoteryczne)…

Pozostańmy przy Komunikatach. Jest to prosta koncepcja, która wciąż jest bardzo popularna w programowaniu GUI. Dobrze nadaje się też jako łatwy przykład dla naszego Engine.

Działa to podobnie jak Poczta. Firma A wysyła wiadomość do firmy B i żąda, aby coś zostało zrobione. Firmy te nie potrzebują żadnego fizycznego połączenia. Firma A po prostu zakłada, że firma B zrobi to w pewnym momencie. Ale na razie firma A tak naprawdę nie dba o to, kiedy i jak firma B to zrobi. Po prostu musi to zrobić. Heck, firma B może nawet zdecydować się przekierować wiadomość do firmy C i D i pozwolić im obsługiwać go.

Możemy pójść o krok dalej, firma A nie musi nawet wysłać go do kogoś konkretnego. Firma A po prostu zamieszcza list, a każdy, kto czuje się odpowiedzialny, przetwarza go. W ten sposób firma C i D mogą bezpośrednio przetworzyć prośbę.

Oczywiście, firmy równają się naszym Systemom. Przyjrzyjmy się prostemu przykładowi:

  1. Framework powiadamia Input System, że „A” zostało naciśnięte
  2. Input tłumaczy, że naciśnięcie klawisza „A” oznacza „Open Inventory” i wysyła wiadomość zawierającą „Open Inventory”
  3. .

  4. GUI zajmuje się wiadomością i otwiera okno inwentarza
  5. Logika gry zajmuje się wiadomością i wstrzymuje grę

Wejście nawet nie dba o to, co jest robione z jego wiadomością. GUI nie dba o to, że Game Logic również przetwarza tę samą wiadomość. Gdyby wszystkie były sprzężone, Input musiałby wywoływać funkcję w GUI System i funkcję w Game Logic. Ale już nie musi. Udało nam się to skutecznie rozdzielić za pomocą Messages.

Jak wygląda Message? Powinien mieć przynajmniej jakiś typ. Na przykład, otwarcie inwentarza może być jakimś enum o nazwie OPEN_INVENTORY. To wystarczy dla prostych wiadomości, takich jak ta. Bardziej zaawansowane wiadomości, które muszą zawierać dane, będą potrzebowały jakiegoś sposobu na przechowywanie tych danych. Istnieje wiele sposobów, aby to osiągnąć. Najłatwiejszym do zaimplementowania jest użycie prostej struktury mapy.

Ale jak wysyłamy Komunikaty? Oczywiście przez magistralę komunikacyjną!

Czy to nie piękne? Nie ma już Spaghetti, tylko dobra stara, zwykła Lasagna. Celowo umieściłem naszą Game Logic po drugiej stronie Message Bus. Jak widzisz, nie ma ona połączenia z warstwą Framework. Jest to ważne, aby uniknąć pokusy „wywołania tylko tej jednej funkcji”. Zaufaj mi, prędzej czy później będziesz chciał to zrobić, ale to zepsułoby nasz projekt. Mamy wystarczająco dużo systemów zajmujących się Frameworkiem, nie ma potrzeby robić tego w naszej Logice Gry.

Szyna komunikatów jest prostą klasą z referencjami do każdego systemu. Jeśli ma Wiadomość w kolejce, Szyna Komunikatów wysyła ją do każdego Systemu poprzez proste handleMessage(Msg msg) wywołanie. W zamian, każdy System posiada referencję do Szyny Komunikatów, aby móc wysyłać Komunikaty. Może to być oczywiście przechowywane wewnętrznie lub przekazane jako argument funkcji.

Wszystkie nasze Systemy muszą więc dziedziczyć lub mieć następującą postać:

class System
{
public:
void handleMessage(Msg *msg);
{
switch(msg->type)
{
//// Example
//case Msg::OPEN_INVENTORY:
// break;
}
}

private:

MessageBus *msgBus;
//// Usage: msgBus->postMessage(msg);
}

(Tak, tak, surowe Pointery…)

Nagle nasza Pętla Gry zmienia się na po prostu pozwalanie Szynie Komunikatów na wysyłanie wiadomości. Nadal będziemy musieli okresowo aktualizować każdy System poprzez jakąś formę update() wywołania. Ale komunikacja będzie obsługiwana w inny sposób.

Jednakże, podobnie jak w przypadku naszych frameworków, używanie Wiadomości tworzy narzut. Nie oszukujmy się, spowolni to trochę działanie silnika. Ale nie zależy nam na tym! Chcemy czystego i prostego projektu. Czystej i prostej Architektury!

A najfajniejsza część? Dostajemy niesamowite rzeczy za darmo!

Konsola

Każda wiadomość jest w zasadzie wywołaniem funkcji. I każda wiadomość jest wysyłana dosłownie wszędzie! Co by było, gdybyśmy mieli System, który po prostu wypisuje każdą wiadomość, która jest wysyłana do jakiegoś okna wyjściowego? Co jeśli ten System może również wysyłać Wiadomości, które wpiszemy do tego okna?

Tak, właśnie narodziła się Konsola. A wszystko co nam to zajęło to kilka linijek kodu. Mój umysł został zdmuchnięty, gdy po raz pierwszy zobaczyłem to w akcji. Nie jest nawet do niczego przywiązana, po prostu istnieje.

Konsola jest oczywiście bardzo pomocna podczas tworzenia gry i możemy ją po prostu usunąć w wersji Release, jeśli nie chcemy, aby Gracz miał taki dostęp.

In-Game Cinematics, Replays & Debugging

Co jeśli podrobimy Wiadomości? Co jeśli stworzymy nowy system, który po prostu wysyła Wiadomości w określonym czasie? Wyobraź sobie, że wysyła coś w stylu MOVE_CAMERA, po czym następuje ROTATE_OBJECT.

And Voila, mamy In-Game Cinematics.

A co jeśli po prostu nagramy Wiadomości wejściowe, które zostały wysłane podczas gry i zapiszemy je do pliku?

And Voila, mamy Replays.

Co by było gdybyśmy po prostu nagrali wszystko co robi gracz, a kiedy gra się zawiesi, kazali mu wysłać te pliki danych do nas?

I Voila, mamy dokładną kopię działań gracza, które doprowadziły do awarii.

Wielowątkowość

Wielowątkowość? Tak, wielowątkowość. Odsprzęgliśmy wszystkie nasze systemy. Oznacza to, że mogą one przetwarzać swoje Komunikaty kiedy chcą, jak chcą i co najważniejsze, gdziekolwiek chcą. Możemy mieć naszą magistralę komunikatów decydującą o tym, w którym wątku każdy System powinien przetworzyć komunikat -> Multi-Threading

Frame Rate Fixing

Mamy zbyt wiele komunikatów do przetworzenia w tej ramce? Nie ma problemu, zatrzymajmy je w kolejce magistrali komunikatów i wyślijmy je w następnej ramce. To da nam możliwość zapewnienia, że nasza gra będzie działać w płynnych 60 FPS. Gracze nie zauważą, że AI potrzebuje kilku klatek dłużej na „myślenie”. Zauważą jednak spadek liczby klatek na sekundę.

Wiadomości są fajne.

Ważne jest, abyśmy skrupulatnie udokumentowali każdą wiadomość i jej parametry. Traktuj to jak API. Jeśli zrobisz to dobrze, każdy programista będzie mógł pracować na różnych Systemach nie psując niczego. Nawet jeśli system jest w trybie offline lub w trakcie budowy, gra nadal będzie działać i będzie można ją przetestować. Nie masz systemu audio? Nie szkodzi, nadal mamy grafikę. Nie ma Renderera, w porządku, możemy użyć Konsoli…

Ale Komunikaty nie są doskonałe. Niestety…

Czasami chcemy znać wynik wiadomości. Czasami potrzebujemy, aby zostały one przetworzone natychmiast. Musimy znaleźć wykonalne opcje. Rozwiązaniem tego problemu jest Speedway. Oprócz prostej postMessage funkcji, możemy zaimplementować postImmediateMessage funkcję, która jest przetwarzana natychmiast. Obsługa wiadomości zwrotnych jest o wiele łatwiejsza. Te zostaną wysłane do naszej funkcji handleMessage prędzej czy później. Musimy tylko o tym pamiętać, gdy wysyłamy wiadomość.

Immediate Messages oczywiście łamią Multi-Threading i Frame Rate Fixing, jeśli są wykonywane w nadmiarze. Należy więc ograniczyć się do ich używania.

Największym problemem tego systemu jest opóźnienie. Nie jest to najszybsza architektura. Jeśli pracujesz nad grą typu First Person Shooter z czasem reakcji jak w twitchu, może to być problem.

Powrót do projektowania naszej architektury

Postanowiliśmy użyć systemów i magistrali komunikatów. Wiemy dokładnie jak chcemy zbudować nasz silnik.

Czas na krok 4 naszego procesu projektowania. Iteracja. Niektóre funkcje mogą nie zmieścić się w żadnym Systemie, musimy znaleźć rozwiązanie. Niektóre funkcje muszą być często wywoływane i zapchałyby magistralę komunikatów, musimy znaleźć rozwiązanie.

To wymaga czasu. Ale na dłuższą metę jest tego warte.

Wreszcie nadszedł czas na kodowanie!

Krok 4. Gdzie zacząć kodować?

Zanim zaczniesz kodować, przeczytaj książkę/artykuł Game Programming Patterns Roberta Nystroma.

Poza tym, naszkicowałem małą mapę drogową, którą możesz podążać. Nie jest to najlepszy sposób, ale jest produktywny.

  1. Jeśli wybierasz silnik typu Message Bus, najpierw zakoduj konsolę i Message Bus. Gdy te zostaną zaimplementowane, możesz udawać istnienie każdego systemu, który nie został jeszcze zakodowany. Będziesz miał stałą kontrolę nad całym silnikiem na każdym etapie rozwoju.
  2. Zastanów się nad przejściem do GUI w następnej kolejności, jak również nad potrzebną funkcjonalnością Draw wewnątrz Frameworka. Solidny GUI w połączeniu z Konsolą pozwoli Ci na jeszcze łatwiejsze podrabianie wszystkich innych systemów. Testowanie będzie bardzo proste.
  3. Następnym krokiem powinien być Framework, przynajmniej jego interfejs. Funkcjonalność może pojawić się później.
  4. Na koniec przejdź do innych Systemów, włączając w to Rozgrywkę.

Zauważysz, że renderowanie czegokolwiek związanego z Rozgrywką może być ostatnią rzeczą jaką zrobisz. I to jest dobra rzecz! Będzie to o wiele bardziej satysfakcjonujące i zmotywuje cię do ukończenia końcowych elementów twojego silnika.

Twój Game Designer może cię zastrzelić podczas tego procesu. Testowanie rozgrywki poprzez komendy konsoli jest tak samo zabawne jak gra w Counter Strike’a poprzez IRC.

Koniec

Poświęć swój czas na znalezienie solidnej Architektury i trzymaj się jej! To jest rada, którą mam nadzieję, że wyniesiesz z tego artykułu. Jeśli to zrobisz, będziesz w stanie zbudować doskonały i łatwy w utrzymaniu silnik na koniec dnia. Albo wiek.

Personalnie, lubię pisać silniki bardziej niż robić te wszystkie rzeczy związane z gameplayem. Jeśli masz jakieś pytania, skontaktuj się ze mną przez Twittera @Spellwrath. Obecnie kończę kolejny silnik używając metod, które opisałem w tym artykule.

Część 2 możesz znaleźć tutaj.

Similar Posts

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.