Använda kompositdesignmönstret för ett RPG Attributesystem

Intelligens, viljestyrka, karisma, visdom: förutom att vara viktiga egenskaper du borde ha som spelutvecklare är dessa också vanliga attribut som används i rollspel. Beräkning av värdena på sådana attribut - tillämpning av tidsbegränsade bonusar och med hänsyn till effekten av utrustade föremål - kan vara svårt. I den här handledningen visar jag dig hur man använder ett lite modifierat kompositmönster för att hantera detta på flyg.

Notera: Även om denna handledning skrivs med Flash och AS3, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.


Introduktion

Attributssystem används ofta i RPGs för att kvantifiera teckenens styrkor, svagheter och förmågor. Om du inte känner till dem, skumma Wikipedia-sidan för en anständig översikt.

För att göra dem mer dynamiska och intressanta förbättrar utvecklarna ofta dessa system genom att lägga till färdigheter, objekt och andra saker som påverkar attributen. Om du vill göra detta behöver du ett bra system som kan beräkna de slutliga attributen (med hänsyn till alla andra effekter) och hantera tillägg eller borttagning av olika typer av bonusar.

I denna handledning utforskar vi en lösning för detta problem med hjälp av en lite modifierad version av kompositdesignmönstret. Vår lösning kommer att kunna hantera bonusar och kommer att fungera på alla uppsättningar attribut du definierar.


Vad är det sammansatta mönstret?

Detta avsnitt är en översikt över kompositdesignmönstret. Om du redan är bekant med det kanske du vill hoppa över till Modellerar vårt problem.

Kompositmönstret är ett designmönster (en välkänd, återanvändbar, generell designmall) för att dela upp något stort i mindre objekt, för att skapa en större grupp genom att hantera endast små objekt. Det gör det enkelt att bryta stora bitar av information till mindre, lättare behandlingsbara bitar. I huvudsak är det en mall för att använda en grupp av ett visst objekt som om det var ett enda objekt i sig.

Vi ska använda ett mycket använt exempel för att illustrera detta: Tänk på en enkel ritningsapplikation. Du vill att den ska låta dig rita trianglar, kvadrater och cirklar och behandla dem på olika sätt. Men du vill också att det ska kunna hantera grupper av ritningar. Hur kan vi enkelt göra det?

Kompositmönstret är den perfekta kandidaten för det här jobbet. Genom att behandla en "grupp av ritningar" som en ritning kan man enkelt lägga till någon ritning inuti denna grupp, och gruppen som helhet skulle fortfarande ses som en enstaka ritning.

När det gäller programmering skulle vi ha en basklass, Ritning, som har standardbeteenden på en ritning (du kan flytta runt, ändra lager, rotera det osv.) och fyra underklasser, Triangel, Fyrkant, Cirkel och Grupp.

I det här fallet kommer de tre första klasserna att ha ett enkelt beteende, vilket endast kräver användarens inmatning av de grundläggande attributen för varje form. De Grupp klassen kommer emellertid att ha metoder för att lägga till och ta bort former, liksom att göra en operation på alla (t.ex. byta färg på alla former i en grupp i taget). Alla fyra underklassen skulle fortfarande behandlas som en Ritning, så du behöver inte oroa dig för att lägga till specifik kod för när du vill arbeta i en grupp.

För att få detta till en bättre representation kan vi se varje ritning som en nod i ett träd. Varje nod är ett löv, förutom Grupp noder, som kan ha barn - som i sin tur är ritningar inom den gruppen.


En visuell representation av mönstret

Klibbar med teckningsappemplet, det här är en visuell representation av "ritningsapplikationen" som vi tänkte på. Observera att det finns tre teckningar i bilden: en triangel, en kvadrat och en grupp som består av en cirkel och en kvadrat:

Och detta är trädrepresentationen för den aktuella scenen (roten är ritningsapplikationsstadiet):

Vad händer om vi vill lägga till en annan ritning, vilken är en grupp av en triangel och en cirkel, inne i gruppen vi för närvarande har? Vi skulle bara lägga till det som vi skulle lägga till någon ritning inom en grupp. Så här ser den visuella representationen ut:

Och det här är vad trädet skulle bli:

Tänk nu att vi ska bygga en lösning på attributen problem vi har. Självklart kommer vi inte att få en direkt visuell representation (vi kan bara se slutresultatet, vilket är det beräknade attributet med de råa värdena och bonusarna), så vi börjar tänka i kompositmönstret med trädrepresentationen.


Modellerar vårt problem

För att göra det möjligt att modellera våra attribut i ett träd måste vi bryta varje attribut till de minsta delarna vi kan.

Vi vet att vi har bonusar, som antingen kan lägga till ett råvärde för attributet eller öka det med en procentandel. Det finns bonusar som lägger till attributet, och andra som beräknas efter alla de första bonusarna tillämpas (till exempel bonusar från färdigheter).

Så vi kan ha:

  • Rå bonusar (tillagt attributets råvärde)
  • Slutliga bonusar (läggs till i attributet efter allt annat har beräknats)

Du kanske har märkt att vi inte skiljer bonusar som lägger till ett värde för attributet från bonusar som ökar attributet med en procentandel. Det beror på att vi modellerar varje bonus för att kunna ändra på samma gång. Det betyder att vi kan få en bonus som lägger till 5 till värdet och ökar attributet med 10%. Detta kommer alla att hanteras i koden.

Dessa två slags bonusar är bara våra träds löv. De är ganska mycket som Triangel, Fyrkant och Cirkel klasser i vårt exempel från tidigare.

Vi har fortfarande inte skapat en enhet som ska fungera som en grupp. Dessa enheter kommer att vara attributen själva! De Grupp klass i vårt exempel blir helt enkelt själva attributet. Så vi kommer ha en Attribut klass som kommer att beter sig som några attribut.

Så här kan ett attributträd se ut:

Nu när allt är bestämt, ska vi starta vår kod?


Skapa basklasserna

Vi kommer att använda ActionScript 3.0 som språket för koden i denna handledning, men oroa dig inte! Koden kommer att kommenteras efteråt, och allt som är unikt för språket (och Flash-plattformen) kommer att förklaras och alternativ kommer att ges - så om du är bekant med något OOP-språk kommer du att kunna följa detta handledning utan problem.

Den första klassen vi behöver skapa är basklassen för alla attribut och bonusar. Filen kommer att ringas BaseAttribute.as, och skapa det är väldigt enkelt. Här är koden, med kommentarer efteråt:

 paket public class BaseAttribute private var _baseValue: int; privat var _baseMultiplier: Number; offentlig funktion BaseAttribute (värde: int, multiplikator: Number = 0) _baseValue = value; _baseMultiplier = multiplikator;  allmän funktion få basValue (): int return _baseValue;  allmän funktion få baseMultiplier (): Number return _baseMultiplier; 

Som du kan se är det mycket enkelt i denna basklass. Vi skapar bara _värde och _multiplikator fält, tilldela dem i konstruktören och gör två getter-metoder, en för varje fält.

Nu behöver vi skapa RawBonus och FinalBonus klasser. Dessa är helt enkelt underklasser av BaseAttribute, med ingenting tillagt Du kan expandera på det så mycket du vill, men för nu kommer vi bara att göra dessa två tomma underklasser av BaseAttribute:

RawBonus.as:

 paket public class RawBonus utökar BaseAttribute public function RawBonus (värde: int = 0, multiplikator: Number = 0) super (värde, multiplikator); 

FinalBonus.as:

 paket public class FinalBonus utökar BaseAttribute public function FinalBonus (värde: int = 0, multiplikator: Number = 0) super (värde, multiplikator); 

Som du kan se har dessa klasser inget i dem utan en konstruktör.


Attributsklassen

De Attribut klassen motsvarar en grupp i kompositmönstret. Det kan innehålla eventuella råa eller slutliga bonusar, och kommer att ha en metod för att beräkna det slutliga värdet av attributet. Eftersom det är en underklass av BaseAttribute, de _baseValue klassens fält kommer att vara startvärdet för attributet.

När vi skapar klassen kommer vi att ha problem vid beräkningen av attributets slutliga värde: Eftersom vi inte skiljer råa bonusar från slutliga bonusar, kan vi inte beräkna det slutliga värdet eftersom vi inte vet när tillämpa varje bonus.

Detta kan lösas genom att göra en liten modifikation av det grundläggande kompositmönstret. I stället för att lägga till ett barn till samma "behållare" inom gruppen skapar vi två "behållare", en för de rika bonusarna och andra för de slutliga bonusarna. Varje bonus kommer fortfarande vara ett barn av Attribut, men kommer att finnas på olika ställen för att tillåta beräkningen av attributets slutliga värde.

Med det förklarade, låt oss komma till koden!

 paket public class Attributet utökar BaseAttribute private var _rawBonuses: Array; privat var _finalBonuses: Array; privat var _finalValue: int; allmän funktion Attribut (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  allmän funktion addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  allmän funktion addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  allmän funktion removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  allmän funktion removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  allmän funktion beräknaValue (): int _finalValue = baseValue; // Att lägga till värde från raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; för varje (var bonus: RawBonus i _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Lägger till värdet från final var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; för varje (var bonus: FinalBonus i _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); returnera _finalValue;  allmän funktion få finalValue (): int return calculateValue (); 

Metoderna addRawBonus (), addFinalBonus (), removeRawBonus () och removeFinalBonus () är mycket tydliga. Allt de gör är att lägga till eller ta bort deras specifika bonus typ till eller från arrayen som innehåller alla bonusar av den typen.

Den knepiga delen är calculateValue () metod. För det första sammanfattas alla de värden som de obehandlade bonusarna lägger till attributet, och summerar också alla multiplikatorer. Därefter läggs summan av alla råbonusvärden till startattributet och applicerar sedan multiplikatorn. Senare gör det samma steg för de slutliga bonusarna, men denna gång appliceras värdena och multiplikatorerna till det halvkalkylerade slutliga attributvärdet.

Och vi är klara med strukturen! Kontrollera nästa steg för att se hur du skulle använda och förlänga det.


Extra uppförande: Timed Bonuses

I vår nuvarande struktur har vi bara enkla råa och slutliga bonusar, som för närvarande inte har någon skillnad alls. I detta steg lägger vi till extra beteende för FinalBonus klass, för att få det att se ut som bonusar som skulle tillämpas genom aktiva färdigheter i ett spel.

Eftersom, som namnet antyder, sådana färdigheter endast är aktiva under en viss tid, lägger vi till ett tidsbeteende beteende på de slutliga bonusarna. De obehandlade bonusarna kan till exempel användas för bonusar som läggs till genom utrustning.

För att göra detta ska vi använda Timer klass. Den här klassen är inbyggd från ActionScript 3.0 och allt som gör det är att fungera som en timer, börjar vid 0 sekunder och sedan ringer en angiven funktion efter en viss tid, återställer sig till 0 och startar räkningen igen tills den når den angivna antal räkningar igen. Om du inte anger dem, ska Timer kommer fortsätta springa tills du stoppar det. Du kan välja när timern startar och när den stannar. Du kan replikera sitt beteende helt enkelt genom att använda ditt språks timningssystem med lämplig extra kod, om det behövs.

Låt oss hoppa till koden!

 paket import flash.events.TimerEvent; importera flash.utils.Timer; public class FinalBonus utökar BaseAttribute private var _timer: Timer; privat var _parent: Attribut; offentlig funktion FinalBonus (tid: int, värde: int = 0, multiplikator: Nummer = 0) super (värde, multiplikator); _timer = ny Timer (tid); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  allmän funktion startTimer (förälder: Attribut): void _parent = förälder; _timer.start ();  privat funktion onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (detta); 

I konstruktören är den första skillnaden att slutliga bonusar kräver nu a tid parameter som visar hur länge de går. Inuti konstruktören skapar vi en Timer för den tidsperioden (förutsatt att tiden är i millisekunder) och lägg till en händelseloggare till den.

(Händelselyttare är i grund och botten vad som gör att timern ringer rätt funktion när den når den vissa tiden - i det här fallet är funktionen som ska ringas onTimerEnd ().)

Observera att vi inte har startat timern ännu. Detta görs i startTimer () metod som också kräver en parameter, förälder, vilket måste vara en Attribut. Den här funktionen kräver attributet som lägger till bonusen för att ringa den funktionen för att aktivera den. I sin tur startar detta tidtabellen och berättar bonusen vilken instans som begär att ta bort bonusen när timern har nått sin gräns.

Avlägsnande delen görs i onTimerEnd () metod som bara frågar den uppsatta föräldern för att ta bort den och stoppa timern.

Nu kan vi använda slutliga bonusar som tidsbestämda bonusar, vilket indikerar att de bara kommer att vara en viss tid.


Extra uppförande: beroende attribut

En sak som vanligtvis ses i RPG-spel är attribut som beror på andra. Låt oss ta till exempel attributet "attack speed". Det är inte bara beroende av vilken typ av vapen du använder, men nästan alltid på karaktärens fingerfärdighet också.

I vårt nuvarande system tillåter vi bara bonusar att vara barn till Attribut instanser. Men i vårt exempel måste vi låta ett attribut vara ett barn av ett annat attribut. Hur kan vi göra det? Vi kan skapa en underklass av Attribut, kallad DependantAttribute, och ge detta underklass allt det beteende vi behöver.

Att lägga till attribut som barn är väldigt enkelt: allt vi behöver göra är att skapa en annan array för att hålla attribut och lägga till en specifik kod för att beräkna den slutliga attributet. Eftersom vi inte vet huruvida varje attribut kommer att beräknas på samma sätt (du kanske vill använda fingerfärdighet för att ändra attackhastigheten och kontrollera bonusarna, men använd först bonusar för att ändra magisk attack och använd till exempel, intelligens) måste vi också skilja beräkningen av det slutliga attributet i Attribut klass i olika funktioner. Låt oss göra det först.

I Attribute.as:

 paket public class Attributet utökar BaseAttribute private var _rawBonuses: Array; privat var _finalBonuses: Array; skyddad var _finalValue: int; allmän funktion Attribut (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  allmän funktion addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  allmän funktion addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  allmän funktion removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  offentlig funktion removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  skyddad funktion applyRawBonuses (): void // Mervärde från raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; för varje (var bonus: RawBonus i _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  skyddad funktion applyFinalBonuses (): void // Mervärde från final var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; för varje (var bonus: RawBonus i _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  allmän funktion beräknaValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returnera _finalValue;  allmän funktion få finalValue (): int return calculateValue (); 

Som du kan se av de markerade linjerna skapades allt vi gjorde applyRawBonuses () och applyFinalBonuses () och ring dem när du beräknar den slutliga attributet i calculateValue (). Vi har också gjort _finalValue skyddad, så vi kan ändra det i underklassen.

Nu är allt inställt för oss att skapa DependantAttribute klass! Här är koden:

 paket public class DependantAttribute utökar Attribut protected var _otherAttributes: Array; offentlig funktion DependantAttribute (startingValue: int) super (startingValue); _otherAttributes = [];  offentlig funktion addAttribute (attr: Attribut): void _otherAttributes.push (attr);  offentlig funktion removeAttribute (attr: Attribut): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  public override-funktionen beräknaValue (): int // Specifika attributkoden går någonstans här _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returnera _finalValue; 

I denna klass, den addAttribute () och removeAttribute () funktionerna bör vara bekanta för dig. Du måste vara uppmärksam på överklagandet calculateValue () fungera. Här använder vi inte attributen för att beräkna det slutliga värdet - du måste göra det för varje beroende attribut!

Detta är ett exempel på hur du skulle göra det för att beräkna attackhastigheten:

 paket public class AttackSpeed ​​förlänger DependantAttribute public function AttackSpeed ​​(startingValue: int) super (startingValue);  public override-funktionen beräknaValue (): int _finalValue = baseValue; // Varje 5 poäng i fingerfärdighet lägger till 1 till attackhastighet var fingerfärdighet: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (fingerfärdighet / 5); applyRawBonuses (); applyFinalBonuses (); returnera _finalValue; 

I den här klassen antar vi att du redan har lagt till fingerfärdighetsattributet som ett barn av Attackhastighet, och att det är det första i _otherAttributes array (det är många antaganden att göra, kolla slutsatsen för mer info). Efter att ha hämtat fingerfärdigheten använder vi det helt enkelt för att lägga till mer än det slutliga värdet av attackhastigheten.


Slutsats

När allt är klart, hur skulle du använda den här strukturen i ett spel? Det är väldigt enkelt: allt du behöver göra är att skapa olika attribut och tilldela dem en Attribut exempel. Därefter handlar det om att lägga till och ta bort bonusar till det genom de redan skapade metoderna.

När ett objekt är utrustat eller används och det lägger till en bonus för ett attribut måste du skapa en bonusinstans av motsvarande typ och sedan lägga till den i teckens attributet. Därefter omräknar du bara det slutliga attributvärdet.

Du kan också expandera på olika typer av bonusar som är tillgängliga. Till exempel kan du få en bonus som ändrar mervärde eller multiplikatorn över tiden. Du kan också använda negativa bonusar (som den nuvarande koden redan kan hantera).

Med något system finns det alltid mer du kan lägga till. Här är några förslag till förbättringar som du kan göra:

  • Identifiera attribut med namn
  • Gör ett "centraliserat" system för att hantera attributen
  • Optimera prestanda (tips: du behöver inte alltid beräkna det slutliga värdet helt)
  • Gör det möjligt för några bonusar att dämpa eller stärka andra bonusar

Tack för att du läser!