Wie kann ich Riesen-Spielerklassen vermeiden?

46

Es gibt fast immer eine Spielerklasse in einem Spiel. Der Spieler kann im Allgemeinen eine Menge im Spiel machen, was bedeutet, dass diese Klasse mit einer Vielzahl von Variablen sehr umfangreich ist, um jede Funktion zu unterstützen, die der Spieler ausführen kann. Jedes Stück ist für sich genommen ziemlich klein, aber zusammen ergeben sich Tausende von Codezeilen, und es ist schwierig, das zu finden, was Sie brauchen, und es ist beängstigend, Änderungen vorzunehmen. Mit etwas, das im Grunde eine allgemeine Kontrolle für das gesamte Spiel ist, wie vermeidest du dieses Problem?

user441521
quelle
26
Mehrere Dateien oder eine Datei, der Code muss irgendwo hin. Spiele sind komplex. Um zu finden, was Sie brauchen, schreiben Sie gute Methodennamen und beschreibende Kommentare. Haben Sie keine Angst vor Änderungen - testen Sie einfach. Und sichern Sie Ihre Arbeit :)
Chris McFarland
7
Ich verstehe, dass es irgendwohin gehen muss, aber Code-Design ist wichtig für Flexibilität und Wartung. Eine Klasse oder Gruppe von Code zu haben, die aus Tausenden von Zeilen besteht, erscheint mir auch nicht so.
user441521
17
@ChrisMcFarland nicht zum Sichern vorschlagen, schlagen Sie den Versionscode XD vor.
GameDeveloper
1
@ChrisMcFarland Ich stimme GameDeveloper zu. Die Versionskontrolle wie Git, svn, TFS, ... erleichtert die Entwicklung erheblich, da große Änderungen viel einfacher rückgängig gemacht werden können und Sie sich leicht von Problemen wie versehentlichem Löschen Ihres Projekts, Hardwarefehlern oder Dateibeschädigungen erholen können.
Nzall
3
@ TylerH: Ich stimme überhaupt nicht zu. Backups erlauben weder das Zusammenführen vieler explorativer Änderungen, noch verknüpfen sie annähernd so viele nützliche Metadaten mit Changesets, noch ermöglichen sie vernünftige Workflows für mehrere Entwickler. Sie können die Versionskontrolle wie ein sehr leistungsfähiges Point-in-Time-Backup-System verwenden, aber das Potenzial dieser Tools ist noch nicht ausgeschöpft.
Phoshi

Antworten:

67

Normalerweise verwenden Sie ein Entitätskomponentensystem (ein Entitätskomponentensystem ist eine komponentenbasierte Architektur). Dies erleichtert auch das Erstellen anderer Entitäten und kann dazu führen, dass die Gegner / NPCs dieselben Komponenten wie der Spieler haben.

Dieser Ansatz geht in die genau entgegengesetzte Richtung wie ein objektorientierter Ansatz. Alles im Spiel ist eine Einheit. Die Entität ist nur ein Fall ohne eingebaute Spielmechanik. Es verfügt über eine Liste von Komponenten und eine Möglichkeit, diese zu bearbeiten.

Der Player verfügt beispielsweise über eine Positionskomponente, eine Animationskomponente und eine Eingabekomponente. Wenn der Benutzer die Leertaste drückt, soll der Player springen.

Sie können dies erreichen, indem Sie der Player-Entität eine Sprungkomponente zuweisen. Wenn diese aufgerufen wird, ändert sich die Animatiom-Komponente in die Sprunganimation, und der Player hat eine positive y-Geschwindigkeit in der Positionskomponente. In der Eingabekomponente warten Sie auf die Leertaste und rufen die Sprungkomponente auf. (Dies ist nur ein Beispiel, Sie sollten eine Controller-Komponente für die Bewegung haben).

Dies hilft dabei, den Code in kleinere, wiederverwendbare Module aufzuteilen, und kann zu einem besser organisierten Projekt führen.

Bálint
quelle
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
MichaelHouse
8
Während ich bewegende Kommentare verstehe, die verschoben werden müssen, verschieben Sie keine Kommentare, die die Richtigkeit der Antwort in Frage stellen. Das sollte doch klar sein, oder?
Bug-a-Lot
20

Spiele sind in dieser Hinsicht nicht einzigartig. Gottklassen sind überall ein Anti-Muster.

Eine übliche Lösung besteht darin, die große Klasse in einen Baum kleinerer Klassen zu zerlegen. Wenn der Spieler ein Inventar hat, machen Sie die Inventarverwaltung nicht zu einem Teil davon class Player. Erstellen Sie stattdessen eine class Inventory. Dies ist ein Member für class Player, kann aber intern class Inventoryeine Menge Code umbrechen.

Ein weiteres Beispiel: Ein Spielercharakter kann Beziehungen zu NSCs haben, sodass Sie möglicherweise class Relationsowohl auf das PlayerObjekt als auch auf das NPCObjekt verweisen , aber zu keinem von beiden gehören.

MSalters
quelle
Ja, ich habe nur nach Ideen gesucht, wie das geht. Was die Denkweise war, weil es viele Funktionen für kleine Teile gibt, so dass es für mich ohnehin nicht selbstverständlich ist, diese kleinen Funktionen während des Codierens herauszubrechen. Es wird jedoch offensichtlich, dass all diese kleinen Funktionalitäten die Spielerklasse zu einem Riesenerfolg machen.
user441521
1
Die Leute sagen normalerweise, dass etwas eine Gottklasse oder ein Gottobjekt ist, wenn es jede andere Klasse / jedes andere Objekt im Spiel enthält und verwaltet.
Bálint
11

1) Player: State-Machine + komponentenbasierte Architektur.

Übliche Komponenten für Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Das sind alles Klassen wie class HealthSystem.

Ich empfehle Update()es nicht, es dort zu benutzen. (In normalen Fällen macht es keinen Sinn, ein Update im Gesundheitssystem zu haben, es sei denn, Sie benötigen es für einige Aktionen, die in jedem Frame ausgeführt werden. Diese treten selten auf. In einem Fall, an den Sie vielleicht denken, wird der Spieler vergiftet und Sie brauchen ihn von Zeit zu Zeit die Gesundheit verlieren - hier empfehle ich die Verwendung von Koroutinen. Eine andere, die Gesundheit oder Laufkraft ständig regeneriert, nimmt einfach die aktuelle Gesundheit oder Kraft und ruft die Koroutine auf, um sich zu füllen, wenn die Zeit gekommen ist er wurde beschädigt oder er fing wieder an zu rennen und so weiter OK das war ein bisschen offtopic aber ich hoffe es war nützlich) .

Zustände: LootState, RunState, WalkState, AttackState, IDLEState.

Jeder Staat erbt von interface IState. IStatehat in unserem Fall 4 Methoden nur als Beispiel.Loot() Run() Walk() Attack()

Wir haben auch, class InputControllerwo wir für jede Eingabe des Benutzers überprüfen.

Nun zum Beispiel: InputControllerWir prüfen, ob der Spieler eine der Tasten drückt WASD or arrowsund dann, ob er auch die Taste drückt Shift. Wenn er nur WASDdann drückt , rufen wir an, _currentPlayerState.Walk();wenn dies passiert und wir müssen currentPlayerStategleich sein, WalkStatedann WalkState.Walk() haben wir alle Komponenten, die für diesen Zustand benötigt werden - in diesem Fall bringen MovementSystemwir den Spieler in Bewegung public void Walk() { _playerMovementSystem.Walk(); }- sehen Sie, was wir hier haben? Wir haben eine zweite Verhaltensebene und das ist sehr gut für die Pflege und das Debuggen von Code.

Nun zum zweiten Fall: was passiert , wenn wir WASD+ Shiftgedrückt? Aber unser vorheriger Zustand war WalkState. In diesem Fall Run()wird angerufen InputController(nicht verwechseln, Run()wird angerufen, weil wir WASD+ Shifteinchecken, InputControllernicht wegen der WalkState). Wenn wir rufen _currentPlayerState.Run();in WalkState- wir wissen , dass wir Schalter haben _currentPlayerStatezu RunStateund wir tun dies in Run()der WalkStatesie und rufen Sie wieder in diesem Verfahren aber jetzt mit einem anderen Zustand , weil wir diesen Rahmen zu verlieren Aktion nicht wollen. Und jetzt rufen wir natürlich an _playerMovementSystem.Run();.

Aber was ist, LootStatewenn der Spieler nicht laufen oder laufen kann, bis er den Knopf loslässt? Nun, in diesem Fall, als wir zu plündern begannen, zum Beispiel, als der Knopf Egedrückt wurde, rufen _currentPlayerState.Loot();wir, wir wechseln zu LootStateund rufen von dort aus an. Dort rufen wir zum Beispiel collsion method auf, um zu ermitteln, ob sich etwas in Reichweite befindet, das geplündert werden kann. Und wir rufen Coroutine auf, wo wir eine Animation haben oder wo wir sie starten, und prüfen, ob der Spieler den Knopf noch hält, wenn nicht, bricht die Coroutine, wenn ja, geben wir ihm am Ende der Coroutine Beute. Aber was ist, wenn der Spieler drückt WASD? - _currentPlayerState.Walk();heißt, aber hier ist das Schöne an der Staatsmaschine, inLootState.Walk()Wir haben eine leere Methode, die nichts macht oder wie ich es als Feature tun würde - Spieler sagen: "Hey Mann, ich habe das noch nicht geplündert, kannst du warten?". Wenn er mit dem Plündern fertig ist, wechseln wir zu IDLEState.

Sie können auch ein anderes Skript ausführen, das aufgerufen class BaseState : IStatewird und in dem alle diese Standardmethoden implementiert sind, die jedoch so definiert sind, virtualdass Sie overridesie in class LootState : BaseStateKlassentypen verwenden können.


Das komponentenbasierte System ist großartig, das einzige, was mich daran stört, sind Instanzen, viele von ihnen. Und es braucht mehr Speicher und Arbeit für den Garbage Collector. Zum Beispiel, wenn Sie 1000 Fälle von Feind haben. Alle mit 4 Komponenten. 4000 Objekte anstelle von 1000. Mb ist keine so große Sache (ich habe keine Leistungstests durchgeführt), wenn wir alle Komponenten in Betracht ziehen, die das Unity-GameObject hat.


2) Vererbungsbasierte Architektur. Sie werden feststellen, dass wir Komponenten nicht vollständig entfernen können - es ist tatsächlich unmöglich, wenn wir sauberen und funktionierenden Code haben möchten. Wenn wir Entwurfsmuster verwenden möchten, die dringend empfohlen werden, um sie in geeigneten Fällen zu verwenden (verwenden Sie sie auch nicht zu häufig, dies wird als Übergenerierung bezeichnet).

Stellen Sie sich vor, wir haben eine Player-Klasse mit allen Eigenschaften, die sie zum Beenden eines Spiels benötigt. Es hat Gesundheit, Mana oder Energie, kann sich bewegen, rennen und Fähigkeiten einsetzen, verfügt über ein Inventar, kann Gegenstände herstellen, Gegenstände plündern und sogar Barrikaden oder Türme bauen.

Zunächst einmal werde ich das Inventar sagen, Crafting, Bewegung, Gebäudekomponente basiert sein sollte , weil sie die Verantwortung haben Methoden wie nicht - Spieler ist AddItemToInventoryArray()- obwohl Spieler eine Methode haben kann wie PutItemToInventory()die vorher beschriebenen Methode aufrufen wird (2 Schichten - wir können Fügen Sie einige Bedingungen hinzu (abhängig von den verschiedenen Ebenen).

Ein weiteres Beispiel beim Bauen. Der Spieler kann so etwas aufrufen OpenBuildingWindow(), Buildingwürde sich aber um den Rest kümmern, und wenn der Benutzer beschließt, ein bestimmtes Gebäude zu bauen, übergibt er alle erforderlichen Informationen an den Spieler Build(BuildingInfo someBuildingInfo)und der Spieler beginnt, es mit allen benötigten Animationen zu erstellen.

SOLID-OOP-Prinzipien. S - Einzelverantwortung: das, was wir in früheren Beispielen gesehen haben. Ja ok, aber wo ist die Vererbung?

Hier: sollten Gesundheit und andere Eigenschaften des Spielers von einer anderen Entität behandelt werden? Ich denke nicht. Es kann keinen Spieler ohne Gesundheit geben, wenn es einen gibt, erben wir einfach nicht. Zum Beispiel haben wir IDamagable, LivingEntity, IGameActor, GameActor. IDamagablenatürlich hat TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

Hier konnte ich also keine Komponenten von der Vererbung trennen, aber wir können sie mischen, wie Sie sehen. Wir können auch einige Basisklassen für das Building-System erstellen, zum Beispiel, wenn wir verschiedene Arten davon haben und nicht mehr Code als nötig schreiben möchten. In der Tat können wir auch verschiedene Arten von Gebäuden haben und es gibt eigentlich keine gute Möglichkeit, dies komponentenbasiert zu tun!

OrganicBuilding : Building, TechBuilding : Building. Sie müssen nicht zwei Komponenten erstellen und dort zweimal Code für allgemeine Vorgänge oder Gebäudeeigenschaften schreiben. Wenn Sie sie dann anders hinzufügen, können Sie die Vererbungskraft und später die Polymorphie und Einkapselung verwenden.


Ich würde vorschlagen, etwas dazwischen zu verwenden. Und nicht überbeanspruchen Komponenten.


Ich empfehle dringend, dieses Buch über Game Programming Patterns zu lesen - es ist kostenlos im WEB.

Candid Moon _Max_
quelle
Ich werde heute Abend später nachschauen, aber zu meiner Information benutze ich keine Einheit, also muss ich einige anpassen, was in Ordnung ist.
user441521
Oh, sry, ich dachte hier wäre ein Unity-Tag, mein schlechtes. Das einzige, was ist MonoBehavior - es ist nur eine Basisklasse für jede Instanz in der Szene im Unity-Editor. Was Physics.OverlapSphere () betrifft, handelt es sich um eine Methode, die während des Frames einen Kugelcollider erstellt und überprüft, was er berührt. Coroutinen sind wie gefälschte Updates, ihre Anrufe können auf einen geringeren Wert als die FPS auf dem PC des Spielers reduziert werden - gut für die Leistung. Start () - nur eine Methode, die beim Erstellen der Instanz einmal aufgerufen wird. Alles andere sollte überall gelten. Im nächsten Teil werde ich nichts mit Unity verwenden. Sry. Hoffe das hat etwas geklärt.
Candid Moon_Max_
Ich habe Unity schon einmal benutzt, um die Idee zu verstehen. Ich benutze Lua, die auch Koroutinen hat, also sollten die Dinge ziemlich gut übersetzt werden.
user441521
Diese Antwort scheint etwas zu Unity-spezifisch zu sein, da das Unity-Tag fehlt. Wenn Sie es allgemeiner machen und das Unity-Zeug zu einem Beispiel machen würden, wäre dies eine viel bessere Antwort.
Pharap
@ CandidMoon Ja, das ist besser.
Pharap
4

Es gibt keine Silberkugel zu diesem Problem, aber es gibt verschiedene Ansätze, die sich fast alle um das Prinzip der "Trennung von Interessen" drehen. In anderen Antworten wurde der beliebte komponentenbasierte Ansatz bereits erörtert, es gibt jedoch auch andere Ansätze, die anstelle oder zusammen mit der komponentenbasierten Lösung verwendet werden können. Ich werde den Entity-Controller-Ansatz diskutieren, da er eine meiner bevorzugten Lösungen für dieses Problem ist.

Erstens ist die Vorstellung einer PlayerKlasse in erster Linie irreführend. Viele Menschen neigen dazu, sich einen Spielercharakter, NPC-Charaktere und Monster / Feinde als verschiedene Klassen vorzustellen, obwohl alle eine Menge gemeinsam haben: Sie werden alle auf dem Bildschirm gezeichnet, sie bewegen sich alle, sie könnten alle haben Vorräte etc.

Diese Denkweise führt zu einer Herangehensweise, bei der Spielercharaktere, Nicht-Spielercharaktere und Monster / Feinde alle als " Entitys" behandelt werden, anstatt anders behandelt zu werden. Natürlich müssen sie sich anders verhalten - der Spielercharakter muss über die Eingabe gesteuert werden und npcs muss ai sein.

Die Lösung hierfür sind ControllerKlassen, die zur Steuerung von Entitys verwendet werden. Auf diese Weise gelangt die gesamte schwere Logik in die Steuerung, und alle Daten und Gemeinsamkeiten werden in der Entität gespeichert.

Außerdem kann der Spieler durch Unterteilen Controllerin InputControllerund AIControllerjeden EntityRaum effektiv steuern . Dieser Ansatz hilft auch im Mehrspielermodus, indem eine RemoteControlleroder NetworkController-Klasse über Befehle aus einem Netzwerkstream ausgeführt wird.

Dies kann dazu führen, dass ein Großteil der Logik in eine einzige umgewandelt wird, Controllerwenn Sie nicht vorsichtig sind. Die Möglichkeit, dies zu vermeiden, besteht darin, Controllers aus anderen Controllers zusammenzusetzen oder die ControllerFunktionalität von verschiedenen Eigenschaften des zu bestimmen Controller. Zum Beispiel AIControllerhätte die eine DecisionTreeangefügt, und die PlayerCharacterControllerkönnte aus verschiedenen anderen zusammengesetzt sein, Controllerwie a MovementController, a JumpController(enthält eine Zustandsmaschine mit den Zuständen OnGround, Ascending und Descending), ein InventoryUIController. Ein zusätzlicher Vorteil davon ist, dass neue Controllers hinzugefügt werden können, wenn neue Funktionen hinzugefügt werden. Wenn ein Spiel ohne ein Inventarsystem gestartet wird und eines hinzugefügt wird, kann ein Controller dafür später angepasst werden.

Pharap
quelle
Die Idee gefällt mir, aber es scheint, als hätte sie den gesamten Code in die Controller-Klasse übertragen, so dass ich das gleiche Problem habe.
user441521
@ user441521 Gerade wurde mir klar, dass ich einen zusätzlichen Absatz hinzufügen wollte, den ich jedoch verlor, als mein Browser abstürzte. Ich werde es jetzt hinzufügen. Grundsätzlich können Sie verschiedene Controller zu aggregierten Controllern zusammenfassen, sodass jeder Controller unterschiedliche Aufgaben übernimmt. zB AggregateController.Controllers = {JumpController (Tastenkombinationen), MoveController (Tastenkombinationen), InventoryUIController (Tastenkombinationen, UISystem)}
Pharap