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?
architecture
user441521
quelle
quelle
Antworten:
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.
quelle
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 eineclass Inventory
. Dies ist ein Member fürclass Player
, kann aber internclass Inventory
eine Menge Code umbrechen.Ein weiteres Beispiel: Ein Spielercharakter kann Beziehungen zu NSCs haben, sodass Sie möglicherweise
class Relation
sowohl auf dasPlayer
Objekt als auch auf dasNPC
Objekt verweisen , aber zu keinem von beiden gehören.quelle
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
.IState
hat in unserem Fall 4 Methoden nur als Beispiel.Loot() Run() Walk() Attack()
Wir haben auch,
class InputController
wo wir für jede Eingabe des Benutzers überprüfen.Nun zum Beispiel:
InputController
Wir prüfen, ob der Spieler eine der Tasten drücktWASD or arrows
und dann, ob er auch die Taste drücktShift
. Wenn er nurWASD
dann drückt , rufen wir an,_currentPlayerState.Walk();
wenn dies passiert und wir müssencurrentPlayerState
gleich sein,WalkState
dannWalkState.Walk()
haben wir alle Komponenten, die für diesen Zustand benötigt werden - in diesem Fall bringenMovementSystem
wir den Spieler in Bewegungpublic 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
+Shift
gedrückt? Aber unser vorheriger Zustand warWalkState
. In diesem FallRun()
wird angerufenInputController
(nicht verwechseln,Run()
wird angerufen, weil wirWASD
+Shift
einchecken,InputController
nicht wegen derWalkState
). Wenn wir rufen_currentPlayerState.Run();
inWalkState
- wir wissen , dass wir Schalter haben_currentPlayerState
zuRunState
und wir tun dies inRun()
derWalkState
sie 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,
LootState
wenn 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 KnopfE
gedrückt wurde, rufen_currentPlayerState.Loot();
wir, wir wechseln zuLootState
und 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ücktWASD
? -_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 zuIDLEState
.Sie können auch ein anderes Skript ausführen, das aufgerufen
class BaseState : IState
wird und in dem alle diese Standardmethoden implementiert sind, die jedoch so definiert sind,virtual
dass Sieoverride
sie inclass LootState : BaseState
Klassentypen 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 wiePutItemToInventory()
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()
,Building
wü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 SpielerBuild(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
.IDamagable
natürlich hatTakeDamage()
.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.
quelle
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
Player
Klasse 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 "
Entity
s" 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
Controller
Klassen, die zur Steuerung vonEntity
s 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
Controller
inInputController
undAIController
jedenEntity
Raum effektiv steuern . Dieser Ansatz hilft auch im Mehrspielermodus, indem eineRemoteController
oderNetworkController
-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,
Controller
wenn Sie nicht vorsichtig sind. Die Möglichkeit, dies zu vermeiden, besteht darin,Controller
s aus anderenController
s zusammenzusetzen oder dieController
Funktionalität von verschiedenen Eigenschaften des zu bestimmenController
. Zum BeispielAIController
hätte die eineDecisionTree
angefügt, und diePlayerCharacterController
könnte aus verschiedenen anderen zusammengesetzt sein,Controller
wie aMovementController
, aJumpController
(enthält eine Zustandsmaschine mit den Zuständen OnGround, Ascending und Descending), einInventoryUIController
. Ein zusätzlicher Vorteil davon ist, dass neueController
s 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.quelle