Het Composite Design Pattern gebruiken voor een RPG Attributes-systeem

Intelligentie, Wilskracht, Charisma, Wijsheid: behalve dat het belangrijke eigenschappen zijn die je als gameontwikkelaar zou moeten hebben, zijn dit ook gemeenschappelijke attributen die in RPG's worden gebruikt. Het berekenen van de waarden van dergelijke attributen - het toepassen van getimede bonussen en het rekening houden met het effect van uitgeruste items - kan lastig zijn. In deze zelfstudie laat ik je zien hoe je een enigszins gewijzigd samengesteld patroon kunt gebruiken om hiermee om te gaan, on the fly.

Notitie: Hoewel deze tutorial geschreven is met behulp van Flash en AS3, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Invoering

Attributes-systemen worden heel vaak gebruikt in RPG's om de sterke en zwakke punten en capaciteiten van de personages te kwantificeren. Als je er niet bekend mee bent, scheur dan op de Wikipedia-pagina voor een fatsoenlijk overzicht.

Om hen dynamischer en interessanter te maken, verbeteren ontwikkelaars deze systemen vaak door vaardigheden, items en andere dingen toe te voegen die van invloed zijn op de kenmerken. Als je dit wilt doen, heb je een goed systeem nodig dat de uiteindelijke attributen kan berekenen (rekening houdend met elk ander effect) en omgaan met de toevoeging of verwijdering van verschillende soorten bonussen.

In deze zelfstudie zullen we een oplossing voor dit probleem onderzoeken door een enigszins aangepaste versie van het composietontwerppatroon te gebruiken. Onze oplossing zal in staat zijn om bonussen af ​​te handelen en zal werken op elke reeks attributen die u definieert.


Wat is het composietpatroon?

Dit gedeelte is een overzicht van het composietontwerppatroon. Als u er al bekend mee bent, wilt u misschien overslaan Ons probleem modelleren.

Het samengestelde patroon is een ontwerppatroon (een bekende, herbruikbare, algemene ontwerpsjabloon) om iets groots onder te verdelen in kleinere objecten, om een ​​grotere groep te maken door alleen de kleine objecten te hanteren. Het maakt het gemakkelijk om grote hoeveelheden informatie in kleinere, gemakkelijk te verwerken brokken te breken. In wezen is het een sjabloon voor het gebruik van een groep van een bepaald object alsof het een enkel object zelf was.

We gaan een veelgebruikt voorbeeld gebruiken om dit te illustreren: denk aan een eenvoudige tekenapplicatie. Je wilt dat je driehoeken, vierkanten en cirkels kunt tekenen en ze anders kunt behandelen. Maar u wilt ook dat het groepen tekeningen kan verwerken. Hoe kunnen we dat gemakkelijk doen?

Het composietpatroon is de perfecte kandidaat voor deze klus. Door een 'groep tekeningen' als een tekening zelf te behandelen, zou je gemakkelijk elke tekening binnen deze groep kunnen toevoegen, en de groep als geheel zou nog steeds als een enkele tekening worden gezien.

Wat betreft programmeren zouden we één basisklasse hebben, Tekening, die het standaardgedrag van een tekening heeft (je kunt het verplaatsen, lagen wijzigen, roteren, enzovoort) en vier subklassen, Driehoek, Plein, Cirkel en Groep.

In dit geval hebben de eerste drie klassen een eenvoudig gedrag, waarbij alleen de gebruikersinvoer van de basiskenmerken van elke vorm vereist is. De Groep klasse zal echter methoden hebben voor het toevoegen en verwijderen van vormen, evenals een bewerking op alle vormen (bijvoorbeeld de kleur van alle vormen in een groep tegelijk wijzigen). Alle vier subklassen zouden nog steeds worden behandeld als een Tekening, dus u hoeft zich geen zorgen te maken over het toevoegen van specifieke code voor wanneer u in een groep wilt werken.

Om dit beter weer te geven, kunnen we elke tekening als een knooppunt in een boom bekijken. Elk knooppunt is een blad, behalve voor Groep knooppunten, die kinderen kunnen hebben - die op hun beurt tekeningen binnen die groep.


Een visuele weergave van het patroon

Vasthouden aan het voorbeeld van de teken-app, dit is een visuele weergave van de "tekenapplicatie" waar we aan dachten. Merk op dat er drie tekeningen in de afbeelding zijn: een driehoek, een vierkant en een groep bestaande uit een cirkel en een vierkant:

En dit is de boomstructuur van de huidige scène (de grondtoon is de fase van de tekenapplicatie):

Wat als we een andere tekening wilden toevoegen, een groep van een driehoek en een cirkel, binnen de groep die we momenteel hebben? We zouden het gewoon toevoegen omdat we elke tekening binnen een groep zouden toevoegen. Dit is hoe de visuele weergave eruit zou zien:

En dit is wat de boom zou worden:

Stel je nu voor dat we een oplossing gaan bouwen voor het probleem met kenmerken dat we hebben. Het is duidelijk dat we geen directe visuele representatie zullen hebben (we kunnen alleen het eindresultaat zien, wat het berekende attribuut is, gegeven de onbewerkte waarden en de bonussen), dus we zullen gaan nadenken in het samengestelde patroon met de boomrepresentatie..


Ons probleem modelleren

Om het mogelijk te maken onze kenmerken in een boom te modelleren, moeten we elk kenmerk in de kleinste delen die we kunnen breken.

We weten dat we bonussen hebben, die een onbewerkte waarde aan het kenmerk kunnen toevoegen of het met een percentage kunnen verhogen. Er zijn bonussen die aan het attribuut toevoegen en andere die worden berekend nadat al die eerste bonussen zijn toegepast (bijvoorbeeld bonussen uit vaardigheden).

Dus we kunnen hebben:

  • Raw-bonussen (toegevoegd aan de onbewerkte waarde van het kenmerk)
  • Laatste bonussen (toegevoegd aan het attribuut nadat al het andere is berekend)

Je hebt misschien gemerkt dat we geen bonussen scheiden die een waarde toevoegen aan het attribuut van bonussen die het attribuut met een percentage verhogen. Dat komt omdat we elke bonus modelleren om op hetzelfde moment te kunnen veranderen. Dit betekent dat we een bonus kunnen hebben die 5 aan de waarde toevoegt en verhoogt het kenmerk met 10%. Dit wordt allemaal in de code behandeld.

Deze twee soorten bonussen zijn slechts de bladeren van onze boom. Ze lijken op het Driehoek, Plein en Cirkel klassen in ons voorbeeld van voordien.

We hebben nog steeds geen entiteit gecreëerd die als een groep zal dienen. Deze entiteiten zullen de attributen zelf zijn! De Groep klasse in ons voorbeeld zal eenvoudigweg het attribuut zelf zijn. Dus we zullen een hebben Attribuut klasse die zich zal gedragen als ieder attribuut.

Dit is hoe een attribuutboom eruit zou kunnen zien:

Nu alles is bepaald, zullen we onze code beginnen?


De basisklassen maken

We gebruiken ActionScript 3.0 als de taal voor de code in deze zelfstudie, maar maak je geen zorgen! De code zal nadien volledig worden becommentarieerd en alles wat uniek is voor de taal (en het Flash-platform) zal worden uitgelegd en er zullen alternatieven worden geboden - dus als u bekend bent met een OOP-taal, kunt u dit volgen tutorial zonder problemen.

De eerste klasse die we moeten maken, is de basisklasse voor elk attribuut en bonussen. Het bestand wordt gebeld BaseAttribute.as, en het maken ervan is heel eenvoudig. Hier is de code, met opmerkingen achteraf:

 package public class BaseAttribute private var _baseValue: int .; privé var _baseMultiplier: Number; openbare functie BaseAttribute (value: int, multiplier: Number = 0) _baseValue = waarde; _baseMultiplier = multiplier;  public function get baseValue (): int return _baseValue;  public function get baseMultiplier (): Number return _baseMultiplier; 

Zoals je ziet, zijn de dingen heel eenvoudig in deze basisklasse. We maken gewoon de _waarde en _multiplier velden, wijs ze toe aan de constructor en maak twee gettermethoden, één voor elk veld.

Nu moeten we het maken RawBonus en FinalBonus klassen. Dit zijn eenvoudig subklassen van BaseAttribute, met niets toegevoegd. Je kunt het zo vaak uitbreiden als je wilt, maar voor nu maken we alleen deze twee lege subklassen van BaseAttribute:

RawBonus.as:

 package public class RawBonus breidt BaseAttribute uit openbare functie RawBonus (waarde: int = 0, multiplier: Number = 0) super (value, multiplier); 

FinalBonus.as:

 package public class FinalBonus breidt BaseAttribute uit public function FinalBonus (value: int = 0, multiplier: Number = 0) super (value, multiplier); 

Zoals je kunt zien, hebben deze klassen niets anders dan een constructeur.


De kenmerkklasse

De Attribuut klasse is het equivalent van een groep in het samengestelde patroon. Het kan alle onbewerkte of definitieve bonussen bevatten en heeft een methode voor het berekenen van de definitieve waarde van het attribuut. Omdat het een subklasse is van BaseAttribute, de _baseValue veld van de klasse is de beginwaarde van het kenmerk.

Bij het maken van de klasse hebben we een probleem bij het berekenen van de uiteindelijke waarde van het kenmerk: omdat we geen ruwe bonussen van de uiteindelijke bonussen scheiden, kunnen we de definitieve waarde niet berekenen, omdat we niet weten wanneer pas elke bonus toe.

Dit kan worden opgelost door een kleine wijziging aan te brengen in het basis samengestelde patroon. In plaats van het toevoegen van een kind aan dezelfde "container" binnen de groep, zullen we twee "containers" creëren, een voor de onbewerkte bonussen en andere voor de laatste bonussen. Elke bonus zal nog steeds een kind zijn van Attribuut, maar zal op verschillende plaatsen staan ​​om de uiteindelijke waarde van het attribuut te berekenen.

Met dat uitgelegd, laten we naar de code gaan!

 package public class Attribute breidt BaseAttribute uit private var _rawBonuses: Array; private var _finalBonuses: Array; private var _finalValue: int; public function Attribute (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  openbare functie addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  openbare functie addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  public function removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  openbare functie removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  public function calculateValue (): int _finalValue = baseValue; // Waarde toevoegen van onbewerkte var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; voor elke (var-bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Waarde toevoegen aan laatste var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; voor elke (var-bonus: FinalBonus in _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); return _finalValue;  public function get finalValue (): int return calculateValue (); 

De methodes addRawBonus (), addFinalBonus (), removeRawBonus () en removeFinalBonus () zijn heel duidelijk. Het enige wat ze doen is het toevoegen of verwijderen van hun specifieke bonustype aan of van de array die alle bonussen van dat type bevat.

Het lastige deel is het calculateValue () methode. Eerst worden alle waarden samengevat die de onbewerkte bonussen toevoegen aan het kenmerk en worden ook alle vermenigvuldigers samengevat. Hierna wordt de som van alle onbewerkte bonuswaarden toegevoegd aan het beginattribuut en wordt de multiplier toegepast. Later wordt dezelfde stap uitgevoerd voor de laatste bonussen, maar deze keer worden de waarden en vermenigvuldigers toegepast op de halfberekende uiteindelijke kenmerkwaarde.

En we zijn klaar met de structuur! Bekijk de volgende stappen om te zien hoe u deze zou gebruiken en uitbreiden.


Extra gedrag: getimede bonussen

In onze huidige structuur hebben we alleen eenvoudige onbewerkte en definitieve bonussen, die op dit moment helemaal geen verschil hebben. In deze stap zullen we extra gedrag toevoegen aan de FinalBonus klasse, om het meer op bonussen te doen lijken die door zouden worden toegepast actief vaardigheden in een spel.

Aangezien, zoals de naam al aangeeft, dergelijke vaardigheden slechts gedurende een bepaalde periode actief zijn, zullen we een timinggedrag toevoegen aan de laatste bonussen. De onbewerkte bonussen kunnen bijvoorbeeld worden gebruikt voor bonussen die worden toegevoegd via apparatuur.

Om dit te doen, zullen we de timer klasse. Deze klasse is native van ActionScript 3.0 en alles wat deze doet, gedraagt ​​zich als een timer, beginnend bij 0 seconden en vervolgens na een opgegeven hoeveelheid tijd een opgegeven functie aanroept, terug naar 0 herstelt en de teller opnieuw start, totdat deze de opgegeven waarde bereikt aantal tellingen opnieuw. Als u ze niet opgeeft, de timer blijft draaien totdat je het stopt. U kunt kiezen wanneer de timer start en wanneer deze stopt. U kunt zijn gedrag eenvoudig repliceren door de distributiesystemen van uw taal te gebruiken met de juiste extra code, indien nodig.

Laten we naar de code springen!

 pakket import flash.events.TimerEvent; import flash.utils.Timer; public class FinalBonus breidt BaseAttribute uit private var _timer: Timer; privé var _parent: kenmerk; public function FinalBonus (time: int, value: int = 0, multiplier: Number = 0) super (value, multiplier); _timer = nieuwe Timer (tijd); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  public function startTimer (parent: Attribute): void _parent = parent; _timer.start ();  private function onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (deze); 

In de constructor is het eerste verschil dat de laatste bonussen nu een a vereisen tijd parameter, die laat zien hoe lang ze duren. In de constructor creëren we een timer voor die tijd (ervan uitgaande dat de tijd in milliseconden is) en voeg er een gebeurtenislistener aan toe.

(Gebeurtenislisteners zijn feitelijk wat ervoor zorgt dat de timer de juiste functie aanroept wanneer het die bepaalde tijdsperiode bereikt - in dit geval is de functie die moet worden aangeroepen onTimerEnd ().)

Merk op dat we de timer nog niet hebben gestart. Dit wordt gedaan in de startTimer () methode, die ook een parameter vereist, ouder, dat moet een zijn Attribuut. Voor deze functie is het attribuut vereist dat de bonus toevoegt om die functie aan te roepen om deze te activeren; op zijn beurt start dit de timer en vertelt de bonus welk exemplaar moet worden gevraagd om de bonus te verwijderen wanneer de timer zijn limiet heeft bereikt.

Het verwijderingsgedeelte wordt gedaan in de onTimerEnd () methode, die de ingestelde ouder zal vragen om deze te verwijderen en de timer te stoppen.

Nu kunnen we uiteindelijke bonussen gebruiken als getimede bonussen, wat aangeeft dat ze slechts een bepaalde tijd zullen duren.


Extra gedrag: afhankelijke kenmerken

Een ding dat vaak wordt gezien in RPG-games zijn attributen die afhankelijk zijn van anderen. Laten we bijvoorbeeld het attribuut 'attack speed' nemen. Het is niet alleen afhankelijk van het type wapen dat je gebruikt, maar ook bijna altijd van de handigheid van het personage.

In ons huidige systeem laten we alleen bonussen toe om kinderen van te zijn Attribuut instances. Maar in ons voorbeeld moeten we een attribuut een kind van een ander attribuut laten zijn. Hoe kunnen we dat doen? We kunnen een subklasse van maken Attribuut, riep DependantAttribute, en geef deze subklasse al het gedrag dat we nodig hebben.

Het toevoegen van kenmerken als kinderen is heel eenvoudig: alles wat we moeten doen is een andere array maken om attributen vast te houden en specifieke code toevoegen voor het berekenen van het uiteindelijke attribuut. Omdat we niet weten of elk attribuut op dezelfde manier zal worden berekend (misschien wilt u eerst handigheid gebruiken om de aanvalsnelheid te veranderen, en dan de bonussen controleren, maar eerst bonussen gebruiken om een ​​magische aanval te veranderen en dan bijvoorbeeld te gebruiken intelligentie), zullen we ook de berekening van het definitieve attribuut in de Attribuut klasse in verschillende functies. Laten we dat eerst doen.

In Attribute.as:

 package public class Attribute breidt BaseAttribute uit private var _rawBonuses: Array; private var _finalBonuses: Array; beschermd var _finalValue: int; public function Attribute (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  openbare functie addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  openbare functie addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  public function removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  openbare functie removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  beschermde functie applyRawBonuses (): void // Waarde toevoegen van onbewerkte var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; voor elke (var-bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  beschermde functie applyFinalBonuses (): void // Waarde toevoegen aan laatste var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; voor elke (var-bonus: RawBonus in _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  public function calculateValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue;  public function get finalValue (): int return calculateValue (); 

Zoals je kunt zien aan de gemarkeerde regels, was alles wat we deden creëren applyRawBonuses () en applyFinalBonuses () en noem ze bij het berekenen van het laatste attribuut in calculateValue (). We hebben ook gemaakt _finalValue beschermd, zodat we dit in de subklassen kunnen wijzigen.

Nu is alles ingesteld om de. Te creëren DependantAttribute klasse! Hier is de code:

 package public class DependantAttribute extends Attribute protected var _otherAttributes: Array; public function DependantAttribute (startingValue: int) super (startingValue); _otherAttributes = [];  public function addAttribute (attr: Attribute): void _otherAttributes.push (attr);  public function removeAttribute (attr: Attribute): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  openbare override-functie calculationValue (): int // Specifieke attribuutcode gaat hier ergens naartoe _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In deze klasse, de attribuut toevoegen() en removeAttribute () functies moeten u bekend voorkomen. Je moet aandacht besteden aan de overbodigheid calculateValue () functie. Hier gebruiken we de attributen niet voor het berekenen van de definitieve waarde - je moet het voor elk afhankelijk attribuut doen!

Dit is een voorbeeld van hoe je dat zou doen voor het berekenen van de aanvalsnelheid:

 package public class AttackSpeed ​​breidt DependantAttribute uit openbare functie AttackSpeed ​​(startingValue: int) super (startingValue);  public override function calculateValue (): int _finalValue = baseValue; // Elke 5 punten in handigheid voegt 1 toe aan aanvalsnelheid var-behendigheid: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (handigheid / 5); applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In deze klasse gaan we ervan uit dat je het kenmerk handigheid al hebt toegevoegd als een kind van Aanval snelheid, en dat het de eerste is in de _otherAttributes array (dat zijn een hoop aannames om te maken, bekijk de conclusie voor meer info). Na het ophalen van de behendigheid gebruiken we het eenvoudig om meer toe te voegen aan de uiteindelijke waarde van de aanvalsnelheid.


Conclusie

Hoe zou je deze structuur in een game gebruiken als alles klaar is? Het is heel simpel: het enige dat u hoeft te doen, is om verschillende attributen te maken en ze elk een toe te wijzen Attribuut aanleg. Hierna gaat het allemaal om het toevoegen en verwijderen van bonussen via de reeds gemaakte methoden.

Wanneer een item is uitgerust of wordt gebruikt en het een bonus toevoegt aan een attribuut, moet u een bonusinstantie van het overeenkomstige type maken en dit vervolgens toevoegen aan het attribuut van het teken. Bereken daarna eenvoudig de definitieve attribuutwaarde.

Je kunt ook de verschillende soorten beschikbare bonussen uitbreiden. U kunt bijvoorbeeld een bonus hebben die de toegevoegde waarde of vermenigvuldiger in de loop van de tijd verandert. Je kunt ook negatieve bonussen gebruiken (die de huidige code al aankan).

Met elk systeem is er altijd meer dat u kunt toevoegen. Hier zijn een paar voorgestelde verbeteringen die u kunt aanbrengen:

  • Identificeer attributen op naam
  • Maak een "gecentraliseerd" systeem voor het beheer van de kenmerken
  • Optimaliseer de prestaties (hint: u hoeft niet altijd de definitieve waarde volledig te berekenen)
  • Maak het mogelijk voor sommige bonussen om andere bonussen te verzachten of te versterken

Bedankt voor het lezen!