Der folgende Blog-Beitrag wurde, sofern nicht anders vermerkt, von einem Mitglied der Gamasutra-Community verfasst.
Die Gedanken und Meinungen, die hier geäußert werden, sind die des Autors und nicht die von Gamasutra oder dessen Muttergesellschaft.
Teil 1 – Nachrichtenübermittlung
Teil 2 – Speicher
Teil 3 – Daten & Cache
Teil 4 – Grafikbibliotheken
Wir leben in einer großartigen Zeit, um Entwickler zu sein. Mit einer riesigen Menge an großartigen AAA-Engines, die jedem zur Verfügung stehen, kann die Entwicklung einfacher Spiele so einfach sein wie Drag-and-Drop. Heutzutage scheint es überhaupt keinen Grund mehr zu geben, eine Engine zu schreiben. Und mit dem allgemeinen Motto „Write Games, not Engines“, warum sollte man auch?
Dieser Artikel richtet sich in erster Linie an Solo-Entwickler und kleine Teams. Ich setze eine gewisse Vertrautheit mit objektorientierter Programmierung voraus.
Ich möchte Ihnen einen Einblick in die Herangehensweise an die Entwicklung einer Engine geben und werde eine einfache fiktive Engine verwenden, um dies zu veranschaulichen.
Warum eine Engine schreiben?
Die kurze Antwort ist: Tun Sie es nicht, wenn Sie es vermeiden können.
Das Leben ist zu kurz, um für jedes Spiel eine eigene Engine zu schreiben (aus dem Buch 3D-Grafikprogrammierung von Sergei Savchenko)
Die aktuelle Auswahl an hervorragenden Engines wie Unity, Unreal oder CryEngine ist so flexibel, wie man es sich nur wünschen kann, und kann für so ziemlich jedes Spiel verwendet werden. Für speziellere Aufgaben gibt es natürlich noch speziellere Lösungen wie Adventure Game Studio oder RPG Maker, um nur ein paar zu nennen. Nicht einmal die Kosten für kommerzielle Engines sind mehr ein Argument.
Es gibt nur noch wenige Nischengründe, um eine eigene Engine zu schreiben:
- Du willst lernen, wie eine Engine funktioniert
- Du brauchst bestimmte Funktionen, die nicht verfügbar sind oder die verfügbaren Lösungen sind instabil
- Du glaubst, dass du es besser/schneller machen kannst
- Du willst die Kontrolle über die Entwicklung behalten
Alle diese Gründe sind vollkommen berechtigt und wenn du dies liest, gehörst du wahrscheinlich zu einem dieser Lager. Ich möchte hier keine langwierige Debatte über „Welche Engine sollte ich verwenden?“ oder „Sollte ich eine Engine schreiben?“ führen, sondern direkt damit beginnen. Also, lassen Sie uns beginnen.
Wie man beim Schreiben einer Engine scheitert
Warten Sie. Erst sage ich Ihnen, dass Sie keine schreiben sollen, und dann erkläre ich Ihnen, wie Sie scheitern können? Tolle Einführung…
Auf jeden Fall gibt es eine Menge Dinge zu bedenken, bevor man auch nur eine einzige Zeile Code schreibt. Das erste und größte Problem, das jeder hat, der mit dem Schreiben einer Spiel-Engine beginnt, lässt sich wie folgt zusammenfassen:
Ich will so schnell wie möglich Gameplay sehen!
Jedoch, je schneller du erkennst, dass es eine Menge Zeit braucht, bis du tatsächlich etwas Interessantes siehst, desto besser wirst du deine Engine schreiben.
Ihren Code zu zwingen, so schnell wie möglich irgendeine Form von Grafik oder Gameplay zu zeigen, nur um eine visuelle Bestätigung des „Fortschritts“ zu haben, ist an diesem Punkt Ihr größter Feind. Nimm. Deine. Zeit!
Denken Sie gar nicht erst daran, mit der Grafik zu beginnen. Du hast wahrscheinlich eine Menge OpenGL/DirectX-Tutorials und -Bücher gelesen und weißt, wie man ein einfaches Dreieck oder ein Sprite rendert. Du denkst vielleicht, dass ein kurzer Codeschnipsel zum Rendern eines kleinen Netzes auf dem Bildschirm ein guter Anfang ist. Das ist es nicht.
Ja, Ihre ersten Fortschritte werden erstaunlich sein. Verdammt, du könntest in nur einem Tag durch ein kleines Level in First-Person laufen, indem du Codeschnipsel aus verschiedenen Tutorials und Stack Overflow kopierst. Aber ich garantiere dir, dass du 2 Tage später jede einzelne Zeile dieses Codes löschen wirst. Schlimmer noch, Sie könnten sogar entmutigt werden, eine Engine zu schreiben, da es nicht motivierend ist, immer weniger zu sehen.
Das zweite große Problem, mit dem sich Entwickler beim Schreiben von Engines konfrontiert sehen, ist der Feature Creep. Jeder würde gerne den heiligen Gral der Engines schreiben. Jeder will die perfekte Engine, die alles kann. Ego-Shooter, taktische RPGs, was auch immer. Aber die einfache Tatsache ist, dass wir das nicht können. Noch nicht. Schauen Sie sich nur die großen Namen an. Nicht einmal Unity kann wirklich jedes Spielgenre perfekt abdecken.
Denken Sie nicht einmal daran, eine Engine zu schreiben, die mehr als ein Genre auf Anhieb abdecken kann. Don’t!
Wo soll man eigentlich anfangen, wenn man eine Engine schreibt
Eine Engine zu schreiben ist wie einen richtigen Motor für ein Auto zu entwickeln. Die Schritte sind eigentlich ziemlich offensichtlich, vorausgesetzt man weiß, an welchem Spiel (oder Auto) man arbeitet. Hier sind sie:
- Bestimme genau, was dein Motor können muss UND was dein Motor nicht können muss.
- Organisiere die Bedürfnisse in Systeme, die dein Motor benötigt.
- Entwirf deine perfekte Architektur, die all diese Systeme miteinander verbindet.
- Wiederhole die Schritte 1. bis 3. so oft wie möglich.
- Codiere.
Wenn (= wenn und nur wenn) du genug Zeit und Mühe in die Schritte 1. bis 4. investierst und sich das Spieldesign nicht plötzlich von einem Horrorspiel zu einem Spielautomaten (lies: Silent Hill) ändert, wird das Programmieren eine sehr angenehme Angelegenheit sein. Das Programmieren wird immer noch alles andere als einfach sein, aber perfekt zu bewältigen, selbst für Solo-Entwickler.
Das ist der Grund, warum sich dieser Artikel hauptsächlich mit den Schritten 1. bis 4. beschäftigt. Betrachten Sie Schritt 5. als „Füllen der Lücken“. 50.000 LOC of Blanks“.
Der wichtigste Teil von all dem ist Schritt 3. Hier werden wir die meisten unserer Bemühungen konzentrieren!
Schritt 1. Bestimmen Sie den Bedarf und das Nicht-Bedürfnis
Alle diese Schritte mögen auf den ersten Blick trivial erscheinen. Aber das sind sie nicht. Man könnte meinen, Schritt 1 des Entwicklungsprozesses einer Ego-Shooter-Engine ließe sich wie folgt zusammenfassen:
Ich muss ein Level laden, die Waffe des Spielers, einige Gegner mit KI. Fertig, weiter zu Schritt 2.
Wenn es nur so einfach wäre. Der beste Weg, Schritt 1 anzugehen, ist, das gesamte Spiel Klick für Klick, Aktion für Aktion durchzugehen, vom Anklicken des Icons auf dem Desktop bis zum Drücken der Exit-Taste, nachdem die Credits gewürfelt wurden. Machen Sie eine Liste, eine große Liste mit dem, was Sie brauchen. Machen Sie eine Liste mit dem, was Sie definitiv nicht brauchen.
Das wird wahrscheinlich so ablaufen:
Ich starte das Spiel und es geht direkt zum Hauptmenü. Wird das Menü ein statisches Bild verwenden? Eine Zwischensequenz? Wie steuere ich das Hauptmenü: Maus? Tastatur? Welche Art von GUI-Elementen benötige ich für das Hauptmenü? Buttons, Formulare, Scrollbars? Was ist mit Musik?
Und das sind nur Makro-Überlegungen. Gehen Sie so detailliert wie möglich vor. Die Entscheidung, dass du Buttons brauchst, ist schön und gut, aber überlege dir auch, was ein Button tun kann.
Ich möchte, dass die Buttons 4 Zustände haben, Up, Hover, Down, Disabled. Brauche ich Sound für die Buttons? Was ist mit Spezialeffekten? Sind sie im Ruhezustand animiert?
Wenn Ihre Liste der Bedürfnisse und Nicht-Bedürfnisse am Ende des Hauptmenüs nur etwa 10 Punkte enthält, haben Sie etwas falsch gemacht.
In diesem Stadium simulieren Sie die Maschine in Ihrem Gehirn und schreiben auf, was getan werden muss. Schritt 1 wird mit jeder Wiederholung klarer werden, machen Sie sich keine Sorgen, dass Sie beim ersten Mal etwas übersehen haben.
Schritt 2. Organisieren Sie die Bedürfnisse in Systemen
So, Sie haben Ihre Listen von Dingen, die Sie brauchen und nicht brauchen. Nun ist es an der Zeit, sie zu organisieren. Offensichtlich werden GUI-bezogene Dinge wie Schaltflächen in eine Art GUI-System aufgenommen. Rendering-bezogene Dinge kommen in das Grafiksystem / die Engine.
Wie bei Schritt 1 wird die Entscheidung, was wohin kommt, bei der zweiten Iteration, nach Schritt 3, klarer. Für den ersten Durchgang gruppieren Sie sie logisch wie im obigen Beispiel.
Die beste Referenz zum Thema „was gehört wohin“ und „was macht was“ ist zweifellos das Buch Game Engine Architecture von Jason Gregory.
Fangen Sie an, die Funktionen zu gruppieren. Überlege dir, wie du sie kombinieren kannst. Du brauchst Camera->rotateYaw(float yaw)
und Camera->rotatePitch(float pitch)
nicht, wenn du sie in Camera->rotate(float yaw, float pitch)
kombinieren kannst. Halten Sie es einfach. Zu viel Funktionalität (denken Sie daran, Feature Creep) wird Ihnen später schaden.
Denken Sie darüber nach, welche Funktionalität öffentlich zugänglich sein muss und welche Funktionalität nur im System selbst vorhanden sein muss. Zum Beispiel muss Ihr Renderer alle transparenten Sprites vor dem Zeichnen sortieren. Die Funktion zum Sortieren dieser Sprites muss jedoch nicht offengelegt werden. Sie wissen, dass Sie transparente Sprites vor dem Zeichnen sortieren müssen, Sie brauchen kein externes System, das Ihnen das sagt.
Schritt 3. Die Architektur (Oder, der eigentliche Artikel)
Wir hätten den Artikel auch hier beginnen können. Das ist der interessante und wichtige Teil.
Eine der einfachsten Architekturen, die deine Engine haben kann, ist, jedes System in eine Klasse zu packen und die Hauptspielschleife ihre Unterprogramme aufrufen zu lassen. Das könnte etwa so aussehen:
while(isRunning)
{
Input->readInput();
isRunning = GameLogic->doLogic();
Camera->update();
World->update();
GUI->update();
AI->update();
Audio->play();
Render->draw();
}
Auf den ersten Blick klingt das ganz vernünftig. Sie haben alle Ihre Grundlagen abgedeckt, Eingabe -> Verarbeitung der Eingabe -> Ausgabe.
Und in der Tat wird dies für ein einfaches Spiel ausreichen. Aber es wird mühsam zu pflegen sein. Der Grund dafür sollte offensichtlich sein: Abhängigkeiten.
Jedes System muss auf irgendeine Weise mit anderen Systemen kommunizieren. In unserer obigen Spielschleife haben wir keine Möglichkeit, das zu tun. Daher zeigt das Beispiel deutlich, dass jedes System eine Referenz der anderen Systeme haben muss, um etwas Sinnvolles zu tun. Unser GUI und die Spiellogik müssen etwas über unseren Input wissen. Unser Renderer muss etwas über unsere Game Logic wissen, um irgendetwas sinnvolles anzuzeigen.
Das führt zu diesem architektonischen Wunder:
Wenn es nach Spaghetti riecht, ist es Spaghetti. Definitiv nicht das, was wir wollen. Ja, es ist einfach und schnell zu programmieren. Ja, wir werden akzeptable Ergebnisse erhalten. Aber wartbar ist es nicht. Ändert man irgendwo ein kleines Stück Code, kann das verheerende Auswirkungen auf alle anderen Systeme haben, ohne dass wir es merken.
Außerdem wird es immer Code geben, auf den viele Systeme Zugriff brauchen. Sowohl das GUI als auch der Renderer müssen Zeichnungsaufrufe tätigen oder zumindest Zugriff auf eine Art Schnittstelle haben, um dies für uns zu erledigen. Ja, wir könnten jedem System die Befugnis geben, OpenGL-/DirectX-Funktionen direkt aufzurufen, aber wir werden mit einer Menge Redundanzen enden.
Wir könnten dies lösen, indem wir alle Zeichenfunktionen im Renderer-System sammeln und diese vom GUI-System aus aufrufen. Aber dann wird das Rendering System spezifische Funktionen für die GUI haben. Diese haben im Renderer nichts zu suchen und stehen daher im Widerspruch zu Schritt 1 und 2. Entscheidungen, Entscheidungen.
Das erste, was wir in Erwägung ziehen sollten, ist unsere Engine in Schichten aufzuteilen.
Engine Lasagne
Lasagna ist besser als Spaghetti. Zumindest programmiertechnisch gesehen. Um bei unserem Renderer-Beispiel zu bleiben, wollen wir OpenGL/DirectX-Funktionen aufrufen, ohne sie direkt im System aufzurufen. Das riecht wie ein Wrapper. Und zum größten Teil ist es das auch. Wir sammeln die gesamte Zeichenfunktionalität in einer anderen Klasse. Diese Klassen sind sogar noch einfacher als unsere Systeme. Nennen wir diese neuen Klassen das Framework.
Die Idee dahinter ist, viele der API-Aufrufe auf niedriger Ebene zu abstrahieren und sie zu etwas zu formen, das auf unser Spiel zugeschnitten ist. Wir wollen nicht den Vertex Buffer setzen, den Index Buffer setzen, die Texturen setzen, dies aktivieren, das deaktivieren, nur um einen einfachen Zeichenaufruf in unserem Renderer System zu machen. Lasst uns all dieses Low-Level-Zeug in unser Framework packen. Und ich werde diesen Teil des Frameworks einfach „Draw“ nennen. Und warum? Nun, alles, was es tut, ist, alles für das Zeichnen einzurichten und es dann zu zeichnen. Es kümmert sich nicht darum, was es zeichnet, wo es zeichnet und warum es zeichnet. Das wird dem Renderer System überlassen.
Das mag seltsam erscheinen, wir wollen doch Geschwindigkeit in unserer Engine, oder? Mehr Abstraktionsebenen = weniger Geschwindigkeit.
Und du hättest Recht, wenn es die 90er Jahre wären. Aber wir brauchen die Wartbarkeit und können mit dem kaum merklichen Geschwindigkeitsverlust für die meisten Teile leben.
Wie sollte dann unser Draw Framework aufgebaut sein? Einfach gesagt, wie unsere eigene kleine API. SFML ist ein großartiges Beispiel dafür.
Wichtige Dinge, die man im Auge behalten sollte:
- Halten Sie es gut dokumentiert. Welche Funktionen haben wir? Wann können sie aufgerufen werden? Wie werden sie aufgerufen?
- Bewahre es einfach. Einfache Funktionen wie drawMesh(Mesh* oMesh) oder loadShader(String sPath) werden dich auf lange Sicht glücklich machen.
- Behalte es funktional. Sei nicht zu spezifisch. Anstelle von
drawButtonSprite
, habe einedrawSprite
Funktion und lass den Aufrufer den Rest erledigen.
Was gewinnen wir? Viel:
- Wir müssen unser Framework nur einmal einrichten und können es in jedem System verwenden, das wir brauchen (GUI, Renderer….)
- Wir können die zugrundeliegenden APIs leicht ändern, wenn wir wollen, ohne jedes System neu zu schreiben. Von OpenGL zu DirectX wechseln? Kein Problem, schreiben Sie einfach die Framework-Klasse neu.
- So bleibt der Code in unseren Systemen sauber und straff.
- Eine gut dokumentierte Schnittstelle zu haben, bedeutet, dass eine Person am Framework arbeiten kann, während eine Person in der Systemschicht arbeitet.
Wir werden wahrscheinlich mit etwas wie diesem enden:
Meine Faustregel, was in das Framework geht, ist ziemlich einfach. Wenn ich eine externe Bibliothek aufrufen muss (OpenGL, OpenAL, SFML…) oder Datenstrukturen / Algorithmen habe, die jedes System braucht, sollte ich das im Framework tun.
Wir haben jetzt unsere erste Schicht der Lasagne fertig. Aber wir haben immer noch dieses riesige Knäuel Spaghetti darüber. Lassen Sie uns das als nächstes in Angriff nehmen.
Messaging
Das große Problem bleibt jedoch bestehen. Unsere Systeme sind immer noch alle miteinander verbunden. Das wollen wir nicht. Es gibt eine Vielzahl von Möglichkeiten, mit diesem Problem umzugehen. Ereignisse, Nachrichten, abstrakte Klassen mit Funktionszeigern (wie esoterisch)…
Lassen Sie uns bei den Nachrichten bleiben. Dies ist ein einfaches Konzept, das in der GUI-Programmierung immer noch sehr beliebt ist. Es eignet sich auch gut als einfaches Beispiel für unsere Engine.
Es funktioniert wie ein Postdienst. Unternehmen A schickt eine Nachricht an Unternehmen B und bittet darum, etwas zu tun. Diese Unternehmen brauchen keine physische Verbindung. Unternehmen A geht einfach davon aus, dass Unternehmen B es irgendwann tun wird. Aber im Moment ist es Unternehmen A egal, wann oder wie Unternehmen B die Aufgabe erledigt. Es muss einfach getan werden. Unternehmen B könnte sogar beschließen, die Nachricht an Unternehmen C und D weiterzuleiten und ihnen die Bearbeitung zu überlassen.
Wir können noch einen Schritt weiter gehen: Unternehmen A muss die Nachricht nicht einmal an eine bestimmte Person schicken. Unternehmen A stellt das Schreiben einfach ein und jeder, der sich zuständig fühlt, wird es bearbeiten. Auf diese Weise können Unternehmen C und D die Anfrage direkt bearbeiten.
Es ist offensichtlich, dass die Unternehmen unseren Systemen entsprechen. Schauen wir uns ein einfaches Beispiel an:
- Framework benachrichtigt Input System, dass „A“ gedrückt wurde
- Input übersetzt, dass der Tastendruck „A“ „Open Inventory“ bedeutet und sendet eine Nachricht mit „Open Inventory“
- Die Benutzeroberfläche verarbeitet die Nachricht und öffnet das Inventarfenster
- Die Spiellogik verarbeitet die Nachricht und pausiert das Spiel
Die Eingabe kümmert sich nicht darum, was mit ihrer Nachricht geschieht. GUI kümmert sich nicht darum, dass Game Logic die gleiche Nachricht verarbeitet. Wären sie alle gekoppelt, müsste Input eine Funktion im GUI-System und eine Funktion in Game Logic aufrufen. Aber das ist nicht mehr nötig. Wir konnten dies erfolgreich mit Messages entkoppeln.
Wie sieht eine Message aus? Sie sollte zumindest einen Typ haben. Zum Beispiel könnte das Öffnen des Inventars ein Enum namens OPEN_INVENTORY
sein. Das reicht für einfache Nachrichten wie diese aus. Fortgeschrittene Nachrichten, die Daten enthalten müssen, benötigen eine Möglichkeit, diese Daten zu speichern. Es gibt eine Vielzahl von Möglichkeiten, dies zu erreichen. Die einfachste ist die Verwendung einer einfachen Map-Struktur.
Aber wie senden wir Nachrichten? Über einen Message Bus natürlich!
Ist das nicht schön? Keine Spaghetti mehr, nur noch die gute alte einfache Lasagne. Ich habe unsere Spiellogik absichtlich auf die andere Seite des Nachrichtenbusses gesetzt. Wie Sie sehen können, hat sie keine Verbindung zur Framework-Schicht. Das ist wichtig, um die Versuchung zu vermeiden, „nur diese eine Funktion aufzurufen“. Glauben Sie mir, früher oder später werden Sie das wollen, aber das würde unser Design zerstören. Wir haben genug Systeme, die mit dem Framework zu tun haben, wir brauchen das nicht auch noch in unserer Spiellogik.
Der Nachrichtenbus ist eine einfache Klasse mit Referenzen zu jedem System. Wenn er eine Nachricht in der Warteschlange hat, sendet der Nachrichtenbus diese an jedes System über einen einfachen handleMessage(Msg msg)
Aufruf. Im Gegenzug hat jedes System einen Verweis auf den Nachrichtenbus, um Nachrichten zu versenden. Diese kann natürlich intern gespeichert oder als Funktionsargument übergeben werden.
Alle unsere Systeme müssen also erben oder von folgender Form sein:
class System
{
public:
void handleMessage(Msg *msg);
{
switch(msg->type)
{
//// Example
//case Msg::OPEN_INVENTORY:
// break;
}
}
private:
MessageBus *msgBus;
//// Usage: msgBus->postMessage(msg);
}
(Ja, ja, rohe Zeiger…)
Plötzlich ändert sich unsere Spielschleife dahingehend, dass wir den Nachrichtenbus einfach Nachrichten herumschicken lassen. Wir müssen immer noch jedes System in regelmäßigen Abständen durch irgendeine Form von update()
Aufrufen aktualisieren. Aber die Kommunikation wird anders gehandhabt.
Wie bei unseren Frameworks erzeugt die Verwendung von Nachrichten jedoch Overhead. Das wird die Engine ein wenig verlangsamen, machen wir uns nichts vor. Aber das ist uns egal! Wir wollen ein sauberes und einfaches Design. Eine saubere und einfache Architektur!
Und das Coolste daran? Wir bekommen erstaunliche Dinge umsonst!
Die Konsole
Jede Nachricht ist so ziemlich ein Funktionsaufruf. Und jede Nachricht wird so ziemlich überall hingeschickt! Was wäre, wenn wir ein System hätten, das einfach jede Nachricht, die gesendet wird, in einem Ausgabefenster ausgibt? Was wäre, wenn dieses System auch Nachrichten senden könnte, die wir in dieses Fenster eingeben?
Ja, wir haben gerade eine Konsole geboren. Und alles, was wir dazu brauchten, sind ein paar Zeilen Code. Als ich das zum ersten Mal in Aktion gesehen habe, war ich baff. Sie ist nicht einmal mit irgendetwas verbunden, sie existiert einfach.
Eine Konsole ist offensichtlich sehr hilfreich während der Entwicklung des Spiels und wir können sie bei der Veröffentlichung einfach herausnehmen, wenn wir nicht wollen, dass der Spieler diese Art von Zugriff hat.
In-Game Cinematics, Replays & Debugging
Was, wenn wir Nachrichten fälschen? Wie wäre es, wenn wir ein neues System erstellen, das einfach zu einer bestimmten Zeit Nachrichten sendet? Stell dir vor, es sendet etwas wie MOVE_CAMERA
, gefolgt von ROTATE_OBJECT
.
Und Voila, wir haben In-Game Cinematics.
Wie wäre es, wenn wir einfach die Eingangsnachrichten, die während des Spiels gesendet wurden, aufzeichnen und in einer Datei speichern?
Und Voila, wir haben Replays.
Wie wäre es, wenn wir einfach alles aufzeichnen, was der Spieler tut, und wenn das Spiel abstürzt, diese Dateien an uns schicken?
Und Voila, wir haben eine exakte Kopie der Aktionen des Spielers, die zum Absturz führten.
Multi-Threading
Multi-Threading? Ja, Multi-Threading. Wir haben alle unsere Systeme entkoppelt. Das heißt, sie können ihre Nachrichten verarbeiten, wann sie wollen, wie sie wollen und vor allem, wo sie wollen. Wir können unseren Nachrichtenbus entscheiden lassen, auf welchem Thread jedes System eine Nachricht verarbeiten soll -> Multi-Threading
Frame Rate Fixing
Wir haben zu viele Nachrichten, um diesen Frame zu verarbeiten? Kein Problem, wir behalten sie einfach in der Warteschlange des Nachrichtenbusses und senden sie im nächsten Frame. Das gibt uns die Möglichkeit, sicherzustellen, dass unser Spiel mit einer flüssigen Geschwindigkeit von 60 FPS läuft. Die Spieler werden nicht bemerken, dass die KI ein paar Frames länger braucht, um zu „denken“. Sie werden jedoch Einbrüche in der Framerate bemerken.
Nachrichten sind cool.
Es ist wichtig, dass wir jede Nachricht und ihre Parameter akribisch dokumentieren. Behandeln Sie sie wie eine API. Wenn man das richtig macht, kann jeder Entwickler an verschiedenen Systemen arbeiten, ohne etwas kaputt zu machen. Selbst wenn ein System offline oder im Bau sein sollte, läuft das Spiel trotzdem und kann getestet werden. Kein Audio-System? Kein Problem, wir haben ja noch Visuals. Kein Renderer, das ist in Ordnung, wir können die Konsole benutzen…
Aber Nachrichten sind nicht perfekt. Traurigerweise
Manchmal wollen wir das Ergebnis einer Nachricht wissen. Manchmal müssen sie sofort bearbeitet werden. Wir müssen praktikable Optionen finden. Eine Lösung für dieses Problem ist ein Speedway. Abgesehen von einer einfachen postMessage
Funktion, können wir eine postImmediateMessage
Funktion implementieren, die sofort verarbeitet wird. Die Behandlung von Rückmeldungen ist viel einfacher. Diese werden früher oder später an unsere handleMessage
Funktion gesendet. Wir müssen nur daran denken, wenn wir eine Nachricht senden.
Sofortige Nachrichten brechen natürlich Multi-Threading und Frame Rate Fixing, wenn sie im Übermaß gemacht werden. Es ist daher wichtig, sich selbst einzuschränken, um ihre Verwendung zu begrenzen.
Aber das größte Problem bei diesem System ist die Latenz. Es ist nicht die schnellste Architektur. Wenn Sie an einem Ego-Shooter mit Twitch-ähnlichen Reaktionszeiten arbeiten, könnte dies ein Dealbreaker sein.
Zurück zum Entwurf unserer Architektur
Wir haben beschlossen, Systeme und einen Nachrichtenbus zu verwenden. Wir wissen genau, wie wir unsere Engine strukturieren wollen.
Es ist Zeit für Schritt 4 unseres Designprozesses. Iteration. Einige Funktionen passen vielleicht nicht in jedes System, wir müssen eine Lösung finden. Einige Funktionen müssen häufig aufgerufen werden und würden den Nachrichtenbus verstopfen, wir müssen eine Lösung finden.
Das braucht Zeit. Aber es lohnt sich auf lange Sicht.
Endlich ist es Zeit zu codieren!
Schritt 4. Wo soll ich anfangen zu programmieren?
Bevor Sie mit dem Programmieren beginnen, lesen Sie das Buch/Artikel Game Programming Patterns von Robert Nystrom.
Außerdem habe ich eine kleine Roadmap skizziert, der Sie folgen können. Es ist bei weitem nicht der beste Weg, aber er ist produktiv.
- Wenn Sie sich für eine Message Bus Engine entscheiden, sollten Sie zuerst die Konsole und den Message Bus programmieren. Sobald diese implementiert sind, können Sie die Existenz eines jeden Systems vortäuschen, das noch nicht codiert wurde. Sie werden in jeder Phase der Entwicklung die ständige Kontrolle über die gesamte Engine haben.
- Überlegen Sie, ob Sie als nächstes mit der GUI und der benötigten Zeichenfunktionalität innerhalb des Frameworks fortfahren wollen. Eine solide GUI gepaart mit der Konsole wird es Ihnen ermöglichen, alle anderen Systeme noch einfacher zu fälschen. Das Testen wird ein Kinderspiel sein.
- Als nächstes sollte das Framework kommen, zumindest die Schnittstelle. Die Funktionalität kann später folgen.
- Schließlich sollten Sie zu den anderen Systemen übergehen, einschließlich des Spiels.
Sie werden feststellen, dass das Rendern von allem, was mit dem Spiel zu tun hat, das Letzte sein könnte, was Sie tun. Und das ist auch gut so! Es wird sich so viel lohnender anfühlen und Sie motivieren, die letzten Feinheiten Ihrer Engine zu vollenden.
Ihr Game Designer könnte Sie jedoch während dieses Prozesses erschießen. Das Testen des Gameplays über Konsolenbefehle macht ungefähr so viel Spaß wie das Spielen von Counter Strike über IRC.
Fazit
Nehmen Sie sich die Zeit, eine solide Architektur zu finden und bleiben Sie dabei! Das ist der Rat, den Sie hoffentlich aus diesem Artikel mitnehmen. Wenn Sie das tun, werden Sie am Ende des Tages in der Lage sein, eine perfekt funktionierende und wartbare Engine zu bauen. Oder Jahrhundert.
Persönlich macht mir das Schreiben von Engines mehr Spaß als der ganze Gameplay-Kram. Wenn du Fragen hast, kannst du mich gerne über Twitter @Spellwrath kontaktieren. Ich bin gerade dabei, eine weitere Engine mit den Methoden, die ich in diesem Artikel beschrieben habe, fertigzustellen.
Teil 2 findet ihr hier.