Wie vermeide ich harte Codierung in Game Engines?

22

Meine Frage ist keine Kodierungsfrage. Es gilt für das gesamte Game-Engine-Design im Allgemeinen.

Wie vermeidet man harte Codierung?

Diese Frage ist viel tiefer, als es scheint. Angenommen, Sie möchten ein Spiel ausführen, das die für den Betrieb erforderlichen Dateien lädt. Wie vermeiden Sie es, so etwas wie load specificfile.wadim Code der Engine zu sagen ? Wenn die Datei geladen wird, wie vermeiden Sie es zu sagen load aspecificmap in specificfile.wad?

Diese Frage betrifft so ziemlich das gesamte Motordesign, und der Motor sollte so wenig wie möglich fest codiert sein. Was ist der beste Weg, um das zu erreichen?

Marcus Cramer
quelle

Antworten:

42

Datengesteuerte Codierung

Alles, was Sie erwähnen, kann in Daten angegeben werden. Warum lädst du aspecificmap? Weil die Spielkonfiguration besagt, dass dies die erste Ebene ist, wenn ein Spieler ein neues Spiel startet, oder weil dies der Name des aktuellen Speicherpunkts in der gerade geladenen Speicherdatei des Spielers ist, usw.

Wie findest aspecificmapdu Weil es sich um eine Datendatei handelt, in der Zuordnungs-IDs und ihre Ressourcen auf der Festplatte aufgelistet sind.

Es muss nur einen besonders kleinen Satz von "Kern" -Ressourcen geben, die aus legitimen Gründen schwer oder unmöglich zu vermeiden sind. Mit einem wenig Arbeit, kann dies zu einem einzigen hartcodierte begrenzt Standard Asset Namen wie main.wadoder dergleichen. Diese Datei kann möglicherweise zur Laufzeit geändert werden, indem ein Befehlszeilenargument an das Spiel übergeben wird, auch bekannt als game.exe -wad mymain.wad.

Das Schreiben von datengesteuertem Code beruht auf einigen anderen Prinzipien. Beispielsweise kann man vermeiden, dass Systeme oder Module nach einer bestimmten Ressource fragen, und stattdessen diese Abhängigkeiten umkehren. Das heißt, DebugDrawerladen Sie debug.fontden Initialisierungscode nicht nach. DebugDrawerNehmen Sie stattdessen ein Ressourcenhandle in den Initialisierungscode. Dieses Handle wird möglicherweise aus der Hauptspielkonfigurationsdatei geladen.

Als konkretes Beispiel aus unserer Codebasis haben wir ein "globales Daten" -Objekt, das aus der Ressourcendatenbank geladen wird (die selbst standardmäßig der ./resourcesOrdner ist, aber mit einem Befehlszeilenargument überladen werden kann). Die Ressourcendatenbank-ID dieser globalen Daten ist der einzig notwendige fest codierte Ressourcenname in der Codebasis (wir haben andere, weil Programmierer manchmal faul werden, aber im Allgemeinen werden diese letztendlich repariert / entfernt). Dieses globale Datenobjekt ist voll von Komponenten, deren einziger Zweck darin besteht, Konfigurationsdaten bereitzustellen. Eine der Komponenten ist die UI Global Data-Komponente, die neben einer Reihe anderer Konfigurationselemente Ressourcenhandles für alle wichtigen UI-Ressourcen (Schriftarten, Flash-Dateien, Symbole, Lokalisierungsdaten usw.) enthält. Wenn ein UI-Entwickler beschließt, das Haupt-UI-Asset von /ui/mainmenu.swfin umzubenennen/ui/lobby.swfSie aktualisieren nur diese globale Datenreferenz. es muss sich überhaupt kein Motorcode ändern.

Wir nutzen diese globalen Daten für alles. Alle spielbaren Charaktere, alle Ebenen, Benutzeroberfläche, Audio, Kernelemente, Netzwerkkonfiguration, alles. (Nun, nicht alles , aber diese anderen Dinge sind zu behebende Fehler.)

Dieser Ansatz hat viele andere Vorteile. Zum einen wird das Packen und Bündeln von Ressourcen zum integralen Bestandteil des gesamten Prozesses. Festcodierte Pfade in der Engine bedeuten in der Regel auch, dass dieselben Pfade in den Skripten oder Tools, die die Spielelemente zusammenfassen, festcodiert werden müssen. Diese Pfade können dann nicht mehr synchron sein. Wenn wir uns stattdessen auf ein einzelnes Kern-Asset und Referenzketten von dort stützen, können wir ein Asset-Bundle mit einem einzigen Befehl erstellen bundle.exe -root config.data -out main.wadund wissen, dass es alle erforderlichen Assets enthalten wird. Da der Bündler lediglich Ressourcenreferenzen folgt, wissen wir, dass nur die von uns benötigten Ressourcen enthalten sind, und überspringen alle verbleibenden Flusen, die sich unvermeidlich während der Laufzeit eines Projekts ansammeln (und wir können automatisch Listen davon erstellen) Flusen zum Beschneiden).

Ein kniffliger Eckfall dieser ganzen Sache ist in Skripten. Es ist konzeptionell einfach, die Engine datengetrieben zu machen, aber ich habe so viele Projekte gesehen (Hobby bis AAA), bei denen Skripte als Daten betrachtet werden und daher "nur wahllos" Ressourcenpfade verwenden dürfen. Mach das nicht. Wenn eine Lua-Datei eine Ressource benötigt und nur eine Funktion wie diese aufruft, wird textures.lua("/path/to/texture.png")die Asset-Pipeline große Probleme damit haben, zu wissen, dass das Skript /path/to/texture.pngordnungsgemäß ausgeführt werden muss, und diese Textur möglicherweise als nicht verwendet und unnötig erachtet. Skripts sollten wie jeder andere Code behandelt werden: Alle erforderlichen Daten, einschließlich Ressourcen oder Tabellen, sollten in einem Konfigurationseintrag angegeben werden, den die Engine und die Ressourcenpipeline auf Abhängigkeiten untersuchen können. Die Daten, in denen "Skript laden foo.lua" steht, sollten stattdessen "foo.luaund geben Sie diese Parameter ein, "wo die Parameter alle erforderlichen Ressourcen enthalten. Wenn ein Skript beispielsweise zufällig Feinde erzeugt, geben Sie die Liste der möglichen Feinde aus dieser Konfigurationsdatei in das Skript ein. Die Engine kann die Feinde dann mit dem Level vorladen ( da kennt er die vollständige Liste der möglichen Spawns) und die Ressource Pipeline kennt alle Feinde mit dem Spiel zu bündeln (da sie von Konfigurationsdaten endgültig referenziert sind.) Wenn die Skripts Strings von Pfadnamen erzeugt und ruft nur eine loadFunktion dann weder Die Engine oder die Ressourcen-Pipeline wissen auf irgendeine Weise, welche Assets das Skript möglicherweise zu laden versucht.

Sean Middleditch
quelle
Gute Antwort, sehr praktisch und erklärt auch die Fallstricke und Fehler, die die Leute bei der Implementierung machen! +1
am
+1. Fügen Sie hinzu, dass das Verweisen auf Ressourcen, die Konfigurationsdaten enthalten, ebenfalls sehr hilfreich ist, wenn Sie das Modden aktivieren möchten. Es ist soooo viel schwieriger und riskanter, Spiele zu modifizieren, bei denen Sie die ursprünglichen Datendateien ändern müssen, anstatt Ihre eigenen zu erstellen und auf diese zu verweisen. Noch besser, wenn Sie auf mehrere Dateien mit einer definierten Prioritätsreihenfolge verweisen können.
Jeutnarg
12

Ebenso vermeiden Sie die Hardcodierung in allgemeinen Funktionen.

Sie übergeben Parameter und speichern Ihre Informationen in Konfigurationsdateien.

In dieser Situation gibt es im Software-Engineering absolut keinen Unterschied zwischen dem Schreiben einer Engine und dem Schreiben einer Klasse.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Dann liest Ihr Client-Code eine "Master" -Konfigurationsdatei ( diese ist entweder fest codiert oder wird als Befehlszeilenargument übergeben), die die Informationen enthält, aus denen hervorgeht, wo sich die Assets-Dateien befinden und welche Zuordnung sie enthalten.

Von dort wird alles von der "Master" -Konfigurationsdatei gesteuert.

Vaillancourt
quelle
1
Ja, das plus eine Art Mechanismus, um benutzerdefinierte Logik einzubringen. Möglicherweise wird eine Sprache wie C #, Python usw. eingebettet, um die Kernfunktionen der Engine um benutzerdefinierte Funktionen zu erweitern
qCring
3

Ich mag die anderen Antworten, deshalb werde ich ein bisschen widersprüchlich sein. ;)

Sie können es nicht vermeiden, Wissen über Ihre Daten in Ihre Engine zu codieren. Woher die Informationen kommen, muss der Motor wissen, ob er danach sucht. Sie können jedoch vermeiden, die eigentlichen Informationen in Ihre Engine zu codieren.

Bei einem "reinen" datengesteuerten Ansatz müssten Sie die ausführbare Datei mit den Befehlszeilenparametern starten, die zum Laden der Erstkonfiguration erforderlich sind. Die Engine muss jedoch codiert sein, um zu wissen, wie diese Informationen zu interpretieren sind. Beispiel : Wenn Sie Ihre Konfigurationsdateien JSON sind, müssen Sie die Variablen hart Code Sie suchen, zum Beispiel der Motor suchen müssen wissen , "intro_movies"und "level_list"und so weiter.

Eine "gut konstruierte" Engine kann jedoch für viele verschiedene Spiele verwendet werden, indem nur die Konfigurationsdaten und die Daten, auf die sie verweist, ausgetauscht werden.

Das Mantra vermeidet also nicht so sehr das harte Codieren, sondern stellt sicher, dass Sie Änderungen mit dem geringstmöglichen Aufwand vornehmen können.

Im Gegensatz zum Ansatz der Datendateien (den ich voll und ganz unterstütze) ist es möglicherweise in Ordnung, dass Sie die Daten in Ihre Engine kompilieren. Wenn die "Kosten" dafür geringer sind, gibt es keinen wirklichen Schaden. Wenn Sie der einzige sind, der daran arbeitet, können Sie das Filehandling auf einen späteren Zeitpunkt verschieben und müssen sich nicht unbedingt selbst verarschen. In meinen ersten Spielprojekten waren große Datentabellen im Spiel selbst fest codiert, z. B. eine Liste der Waffen und ihrer sortierten Daten:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Sie können diese Daten also an einem leicht zu referenzierenden Ort ablegen und bei Bedarf leicht bearbeiten. Das Ideal wäre, dieses Zeug in eine Art Konfigurationsdatei zu packen, aber dann müsst ihr Parsing und Übersetzung machen und all diesen Jazz, und das Verknüpfen von Interstruktur-Referenzen könnte zu einem zusätzlichen Schmerz werden, den ihr wirklich nicht wollt Zurecht kommen.

Dash-Tom-Bang
quelle
Es ist nicht besonders schwierig, Json zu analysieren. Der einzige "Kostenfaktor" ist das Lernen. (Insbesondere das Erlernen der Verwendung des entsprechenden Moduls oder der entsprechenden Bibliothek. Go bietet beispielsweise gute Unterstützung für JSON.)
Wildcard
Es ist nicht sonderlich schwierig, aber es muss nicht nur gelernt werden. ZB weiß ich, wie man JSON technisch analysiert. Ich habe Parser für viele andere Dateiformate geschrieben, aber ich muss entweder eine Drittanbieterlösung finden und installieren (und Abhängigkeiten herausfinden und wie man sie erstellt) oder meine eigene rollen. Es braucht mehr Zeit als es nicht zu tun.
Dash-Tom-Bang
4
Alles braucht mehr Zeit als es nicht zu tun. Aber die Tools, die Sie benötigen, wurden bereits geschrieben. Genauso wie Sie keinen Compiler entwerfen müssen , um ein Spiel zu schreiben oder mit Maschinencode herumzuspielen, aber Sie müssen eine Sprache für die Plattform lernen, mit der Sie arbeiten. Lernen Sie also auch, einen Json-Parser zu verwenden.
Wildcard
Ich bin mir nicht sicher, was dein Argument ist. In dieser Antwort befürworte ich YAGNI; Wenn Sie nicht die Zeit verschwenden müssen, etwas zu tun, das Ihnen nicht hilft, dann tun Sie es nicht. Wenn Sie Zeit damit verbringen möchten, dann großartig. Vielleicht musst du die Zeit später verbringen, vielleicht auch nicht, aber es von vorne zu machen, lenkt dich nur von der Aufgabe ab, das Spiel tatsächlich zu machen. Spielentwicklung ist trivial; Jede einzelne Aufgabe, die zum Erstellen eines Spiels gehört, ist einfach. Es ist nur so, dass die meisten Spiele eine Million einfacher Aufgaben haben und ein verantwortungsbewusster Entwickler diejenigen auswählt, die am schnellsten zu diesem Ziel gelangen.
Dash-Tom-Bang
2
Eigentlich habe ich Ihre Antwort positiv bewertet. kein wirkliches Argument als solches. Ich wollte nur feststellen, dass JSON nicht schwer zu analysieren ist. Wenn ich noch einmal lese, habe ich wahrscheinlich hauptsächlich auf das Snippet geantwortet, "aber dann muss man Parsing und Übersetzung machen und all diesen Jazz." Aber ich stimme zu, dass für persönliche Projektspiele und dergleichen YAGNI. :)
Wildcard