Verwenden des Composite Design Pattern für ein RPG-Attributsystem

Intelligenz, Willenskraft, Charisma, Weisheit: Abgesehen davon, dass Sie als Spieleentwickler wichtige Eigenschaften haben sollten, sind dies auch übliche Attribute, die in RPGs verwendet werden. Das Berechnen der Werte solcher Attribute - das Anwenden von zeitlich festgelegten Boni und die Berücksichtigung der Auswirkungen von ausgerüsteten Gegenständen - kann schwierig sein. In diesem Tutorial zeige ich Ihnen, wie Sie ein leicht modifiziertes Composite-Pattern verwenden, um dies schnell zu handhaben.

Hinweis: Obwohl dieses Tutorial mit Flash und AS3 geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.


Einführung

Attributsysteme werden sehr häufig in RPGs verwendet, um die Stärken, Schwächen und Fähigkeiten von Charakteren zu quantifizieren. Wenn Sie nicht mit ihnen vertraut sind, überfliegen Sie die Wikipedia-Seite, um eine anständige Übersicht zu erhalten.

Um sie dynamischer und interessanter zu machen, verbessern Entwickler diese Systeme häufig, indem sie Fähigkeiten, Gegenstände und andere Dinge hinzufügen, die die Attribute beeinflussen. Wenn Sie dies tun möchten, benötigen Sie ein gutes System, das die endgültigen Attribute (unter Berücksichtigung aller anderen Effekte) berechnet und die Hinzufügung oder Entfernung verschiedener Boniarten übernimmt.

In diesem Lernprogramm untersuchen wir eine Lösung für dieses Problem, indem Sie eine leicht modifizierte Version des Composite-Entwurfsmusters verwenden. Unsere Lösung ist in der Lage, Boni zu verarbeiten und funktioniert mit allen Attributen, die Sie definieren.


Was ist das zusammengesetzte Muster??

In diesem Abschnitt finden Sie eine Übersicht über das Composite-Designmuster. Wenn Sie sich bereits damit auskennen, können Sie zu überspringen Unser Problem modellieren.

Das Composite-Muster ist ein Entwurfsmuster (eine bekannte, wiederverwendbare, allgemeine Entwurfsvorlage), um etwas Großes in kleinere Objekte zu unterteilen, um eine größere Gruppe zu erstellen, indem nur die kleinen Objekte behandelt werden. Es macht es einfach, große Informationsbrocken in kleinere, leichter zu behandelnde Informationsblöcke aufzuteilen. Im Wesentlichen handelt es sich dabei um eine Vorlage für die Verwendung einer Gruppe eines bestimmten Objekts, als wäre es ein einzelnes Objekt.

Wir werden ein weit verbreitetes Beispiel verwenden, um dies zu veranschaulichen: Denken Sie an eine einfache Zeichenanwendung. Sie möchten damit Sie Dreiecke, Quadrate und Kreise zeichnen und anders behandeln können. Sie möchten aber auch Zeichnungsgruppen handhaben können. Wie können wir das leicht tun??

Das Composite-Muster ist der perfekte Kandidat für diesen Job. Indem eine "Gruppe von Zeichnungen" als Zeichnung selbst behandelt wird, könnte man leicht jede Zeichnung innerhalb dieser Gruppe hinzufügen, und die Gruppe als Ganzes würde immer noch als eine einzige Zeichnung betrachtet.

In Bezug auf die Programmierung hätten wir eine Basisklasse, Zeichnung, das Standardverhalten einer Zeichnung (Sie können sie verschieben, Ebenen ändern, drehen usw.) und vier Unterklassen haben, Dreieck, Quadrat, Kreis und Gruppe.

In diesem Fall weisen die ersten drei Klassen ein einfaches Verhalten auf, das nur die Eingabe der grundlegenden Attribute jeder Form durch den Benutzer erfordert. Das Gruppe Die Klasse verfügt jedoch über Methoden zum Hinzufügen und Entfernen von Formen sowie zum Ausführen einer Operation für alle Formen (z. B. Ändern der Farbe aller Formen in einer Gruppe). Alle vier Unterklassen würden weiterhin als a behandelt Zeichnung, Sie müssen sich also nicht darum kümmern, spezifischen Code hinzuzufügen, wenn Sie mit einer Gruppe arbeiten möchten.

Um dies in eine bessere Darstellung zu bringen, können wir jede Zeichnung als Knoten in einem Baum anzeigen. Jeder Knoten ist ein Blatt mit Ausnahme von Gruppe Knoten, die untergeordnete Elemente haben können - wiederum Zeichnungen innerhalb dieser Gruppe.


Eine visuelle Darstellung des Musters

Im Beispiel der Zeichen-App ist dies eine visuelle Darstellung der "Zeichenanwendung", über die wir nachgedacht haben. Beachten Sie, dass das Bild drei Zeichnungen enthält: ein Dreieck, ein Quadrat und eine Gruppe, die aus einem Kreis und einem Quadrat besteht:

Und dies ist die Baumdarstellung der aktuellen Szene (die Wurzel ist die Stufe der Zeichenanwendung):

Was wäre, wenn wir eine weitere Zeichnung hinzufügen wollten, die aus einer Gruppe von Dreiecken und Kreisen innerhalb der aktuellen Gruppe besteht? Wir würden es einfach so hinzufügen, wie wir jede Zeichnung innerhalb einer Gruppe hinzufügen würden. So würde die visuelle Darstellung aussehen:

Und so würde der Baum werden:

Nun stellen wir uns vor, dass wir eine Lösung für das Attributproblem erstellen werden, das wir haben. Offensichtlich haben wir keine direkte visuelle Darstellung (wir können nur das Endergebnis sehen, das ist das berechnete Attribut angesichts der Rohwerte und der Boni). Wir werden also im Composite-Pattern mit der Baumdarstellung nachdenken.


Unser Problem modellieren

Um unsere Attribute in einem Baum modellieren zu können, müssen wir jedes Attribut in die kleinsten Teile unterteilen, die wir können.

Wir wissen, dass wir Boni haben, die dem Attribut entweder einen Rohwert hinzufügen oder um einen Prozentsatz erhöhen können. Es gibt Boni, die zu dem Attribut beitragen, und andere, die berechnet werden, nachdem alle ersten Boni angewendet wurden (z. B. Boni von Fertigkeiten)..

So können wir haben:

  • Rohe Boni (addiert zum Rohwert des Attributs)
  • Endgültige Boni (hinzugefügt, nachdem alles andere berechnet wurde)

Sie haben vielleicht bemerkt, dass wir Boni, die einen Wert zum Attribut hinzufügen, nicht von Boni trennen, die das Attribut um einen Prozentsatz erhöhen. Das liegt daran, dass wir jeden Bonus modellieren, um beide gleichzeitig ändern zu können. Dies bedeutet, dass wir einen Bonus haben könnten, der den Wert um 5 erhöht und erhöht das Attribut um 10%. Dies wird alles im Code behandelt.

Diese zwei Arten von Boni sind nur die Blätter unseres Baumes. Sie sind ziemlich ähnlich Dreieck, Quadrat und Kreis Klassen in unserem Beispiel von vor.

Wir haben immer noch keine Einheit erstellt, die als Gruppe dienen soll. Diese Entitäten werden die Attribute selbst sein! Das Gruppe Klasse in unserem Beispiel wird einfach das Attribut selbst sein. Also haben wir eine Attribut Klasse, die sich wie benimmt irgendein Attribut.

So könnte ein Attributbaum aussehen:

Nun, da alles entschieden ist, beginnen wir unseren Code?


Erstellen der Basisklassen

In diesem Lernprogramm verwenden wir ActionScript 3.0 als Sprache für den Code. Machen Sie sich jedoch keine Sorgen! Der Code wird anschließend vollständig kommentiert. Alles, was für die Sprache (und die Flash-Plattform) einzigartig ist, wird erklärt und es werden Alternativen zur Verfügung gestellt. Wenn Sie also mit einer OOP-Sprache vertraut sind, können Sie dem folgen Tutorial ohne Probleme.

Die erste Klasse, die wir erstellen müssen, ist die Basisklasse für alle Attribute und Boni. Die Datei wird aufgerufen BaseAttribute.as, und das Erstellen ist sehr einfach. Hier ist der Code mit Kommentaren danach:

 package public class BaseAttribute private var _baseValue: int; private var _baseMultiplier: Number; öffentliche Funktion BaseAttribute (Wert: int, Multiplikator: Number = 0) _baseValue = Wert; _baseMultiplier = Multiplikator;  public function get baseValue (): int return _baseValue;  public function get baseMultiplier (): Number return _baseMultiplier; 

Wie Sie sehen, sind die Dinge in dieser Basisklasse sehr einfach. Wir schaffen einfach die _Wert und _Multiplikator ordnen Sie sie im Konstruktor zu und erstellen Sie zwei Getter-Methoden, eine für jedes Feld.

Jetzt müssen wir die erstellen RawBonus und FinalBonus Klassen. Dies sind einfach Unterklassen von BaseAttribute, mit nichts hinzugefügt. Sie können es beliebig erweitern, aber jetzt werden wir nur diese beiden leeren Unterklassen von erstellen BaseAttribute:

RawBonus.as:

 Paket öffentliche Klasse RawBonus erweitert BaseAttribute öffentliche Funktion RawBonus (Wert: int = 0, Multiplikator: Number = 0) Super (Wert, Multiplikator); 

FinalBonus.as:

 package public class FinalBonus erweitert BaseAttribute public-Funktion FinalBonus (Wert: int = 0, Multiplikator: Number = 0) super (Wert, Multiplikator); 

Wie Sie sehen können, enthalten diese Klassen nur einen Konstruktor.


Die Attributklasse

Das Attribut Die Klasse entspricht einer Gruppe im Composite-Pattern. Es kann Roh- oder Endboni enthalten und verfügt über eine Methode zur Berechnung des Endwerts des Attributs. Da ist es eine Unterklasse von BaseAttribute, das _Basiswert Feld der Klasse ist der Startwert des Attributs.

Beim Erstellen der Klasse haben wir ein Problem bei der Berechnung des endgültigen Werts des Attributs: Da wir keine rohen Boni von den endgültigen Boni trennen, können wir den endgültigen Wert nicht berechnen, da wir nicht wissen, wann wende jeden Bonus an.

Sie können dieses Problem lösen, indem Sie das grundlegende Composite-Muster leicht modifizieren. Anstatt ein Kind zu demselben "Container" in der Gruppe hinzuzufügen, erstellen wir zwei "Container", einen für die Rohboni und einen für die Endboni. Jeder Bonus wird noch ein Kind von sein Attribut, wird sich jedoch an verschiedenen Stellen befinden, um den endgültigen Wert des Attributs berechnen zu können.

Wenn wir das erklärt haben, kommen wir zum Code!

 package public class Attribut erweitert BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; private var _finalValue: int; öffentliche Funktion Attribut (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  public function addRawBonus (Bonus: RawBonus): void _rawBonuses.push (Bonus);  public function addFinalBonus (Bonus: FinalBonus): void _finalBonuses.push (Bonus);  public function removeRawBonus (Bonus: RawBonus): void if (_rawBonuses.indexOf (Bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (Bonus), 1);  public function removeFinalBonus (Bonus: FinalBonus): ungültig if (_finalBonuses.indexOf (Bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (Bonus), 1);  public function berechneValue (): int _finalValue = baseValue; // Wert aus raw hinzufügen var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; für jeden (var Bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Wert aus final var hinzufügen finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; für jeden (var Bonus: FinalBonus in _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); return _finalValue;  public function get finalValue (): int return berechneValue (); 

Die Methoden addRawBonus (), addFinalBonus (), removeRawBonus () und removeFinalBonus () sind sehr klar. Alles, was sie tun, ist, ihren spezifischen Bonustyp zu dem Array hinzuzufügen oder zu entfernen, das alle Boni dieses Typs enthält.

Der schwierige Teil ist der berechneWert () Methode. Zuerst werden alle Werte zusammengefasst, die die rohen Boni dem Attribut hinzufügen, und alle Multiplikatoren. Danach addiert es die Summe aller rohen Bonuswerte zum Startattribut und wendet dann den Multiplikator an. Später führt derselbe Schritt für die endgültigen Boni aus, wobei jedoch die Werte und Multiplikatoren auf den halb berechneten endgültigen Attributwert angewendet werden.

Und mit der Struktur sind wir fertig! Überprüfen Sie die nächsten Schritte, um zu sehen, wie Sie diese verwenden und erweitern würden.


Zusätzliches Verhalten: Zeitliche Boni

In unserer jetzigen Struktur haben wir nur einfache Roh- und Endboni, die derzeit überhaupt keinen Unterschied haben. In diesem Schritt fügen wir dem hinzu FinalBonus Klasse, um es mehr wie Boni aussehen zu lassen, die angewendet werden aktiv Fähigkeiten in einem Spiel.

Da solche Fähigkeiten, wie der Name schon sagt, nur für einen bestimmten Zeitraum aktiv sind, werden wir bei den endgültigen Boni ein Timing-Verhalten hinzufügen. Die Rohboni könnten beispielsweise für Boni verwendet werden, die durch Ausrüstung hinzugefügt werden.

Dazu verwenden wir die Timer Klasse. Diese Klasse stammt aus ActionScript 3.0 und verhält sich wie ein Zeitgeber. Sie beginnt bei 0 Sekunden und ruft eine angegebene Funktion nach einer bestimmten Zeit auf, setzt sie auf 0 zurück und startet die Zählung erneut, bis der angegebene Wert erreicht wird Anzahl der Zähler erneut. Wenn Sie sie nicht angeben, wird die Timer läuft weiter, bis Sie aufhören. Sie können auswählen, wann der Timer startet und wann er stoppt. Sie können ihr Verhalten einfach replizieren, indem Sie bei Bedarf das Zeitgebungssystem Ihrer Sprache mit entsprechendem zusätzlichen Code verwenden.

Lass uns zum Code springen!

 package import flash.events.TimerEvent; import flash.utils.Timer; public class FinalBonus erweitert BaseAttribute private var _timer: Timer; private var _parent: Attribut; öffentliche Funktion FinalBonus (Zeit: int, Wert: int = 0, Multiplikator: Number = 0) super (Wert, Multiplikator); _timer = neuer Timer (Zeit); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  public function startTimer (übergeordnetes Attribut): void _parent = übergeordnetes Element; _timer.start ();  private Funktion onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (this); 

Im Konstruktor besteht der erste Unterschied darin, dass die endgültigen Boni jetzt a benötigen Zeit Parameter, der anzeigt, wie lange sie dauern. Innerhalb des Konstruktors erstellen wir eine Timer für diese Zeit (vorausgesetzt, die Zeit ist in Millisekunden) und fügen Sie einen Ereignis-Listener hinzu.

(Ereignis-Listener sind im Grunde das, was den Timer dazu bringt, die richtige Funktion aufzurufen, wenn er diese bestimmte Zeitspanne erreicht. In diesem Fall ist dies die aufzurufende Funktion onTimerEnd ().)

Beachten Sie, dass wir den Timer noch nicht gestartet haben. Dies geschieht im startTimer () Methode, die auch einen Parameter erfordert, Elternteil, was muss ein sein Attribut. Diese Funktion erfordert das Attribut, das den Bonus hinzufügt, um diese Funktion aufzurufen, um sie zu aktivieren. Dies startet wiederum den Timer und teilt dem Bonus mit, welche Instanz gebeten werden soll, den Bonus zu entfernen, wenn der Timer sein Limit erreicht hat.

Der Abtragsteil ist im erledigt onTimerEnd () Diese Methode fordert den übergeordneten Befehl nur auf, ihn zu entfernen und den Timer zu stoppen.

Endgültige Boni können jetzt als zeitlich festgelegte Boni verwendet werden, was darauf hinweist, dass sie nur für eine bestimmte Zeit gültig sind.


Zusätzliches Verhalten: Abhängige Attribute

Was in RPG-Spielen häufig vorkommt, sind Attribute, die von anderen abhängen. Nehmen wir zum Beispiel das Attribut "Angriffsgeschwindigkeit". Es hängt nicht nur von der Art der Waffe ab, die Sie verwenden, sondern auch fast immer von der Geschicklichkeit des Charakters.

In unserem derzeitigen System lassen wir nur Boni zu Attribut Instanzen. In unserem Beispiel müssen wir jedoch zulassen, dass ein Attribut ein Kind eines anderen Attributs ist. Wie können wir das machen? Wir können eine Unterklasse von erstellen Attribut, namens DependantAttribute, und geben Sie dieser Unterklasse alles Verhalten, das wir brauchen.

Das Hinzufügen von Attributen als untergeordnete Elemente ist sehr einfach: Alles, was wir tun müssen, ist, ein anderes Array für Attribute zu erstellen und spezifischen Code für die Berechnung des endgültigen Attributs hinzuzufügen. Da wir nicht wissen, ob jedes Attribut auf die gleiche Weise berechnet wird (Sie können die Angriffsgeschwindigkeit zuerst mit Geschicklichkeit ändern und dann die Boni überprüfen, aber zuerst die Bonusse verwenden, um den magischen Angriff zu ändern, und dann beispielsweise Intelligenz), müssen wir auch die Berechnung des endgültigen Attributs in der Attribut Klasse in verschiedenen Funktionen. Lass uns das zuerst machen.

Im Attribute.as:

 package public class Attribut erweitert BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; protected var _finalValue: int; öffentliche Funktion Attribut (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  public function addRawBonus (Bonus: RawBonus): void _rawBonuses.push (Bonus);  public function addFinalBonus (Bonus: FinalBonus): void _finalBonuses.push (Bonus);  public function removeRawBonus (Bonus: RawBonus): void if (_rawBonuses.indexOf (Bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (Bonus), 1);  public function removeFinalBonus (Bonus: RawBonus): ungültig if (_finalBonuses.indexOf (Bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (Bonus), 1);  Geschützte Funktion applyRawBonuses (): void // Wert aus raw hinzufügen var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; für jeden (var Bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  protected function applyFinalBonuses (): void // Hinzufügen von Werten aus final var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; für jeden (var Bonus: RawBonus in _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  public function berechneValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue;  public function get finalValue (): int return berechneValue (); 

Wie Sie an den hervorgehobenen Linien sehen können, haben wir nur erstellt applyRawBonuses () und applyFinalBonuses () und rufen Sie sie auf, wenn Sie das letzte Attribut in berechnen berechneWert (). Wir haben auch gemacht _finalValue geschützt, damit wir es in den Unterklassen ändern können.

Nun ist alles für uns eingestellt, um das zu schaffen DependantAttribute Klasse! Hier ist der Code:

 package public class DependantAttribute erweitert Attribut protected var _otherAttributes: Array; öffentliche Funktion DependantAttribute (startingValue: int) super (startingValue); _otherAttributes = [];  public function addAttribute (attr: Attribut): void _otherAttributes.push (attr);  public function removeAttribute (attr: Attribute): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  public override-Funktion berechneValue (): int // Bestimmter Attributcode geht irgendwo hier rein _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In dieser Klasse die Attribute hinzufügen() und removeAttribute () Funktionen sollten Ihnen vertraut sein. Sie müssen auf das Überschreiben achten berechneWert () Funktion. Hier verwenden wir nicht die Attribute zur Berechnung des endgültigen Werts - Sie müssen dies für jedes abhängige Attribut tun!

Dies ist ein Beispiel, wie Sie dies zur Berechnung der Angriffsgeschwindigkeit tun würden:

 package public class AttackSpeed ​​erweitert DependantAttribute öffentliche Funktion AttackSpeed ​​(startingValue: int) super (startingValue);  public override-Funktion berechneValue (): int _finalValue = baseValue; // Alle 5 Punkte in der Fingerfertigkeit addiert 1 zur Geschwindigkeit der Angriffsgeschwindigkeit: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (Geschicklichkeit / 5); applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In dieser Klasse wird davon ausgegangen, dass Sie das Attribut "Geschicklichkeit" bereits als untergeordnetes Element von hinzugefügt haben Angriffsgeschwindigkeit, und das ist das erste in der _otherAttributes array (das sind viele Annahmen, die Sie überprüfen sollten, überprüfen Sie die Schlussfolgerung für weitere Informationen). Nachdem wir die Fingerfertigkeit abgerufen haben, fügen wir einfach den Endwert der Angriffsgeschwindigkeit hinzu.


Fazit

Wie würden Sie diese Struktur in einem Spiel verwenden, wenn alles fertig ist? Es ist sehr einfach: Sie müssen lediglich verschiedene Attribute erstellen und jedem zuweisen Attribut Beispiel. Danach müssen Sie mit den bereits erstellten Methoden Boni hinzufügen und entfernen.

Wenn ein Gegenstand ausgerüstet oder verwendet wird und er einem Attribut einen Bonus hinzufügt, müssen Sie eine Bonusinstanz des entsprechenden Typs erstellen und diese dann dem Attribut des Charakters hinzufügen. Danach einfach den endgültigen Attributwert neu berechnen.

Sie können auch die verschiedenen verfügbaren Boniarten erweitern. Beispielsweise könnten Sie einen Bonus haben, der den Mehrwert oder den Multiplikator im Laufe der Zeit ändert. Sie können auch negative Boni verwenden (die der aktuelle Code bereits verarbeiten kann).

Bei jedem System können Sie immer mehr hinzufügen. Hier sind einige Verbesserungsvorschläge, die Sie vornehmen könnten:

  • Identifizieren Sie Attribute anhand von Namen
  • Erstellen Sie ein "zentralisiertes" System zur Verwaltung der Attribute
  • Optimieren Sie die Leistung (Hinweis: Sie müssen den endgültigen Wert nicht immer vollständig berechnen).
  • Ermöglichen Sie, dass einige Boni andere Boni abschwächen oder verstärken

Danke fürs Lesen!