Bruke Composite Design Pattern for et RPG Attributes System

Intelligens, viljestyrke, karisma, visdom: foruten å være viktige kvaliteter du burde ha som spillutvikler, er disse også vanlige attributter som brukes i RPGs. Beregning av verdiene for slike attributter - bruk av tidsbestemte bonuser og hensyntagen til effekten av utstyrte elementer - kan være vanskelig. I denne veiledningen vil jeg vise deg hvordan du bruker et litt modifisert komposittmønster for å håndtere dette, på fly.

Merk: Selv om denne opplæringen er skrevet med Flash og AS3, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Introduksjon

Attributtsystemer er svært vanlig brukt i RPGs for å kvantifisere karakterer, sterke svakheter og evner. Hvis du ikke er kjent med dem, skum Wikipedia-siden for en anstendig oversikt.

For å gjøre dem mer dynamiske og interessante, forbedrer utviklerne ofte disse systemene ved å legge til ferdigheter, elementer og andre ting som påvirker attributter. Hvis du vil gjøre dette, trenger du et godt system som kan beregne de endelige egenskapene (ta hensyn til alle andre effekter) og håndtere tillegg eller fjerning av ulike typer bonuser.

I denne opplæringen vil vi utforske en løsning for dette problemet ved å bruke en litt modifisert versjon av komposittdesignmønsteret. Vår løsning vil kunne håndtere bonuser, og vil fungere på et hvilket som helst sett med attributter du definerer.


Hva er komposittmønsteret?

Denne delen er en oversikt over komposittdesignmønsteret. Hvis du allerede er kjent med det, vil du kanskje hoppe over til Modellering av vårt problem.

Komposittmønsteret er et designmønster (en kjent, gjenbrukbar, generell designmal) for å dele noe stort inn i mindre gjenstander for å skape en større gruppe ved å håndtere bare de små gjenstandene. Det gjør det enkelt å knuse store biter av informasjon i mindre, enklere å behandle, biter. I hovedsak er det en mal for å bruke en gruppe av en bestemt gjenstand som om det var et enkelt objekt selv.

Vi skal bruke et mye brukt eksempel for å illustrere dette: tenk på et enkelt tegneprogram. Du vil at den skal la deg tegne trekanter, firkanter og sirkler, og behandle dem på en annen måte. Men du vil også at den skal kunne håndtere grupper av tegninger. Hvordan kan vi enkelt gjøre det?

Komposittmønsteret er den perfekte kandidaten til denne jobben. Ved å behandle en "tegningsgruppe" som tegning, kan man enkelt legge til tegninger i denne gruppen, og gruppen som helhet vil fortsatt bli sett på som en tegning.

Når det gjelder programmering, ville vi ha en grunnklasse, Tegning, som har standardtrekk på en tegning (du kan flytte den rundt, endre lag, rotere den og så videre) og fire underklasser, Triangel, Torget, Sirkel og Gruppe.

I dette tilfellet vil de første tre klassene ha en enkel oppførsel, og krever bare brukerinngangen til de grunnleggende egenskapene til hver form. De Gruppe klassen vil imidlertid ha metoder for å legge til og fjerne former, samt å gjøre en operasjon på alle dem (for eksempel å endre fargen på alle former i en gruppe samtidig). Alle fire underklassene vil fortsatt bli behandlet som en Tegning, så du trenger ikke å bekymre deg for å legge til spesifikk kode for når du vil operere på en gruppe.

For å få dette til en bedre representasjon, kan vi se hver tegning som en knutepunkt i et tre. Hver knute er et blad, bortsett fra Gruppe noder, som kan ha barn - som igjen er tegninger i den gruppen.


En visuell representasjon av mønsteret

Stikker med tegningsappseksemplet, dette er en visuell representasjon av "tegneapplikasjonen" vi tenkte på. Merk at det er tre tegninger i bildet: en trekant, en firkant og en gruppe som består av en sirkel og en firkant:

Og dette er trepresentasjonen av den nåværende scenen (roten er tegningsprogrammets scenen):

Hva om vi ønsket å legge til en annen tegning, som er en gruppe av en trekant og en sirkel, inne i gruppen vi har for øyeblikket? Vi ville bare legge til det som vi ville legge til noen tegning inne i en gruppe. Slik ser den visuelle representasjonen ut:

Og dette er hva treet ville bli:

Forestill deg nå at vi skal bygge en løsning på attributter problemet vi har. Selvfølgelig vil vi ikke ha en direkte visuell representasjon (vi kan bare se sluttresultatet, som er den beregnede egenskapen gitt de rå verdiene og bonusene), så vi begynner å tenke i komposittmønsteret med trerepresentasjonen.


Modellering av vårt problem

For å gjøre det mulig å modellere våre attributter i et tre, må vi bryte hver egenskap i de minste delene vi kan.

Vi vet at vi har bonuser, som enten kan legge til en rå verdi for attributtet, eller øke den med en prosentandel. Det er bonuser som legger til attributten, og andre som beregnes etter at alle de første bonusene er brukt (for eksempel bonuser fra ferdigheter).

Så, vi kan ha:

  • Råbonuser (lagt til den raske verdien av attributtet)
  • Endelige bonuser (lagt til attributtet etter at alt annet er beregnet)

Du har kanskje lagt merke til at vi ikke skiller bonuser som legger til en verdi for attributtet fra bonuser som øker attributtet med en prosentandel. Det er fordi vi modellerer hver bonus for å kunne endre på samme tid. Dette betyr at vi kan få en bonus som legger til 5 til verdien og øker attributtet med 10%. Dette vil alle bli håndtert i koden.

Disse to slags bonuser er bare bladene i vårt tre. De er ganske mye som Triangel, Torget og Sirkel klasser i vårt eksempel fra før.

Vi har fortsatt ikke opprettet et foretak som skal fungere som en gruppe. Disse enhetene vil være egenskapene selv! De Gruppe klassen i vårt eksempel vil være rett og slett selve attributten. Så vi vil ha en Egenskap klasse som vil oppføre seg som noen Egenskap.

Slik kan et attributtreet se ut:

Nå som alt er bestemt, skal vi starte vår kode?


Opprette baseklassene

Vi bruker ActionScript 3.0 som språket for koden i denne opplæringen, men ikke bekymre deg! Koden vil bli fullt kommentert etterpå, og alt som er unikt for språket (og Flash-plattformen) vil bli forklart og alternativer vil bli gitt - så hvis du er kjent med et hvilket som helst OOP-språk, vil du kunne følge dette opplæring uten problemer.

Den første klassen vi trenger å opprette, er grunnklassen for noen attributt og bonuser. Filen vil bli kalt BaseAttribute.as, og å lage det er veldig enkelt. Her er koden, med kommentarer etterpå:

 pakke offentlig klasse BaseAttribute private var _baseValue: int; privat var _baseMultiplier: Nummer; offentlig funksjon BaseAttribute (verdi: int, multiplikator: Nummer = 0) _baseValue = value; _baseMultiplier = multiplikator;  offentlig funksjon få baseValue (): int return _baseValue;  offentlig funksjon få baseMultiplier (): Nummer return _baseMultiplier; 

Som du kan se, er det veldig enkelt i denne grunnklassen. Vi lager bare _verdi og _multiplier felter, tilordne dem i konstruktøren, og lag to getter-metoder, ett for hvert felt.

Nå må vi opprette RawBonus og FinalBonus klasser. Disse er ganske enkelt subklasser av BaseAttribute, med ingenting lagt til. Du kan utvide på det så mye du vil, men for nå vil vi bare lage disse to blanke underklassene av BaseAttribute:

RawBonus.as:

 pakke offentlig klasse RawBonus utvider BaseAttribute offentlig funksjon RawBonus (verdi: int = 0, multiplikator: Nummer = 0) super (verdi, multiplikator); 

FinalBonus.as:

 pakke offentlig klasse FinalBonus utvider BaseAttribute offentlig funksjon FinalBonus (verdi: int = 0, multiplikator: Nummer = 0) super (verdi, multiplikator); 

Som du kan se, har disse klassene ingenting i dem, men en konstruktør.


Egenskapsklassen

De Egenskap klassen vil være lik en gruppe i komposittmønsteret. Det kan inneholde eventuelle rå eller endelige bonuser, og vil ha en metode for å beregne den endelige verdien av attributten. Siden det er en underklasse av BaseAttribute, de _baseValue feltet i klassen vil være startverdien av attributten.

Når du oppretter klassen, vil vi ha et problem når du beregner den endelige verdien av attributtet: Siden vi ikke adskiller raske bonuser fra endelige bonuser, er det ikke mulig å beregne sluttverdien fordi vi ikke vet når du skal Påfør hver bonus.

Dette kan løses ved å gjøre en liten modifikasjon til det grunnleggende komposittmønsteret. I stedet for å legge til et barn til samme "container" i gruppen, vil vi opprette to "containere", en for råbonusene og andre for de endelige bonusene. Hver bonus vil fortsatt være et barn av Egenskap, men vil være på forskjellige steder for å tillate beregning av den endelige verdien av attributten.

Med det forklart, la oss komme til koden!

 pakke public class Attribute utvider BaseAttribute private var _rawBonuses: Array; private var _finalBonuser: Array; privat var _finalValue: int; offentlig funksjon Attributt (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  offentlig funksjon addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  offentlig funksjon addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  offentlig funksjon removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  offentlig funksjon removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  offentlig funksjon calculateValue (): int _finalValue = baseValue; // Legge til verdi fra raw var rawBonusValue: int = 0; var rawBonusMultiplier: Nummer = 0; for hver (var bonus: RawBonus i _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Legge til verdi fra siste var finalBonusValue: int = 0; var finalBonusMultiplier: Nummer = 0; for hver (var bonus: FinalBonus i _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); returner _finalValue;  offentlig funksjon få finalValue (): int return calculateValue (); 

Metodene addRawBonus (), addFinalBonus (), removeRawBonus () og removeFinalBonus () er veldig tydelige. Alt de gjør er å legge til eller fjerne deres spesifikke bonustype til eller fra arrayet som inneholder alle bonuser av den typen.

Den vanskelige delen er calculateValue () metode. Først oppsummerer det alle verdiene som de raske bonusene legger til attributtet, og oppsummerer også alle multipliseringene. Deretter legger den summen av alle rå bonusverdier til startattributtet, og bruker deretter multiplikatoren. Senere gjør det samme trinn for de endelige bonusene, men denne gangen brukes verdiene og multiplikatorene til den halvkalkulerte endelige attributtverdien.

Og vi er ferdige med strukturen! Sjekk de neste trinnene for å se hvordan du vil bruke og utvide den.


Ekstra oppførsel: Timed Bonuses

I vår nåværende struktur har vi bare enkle rå og endelige bonuser, som for øyeblikket ikke har noen forskjell. I dette trinnet vil vi legge til ekstra oppførsel til FinalBonus klassen, for å få det til å se ut som bonuser som ville bli brukt gjennom aktiv ferdigheter i et spill.

Siden, som navnet antyder, er slike ferdigheter bare aktive i en viss tidsperiode, vil vi legge til en timingadferd på de endelige bonusene. De raske bonusene kan brukes for eksempel for bonuser lagt til gjennom utstyr.

For å gjøre dette, bruker vi Timer klasse. Denne klassen er innfødt fra ActionScript 3.0, og alt som gjør det, oppfører seg som en timer, starter på 0 sekunder og deretter ringer en spesifisert funksjon etter en angitt tid, tilbakestiller tilbake til 0 og starter tellingen igjen, til den når det spesifiserte antall teller igjen. Hvis du ikke angir dem, vil Timer vil fortsette å løpe til du stopper det. Du kan velge når timeren starter og når den stopper. Du kan gjenskape oppførselen ved å bruke språketes timingsystemer med riktig ekstra kode, om nødvendig.

La oss hoppe til koden!

 pakke import flash.events.TimerEvent; importere flash.utils.Timer; offentlig klasse FinalBonus utvider BaseAttribute private var _timer: Timer; privat var _parent: Attribut; offentlig funksjon FinalBonus (tid: int, verdi: int = 0, multiplikator: Nummer = 0) super (verdi, multiplikator); _timer = ny Timer (tid); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  offentlig funksjon startTimer (foreldre: Attributt): void _parent = foreldre; _timer.start ();  privat funksjon onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (this); 

I konstruktøren er den første forskjellen at de endelige bonusene nå krever a tid parameter, som vil vise hvor lenge de varer. Inne i konstruktøren lager vi en Timer for den tiden (forutsatt at tiden er i millisekunder), og legg til en hendelseslytter til den.

(Eventlyttere er i utgangspunktet hva som vil gjøre timeren ringe til riktig funksjon når den når den bestemte tidsperioden - i dette tilfellet er funksjonen som skal kalles, onTimerEnd ().)

Legg merke til at vi ikke har startet timeren ennå. Dette gjøres i startTimer () metode, som også krever en parameter, forelder, som må være en Egenskap. Denne funksjonen krever attributtet som legger til bonusen for å ringe den funksjonen for å aktivere den; i sin tur starter dette timeren og forteller bonusen hvilken instans å be om å fjerne bonusen når timeren har nådd grensen.

Fjerningsdelen er ferdig i onTimerEnd () metode, som bare spør den angitte forelderen for å fjerne den og stoppe timeren.

Nå kan vi bruke endelige bonuser som tidsbestemte bonuser, noe som indikerer at de varer bare for en viss tid.


Ekstra oppførsel: Avhengige egenskaper

En ting som ofte settes i RPG-spill er attributter som avhenger av andre. La oss ta for eksempel angrepshastighetsattributtet. Det er ikke bare avhengig av hvilken type våpen du bruker, men nesten alltid på karakterens fingerferdighet også.

I vårt nåværende system tillater vi bare at bonuser er barn av Egenskap forekomster. Men i vårt eksempel må vi la et attributt være et barn av et annet attributt. Hvordan kan vi gjøre det? Vi kan lage en underklasse av Egenskap, kalt DependantAttribute, og gi denne underkategorien all den oppførselen vi trenger.

Å legge til attributter som barn er veldig enkelt: alt vi trenger å gjøre er å opprette en annen matrise for å holde attributter, og legg til spesifikk kode for å beregne det endelige attributtet. Siden vi ikke vet om alle attributter vil bli beregnet på samme måte (du vil kanskje først bruke fingerferdighet til å endre angrepshastigheten, og deretter sjekke bonusene, men bruk først bonuser for å endre magisk angrep og bruk for eksempel, intelligens), må vi også skille beregningen av det endelige attributtet i Egenskap klasse i ulike funksjoner. La oss gjøre det først.

I Attribute.as:

 pakke public class Attribute utvider BaseAttribute private var _rawBonuses: Array; private var _finalBonuser: Array; beskyttet var _finalValue: int; offentlig funksjon Attributt (startingValue: int) super (startingValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  offentlig funksjon addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  offentlig funksjon addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  offentlig funksjon removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  offentlig funksjon removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  beskyttet funksjon applyRawBonuses (): void // Legg til verdi fra raw var rawBonusValue: int = 0; var rawBonusMultiplier: Nummer = 0; for hver (var bonus: RawBonus i _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  beskyttet funksjon applyFinalBonuses (): void // Legg til verdi fra siste var finalBonusValue: int = 0; var finalBonusMultiplier: Nummer = 0; for hver (var bonus: RawBonus i _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  offentlig funksjon calculateValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returner _finalValue;  offentlig funksjon få finalValue (): int return calculateValue (); 

Som du ser av de uthevede linjene, var alt vi gjorde skape applyRawBonuses () og applyFinalBonuses () og ring dem når du beregner den endelige attributtet i calculateValue (). Vi har også laget _finalValue beskyttet, slik at vi kan endre det i underklassene.

Nå er alt satt for oss å lage DependantAttribute klasse! Her er koden:

 pakke offentlig klasse DependantAttribute extends Attribut protected var _otherAttributes: Array; offentlig funksjon DependantAttribute (startingValue: int) super (startingValue); _otherAttributes = [];  offentlig funksjon addAttribute (attr: Attribut): void _otherAttributes.push (attr);  offentlig funksjon removeAttribute (attr: Attribut): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  offentlig overstyringsfunksjon beregneValue (): int // Spesifikk attributtkode går et sted her _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returner _finalValue; 

I denne klassen, den addAttribute () og removeAttribute () funksjoner bør være kjent for deg. Du må være oppmerksom på overstyringen calculateValue () funksjon. Her bruker vi ikke attributter for beregning av sluttverdien - du må gjøre det for alle avhengige attributter!

Dette er et eksempel på hvordan du vil gjøre det for å beregne angrepshastigheten:

 pakke offentlig klasse AttackSpeed ​​strekker DependantAttribute offentlig funksjon AttackSpeed ​​(startingValue: int) super (startingValue);  offentlig overstyringsfunksjon beregneValue (): int _finalValue = baseValue; // Hver 5 poeng i fingerferdighet legger til 1 til angrepshastighet var fingerfylling: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (fingerfrekvens / 5); applyRawBonuses (); applyFinalBonuses (); returner _finalValue; 

I denne klassen antar vi at du allerede har lagt til fingerferdighetsattributtet som et barn av Angrepshastighet, og at det er det første i _otherAttributes array (det er mange forutsetninger å gjøre, se konklusjonen for mer info). Etter å ha hentet fingerferdighet, bruker vi det bare til å legge til mer enn den endelige verdien av angrepshastigheten.


Konklusjon

Med alt ferdig, hvordan ville du bruke denne strukturen i et spill? Det er veldig enkelt: alt du trenger å gjøre er å opprette forskjellige attributter og tilordne hver av dem en Egenskap forekomst. Deretter handler det om å legge til og fjerne bonuser til det gjennom de allerede opprettede metodene.

Når et element er utstyrt eller brukt, og det legger til en bonus på et hvilket som helst attributt, må du opprette en bonusinstans av den tilsvarende typen og deretter legge den til tegnets attributt. Deretter beregner du bare den endelige attributtverdien.

Du kan også utvide på ulike typer bonuser som er tilgjengelige. For eksempel kan du få en bonus som endrer merverdien eller multiplikatoren over tid. Du kan også bruke negative bonuser (som den nåværende koden allerede kan håndtere).

Med ethvert system er det alltid mer du kan legge til. Her er noen foreslåtte forbedringer du kan gjøre:

  • Identifiser attributter etter navn
  • Lag et "sentralisert" system for å administrere attributter
  • Optimaliser ytelsen (hint: du trenger ikke alltid å beregne den endelige verdien helt)
  • Gjør det mulig for noen bonuser å dempe eller styrke andre bonuser

Takk for at du leste!