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ückt WASD or arrows
und dann, ob er auch die Taste drückt Shift
. Wenn er nur WASD
dann drückt , rufen wir an, _currentPlayerState.Walk();
wenn dies passiert und wir müssen currentPlayerState
gleich sein, WalkState
dann WalkState.Walk()
haben wir alle Komponenten, die für diesen Zustand benötigt werden - in diesem Fall bringen MovementSystem
wir 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
+ Shift
gedrückt? Aber unser vorheriger Zustand war WalkState
. In diesem Fall Run()
wird angerufen InputController
(nicht verwechseln, Run()
wird angerufen, weil wir WASD
+ Shift
einchecken, InputController
nicht wegen der WalkState
). Wenn wir rufen _currentPlayerState.Run();
in WalkState
- wir wissen , dass wir Schalter haben _currentPlayerState
zu RunState
und wir tun dies in Run()
der WalkState
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 Knopf E
gedrückt wurde, rufen _currentPlayerState.Loot();
wir, wir wechseln zu LootState
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ü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 : IState
wird und in dem alle diese Standardmethoden implementiert sind, die jedoch so definiert sind, virtual
dass Sie override
sie in class 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 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()
, 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 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
. IDamagable
natü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.