Beratung zu Spielarchitektur / Designmustern

16

Ich arbeite jetzt schon eine Weile an einem 2D-Rollenspiel, und mir ist klar geworden, dass ich einige schlechte Designentscheidungen getroffen habe. Es gibt ein paar Dinge, die mir Probleme bereiten, und ich habe mich gefragt, welche Entwürfe andere Leute verwendet haben, um sie zu überwinden, oder welche sie verwenden würden.

Für einen kleinen Hintergrund habe ich letzten Sommer in meiner Freizeit angefangen, daran zu arbeiten. Ich habe das Spiel anfangs in C # gemacht, aber vor ungefähr 3 Monaten habe ich beschlossen, zu C ++ zu wechseln. Ich wollte C ++ in den Griff bekommen, da es schon eine Weile her ist, dass ich es intensiv genutzt habe, und dachte, ein interessantes Projekt wie dieses wäre ein guter Motivator. Ich habe die Boost-Bibliothek intensiv genutzt und SFML für Grafiken und FMOD für Audio verwendet.

Ich habe ein gutes Stück Code geschrieben, überlege mir aber, ob ich es wegwerfen und von vorne anfangen soll.

Hier sind die Hauptanliegen, die ich habe und die ich ein paar Meinungen dazu einholen möchte, wie andere sie gelöst haben oder lösen würden.

1. Zyklische Abhängigkeiten Als ich das Spiel in C # machte, musste ich mir darüber keine Sorgen machen, da es dort kein Problem gibt. Der Umstieg auf C ++ ist zu einem ziemlich großen Problem geworden und hat mich zu der Annahme gebracht, dass ich die Dinge möglicherweise falsch entworfen habe. Ich kann mir nicht wirklich vorstellen, wie ich meine Klassen entkoppeln kann und sie trotzdem machen lassen, was ich will. Hier einige Beispiele für eine Abhängigkeitskette:

Ich habe eine Statuseffektklasse. Die Klasse verfügt über eine Reihe von Methoden (Anwenden / Unanwenden, Häkchen usw.), um ihre Effekte auf ein Zeichen anzuwenden. Zum Beispiel,

virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);

Diese Funktion wird jedes Mal aufgerufen, wenn der Charakter, dem der Statuseffekt zugefügt wurde, an der Reihe ist. Es wird verwendet, um Effekte wie Regen, Poison usw. zu implementieren. Es werden jedoch auch Abhängigkeiten von der BaseCharacter-Klasse und der BattleField-Klasse eingeführt. Natürlich muss die BaseCharacter-Klasse nachverfolgen, welche Statuseffekte derzeit auf sie aktiv sind, damit dies eine zyklische Abhängigkeit darstellt. Battlefield muss den Überblick über die kämpfenden Gruppen behalten, und die Gruppenklasse verfügt über eine Liste von BaseCharacters, die eine weitere zyklische Abhängigkeit einführen.

2 - Ereignisse

In C # habe ich die Delegierten ausgiebig genutzt, um Ereignisse an Charakteren, Schlachtfeldern usw. anzuhängen (zum Beispiel gab es einen Delegierten, wenn sich der Gesundheitszustand des Charakters änderte, wenn sich ein Status änderte, wenn ein Statuseffekt hinzugefügt / entfernt wurde usw.) .) und die Schlachtfeld- / Grafikkomponenten würden sich in diese Delegierten einhängen, um deren Auswirkungen durchzusetzen. In C ++ habe ich etwas Ähnliches gemacht. Offensichtlich gibt es kein direktes Äquivalent zu C # -Delegierten. Stattdessen habe ich Folgendes erstellt:

typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;

und in meiner Charakterklasse

std::map<std::string, StatChangeFunction> StatChangeEventHandlers;

Wenn sich der Status des Charakters änderte, iterierte ich und rief jede StatChangeFunction auf der Karte auf. Ich mache mir zwar Sorgen, dass dies ein schlechter Ansatz ist, um Dinge zu tun.

3 - Grafiken

Das ist das große Ding. Es hat nichts mit der Grafikbibliothek zu tun, die ich verwende, ist aber eher eine konzeptionelle Sache. In C # habe ich Grafiken mit vielen meiner Kurse gekoppelt, von denen ich weiß, dass es eine schreckliche Idee ist. Diesmal wollte ich es entkoppelt tun und versuchte es mit einem anderen Ansatz.

Um meine Grafiken zu implementieren, stellte ich mir alle Grafiken im Spiel als eine Reihe von Bildschirmen vor. Das heißt, es gibt einen Titelbildschirm, einen Charakterstatusbildschirm, einen Kartenbildschirm, einen Inventarbildschirm, einen Kampfbildschirm und einen Kampf-GUI-Bildschirm. Grundsätzlich könnte ich diese Bildschirme nach Bedarf übereinander stapeln, um die Spielgrafiken zu erstellen. Was auch immer der aktive Bildschirm ist, besitzt die Spieleingabe.

Ich habe einen Bildschirmmanager entworfen, der Bildschirme basierend auf Benutzereingaben verschiebt und öffnet.

Wenn Sie sich beispielsweise in einem Kartenbildschirm (Eingabehandler / Visualisierer für eine Kachelkarte) befinden und die Starttaste drücken, wird der Bildschirmmanager aufgerufen, um einen Hauptmenübildschirm über den Kartenbildschirm zu schieben und die Karte zu markieren Bildschirm nicht gezeichnet / aktualisiert werden. Der Player navigiert durch das Menü, gibt dem Bildschirmmanager je nach Bedarf weitere Befehle aus, um neue Bildschirme auf den Bildschirmstapel zu verschieben, und fügt sie dann ein, wenn der Benutzer Bildschirme / Abbrüche ändert. Schließlich, wenn der Spieler das Hauptmenü verlässt, würde ich es ausklappen und zum Kartenbildschirm zurückkehren, darauf hinweisen, dass es gezeichnet / aktualisiert werden soll, und von dort aus fortfahren.

Kampfbildschirme wären komplexer. Ich hätte einen Bildschirm als Hintergrund, einen Bildschirm zur Visualisierung jeder Partei im Kampf und einen Bildschirm zur Visualisierung der Benutzeroberfläche für den Kampf. Die Benutzeroberfläche würde sich in die Zeichenereignisse einhängen und diese verwenden, um zu bestimmen, wann die Benutzeroberflächenkomponenten aktualisiert / neu gezeichnet werden sollen. Schließlich würde jeder Angriff, für den ein Animationsskript verfügbar ist, eine zusätzliche Ebene aufrufen, um sich selbst zu animieren, bevor der Bildschirmstapel ausgeblendet wird. In diesem Fall wird jede Ebene durchgehend als zeichnbar und aktualisierbar markiert, und ich erhalte einen Stapel von Bildschirmen, auf denen meine Kampfgrafiken angezeigt werden.

Obwohl ich den Bildschirmmanager noch nicht zum einwandfreien Funktionieren gebracht habe, glaube ich, dass ich es mit der Zeit schaffen kann. Meine Frage ist, ist dies überhaupt ein lohnender Ansatz? Wenn es ein schlechtes Design ist, möchte ich jetzt wissen, bevor ich zu viel Zeit in die Herstellung aller Bildschirme investiere, die ich benötigen werde. Wie baust du die Grafik für dein Spiel auf?

user127817
quelle

Antworten:

15

Insgesamt würde ich nicht sagen, dass irgendetwas, was Sie aufgelistet haben, dazu führen sollte, dass Sie das System verschrotten und von vorne beginnen. Dies ist etwas, was jeder Programmierer im Verlauf eines Projekts, an dem er arbeitet, zu etwa 50-75% tun möchte, aber es führt zu einem nie endenden Entwicklungszyklus und niemals zum Abschluss von irgendetwas. Zu diesem Zweck geben einige Rückmeldungen zu jedem Abschnitt.

  1. Dies kann ein Problem sein, ist jedoch in der Regel eher ärgerlich als alles andere. Verwenden Sie das #pragma einmal oder #ifndef MY_HEADER_FILE_H #def MY_HEADER_FILE_H ... #endif oben bzw. um Ihre .h-Dateien herum? Auf diese Weise ist die .h-Datei in jedem Bereich nur einmal vorhanden. Wenn ja, dann wird meine Empfehlung darin bestehen, alle # include-Anweisungen zu entfernen und zu kompilieren und die Anweisungen nach Bedarf hinzuzufügen, um das Spiel erneut zu kompilieren.

  2. Ich bin ein Fan dieser Art von Systemen und sehe nichts falsch daran. Was in C # ein Ereignis ist, wird normalerweise durch ein Ereignissystem oder ein Messagingsystem ersetzt (Sie können die Fragen hier nach diesen Dingen durchsuchen, um weitere Informationen zu erhalten). Der Schlüssel hier ist, diese auf ein Minimum zu beschränken, wenn Dinge passieren müssen, was sich bereits so anhört, als ob Sie dies tun, und dies sollte hier keine bis minimale Sorge sein.

  3. Dies scheint mir auch auf dem richtigen Weg zu sein und ist das, was ich persönlich und beruflich für meine eigenen Motoren tue. Dadurch wird das Menüsystem zu einem Statussystem, in dem entweder das Hauptmenü (vor dem Spielstart) oder das Player-HUD als "Hauptbildschirm" angezeigt wird, je nachdem, wie Sie es eingerichtet haben.

Zusammenfassend sehe ich also nichts, was es wert ist, neu gestartet zu werden. Vielleicht möchten Sie später ein formelleres Event-System ersetzen, aber das wird mit der Zeit geschehen. Cyclic Includes ist eine Hürde, durch die alle C / C ++ - Programmierer ständig springen müssen, und die Arbeit, um die Grafiken zu entkoppeln, scheint wie logische „nächste Schritte“.

Hoffe das hilft!

James
quelle
#ifdef hilft nicht beim Einschließen von Problemen.
Die kommunistische Ente
Ich habe gerade damit gerechnet, dass das hier drin sein wird, bevor ich die zyklischen Includes aufspüre. Kann ein ganz anderer Fischkessel sein, wenn Sie mehrere Symboldefinitionen haben, im Gegensatz zu einer Datei, die eine Datei enthalten muss, die sich selbst enthält. (obwohl von dem, was er beschrieb, wenn die Includes in den .CPP-Dateien und nicht in den .H-Dateien sind, sollte er damit einverstanden sein, dass zwei Basisobjekte voneinander wissen)
James,
Danke für den Rat :)
Freut
4

Ihre zyklischen Abhängigkeiten sollten kein Problem sein, solange Sie die Klassen in den Header-Dateien deklarieren und sie tatsächlich in die CPP-Dateien (oder was auch immer) einschließen.

Für das Ereignissystem zwei Vorschläge:

1) Wenn Sie das Muster beibehalten möchten, das Sie jetzt verwenden, sollten Sie zu einer boost :: unordered_map anstelle von std :: map wechseln. Das Mappen mit Strings als Schlüsseln ist langsam, zumal .NET einige nette Dinge unter der Haube tut, um die Dinge zu beschleunigen. Bei Verwendung von unordered_map-Hashes werden die Zeichenfolgen durchsucht, sodass Vergleiche im Allgemeinen schneller sind.

2) Überlegen Sie sich, auf etwas Stärkeres wie boost :: signals umzuschalten. Wenn Sie das tun, können Sie nette Dinge tun, wie Ihre Spielobjekte verfolgbar zu machen, indem Sie von boost :: signals :: trackable ableiten, und den Destruktor sich darum kümmern lassen, alles zu bereinigen, anstatt sich manuell vom Ereignissystem abmelden zu müssen. Sie können auch mehrere Signale zu jedem Schlitz zeigen (oder umgekehrt, ich die genaue Nomenklatur nicht erinnern) , so dass es sehr ähnlich ist zu tun , +=auf eine delegatein C #. Das größte Problem bei boost :: signals besteht darin, dass es kompiliert werden muss, nicht nur Header. Abhängig von Ihrer Plattform kann es schwierig sein, einsatzbereit zu sein.

Tetrade
quelle