Utilizarea modelului de design compozit pentru un sistem de atribute RPG

Inteligență, voință, Charisma, înțelepciune: în afară de calitățile importante pe care ar trebui să le aveți ca dezvoltator de jocuri, acestea sunt, de asemenea, atribute comune utilizate în RPG-uri. Calculul valorilor acestor atribute - aplicarea bonusurilor temporizate și luarea în considerare a efectului elementelor echipate - poate fi dificilă. În acest tutorial, vă voi arăta cum să utilizați un model ușor modificat pentru a face față acestei situații.

Notă: Deși acest tutorial este scris folosind Flash și AS3, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.


Introducere

Sistemele de atribute sunt foarte frecvent utilizate în RPG-uri pentru a cuantifica punctele forte, punctele slabe și abilitățile personajelor. Dacă nu sunteți familiarizați cu acestea, schimbați pagina Wikipedia pentru o imagine de ansamblu decentă.

Pentru a le face mai dinamice și mai interesante, dezvoltatorii adesea îmbunătățesc aceste sisteme adăugând abilități, elemente și alte lucruri care afectează atributele. Dacă doriți să faceți acest lucru, veți avea nevoie de un sistem bun care să poată calcula atributele finale (luând în considerare orice alt efect) și să gestioneze adăugarea sau eliminarea diferitelor tipuri de bonusuri.

În acest tutorial, vom explora o soluție pentru această problemă utilizând o versiune ușor modificată a modelului de design compozit. Soluția noastră va fi capabilă să gestioneze bonusurile și va lucra la orice set de atribute pe care le definiți.


Care este modelul compozit?

Această secțiune este o prezentare generală a modelului de design compozit. Dacă sunteți deja familiarizat cu aceasta, poate doriți să treceți la Modelarea problemei noastre.

Modelul compozit este un model de design (un șablon de design general binecunoscut, reutilizabil) pentru a împărți ceva mare în obiecte mai mici, pentru a crea un grup mai mare prin manipularea doar a obiectelor mici. Ea facilitează ruperea unor bucăți mari de informații în bucăți mai mici, mai ușor de tratat. În esență, este un șablon pentru utilizarea unui grup dintr-un anumit obiect ca și cum ar fi fost un singur obiect în sine.

Vom folosi un exemplu folosit pe scară largă pentru a ilustra acest lucru: gândiți-vă la o aplicație simplă de desen. Vrei să te lași să desenezi triunghiuri, pătrate și cercuri și să le tratezi diferit. Dar doriți, de asemenea, să puteți gestiona grupurile de desene. Cum putem face acest lucru cu ușurință?

Modelul compozit este candidatul perfect pentru această slujbă. Prin tratarea unui "grup de desene" ca desen în sine, se poate adăuga cu ușurință orice desen în interiorul acestui grup, iar grupul în ansamblu ar fi în continuare văzut ca un singur desen.

În ceea ce privește programarea, am avea o clasă de bază, Desen, care are comportamentele implicite ale unui desen (îl puteți muta, schimba straturile, rotiți-l și așa mai departe) și patru subclase, Triunghi, Pătrat, Cerc și grup.

În acest caz, primele trei clase vor avea un comportament simplu, necesitând doar introducerea de către utilizator a atributelor de bază ale fiecărei forme. grup Cu toate acestea, clasa va avea metode de adăugare și eliminare a formelor, precum și o operațiune pe toate (de exemplu, schimbarea culorii tuturor formelor într-un grup simultan). Toate cele patru subclase ar fi tratate ca a Desen, astfel încât nu trebuie să vă faceți griji cu privire la adăugarea unui cod specific atunci când doriți să operezi pe un grup.

Pentru a face aceasta într-o reprezentare mai bună, putem vedea fiecare desen ca un nod dintr-un copac. Fiecare nod este o frunză, cu excepția grup noduri, care pot avea copii - care sunt desene în interiorul acelui grup.


O reprezentare vizuală a modelului

Lipirea cu exemplul aplicației de desen, aceasta este o reprezentare vizuală a "aplicației de desen" pe care am crezut-o. Rețineți că există trei desene în imagine: un triunghi, un pătrat și un grup format dintr-un cerc și un pătrat:

Și aceasta este reprezentarea copac a scenei curente (rădăcina este etapa aplicației de desen):

Dacă am vrea să adăugăm un alt desen, care este un grup de triunghi și cerc, în interiorul grupului pe care îl avem acum? Doar l-am adăuga pe măsură ce adăugăm orice desen în interiorul unui grup. Aceasta ar reprezenta reprezentarea vizuală:

Și acesta ar fi arborele:

Acum, imaginați-vă că vom construi o soluție la problema atributelor pe care o avem. Evident, nu vom avea o reprezentare vizuală directă (putem vedea doar rezultatul final, care este atributul calculat având în vedere valorile brute și bonusurile), așa că vom începe să gândim în modelul compozit cu reprezentarea copacilor.


Modelarea problemei noastre

Pentru a face posibilă modelarea atributelor noastre într-un arbore, trebuie să rupem fiecare atribut în cele mai mici părți pe care le putem.

Știm că avem bonusuri, care pot să adauge fie o valoare brute la atribut, fie să o crească cu un procent. Există bonusuri care se adaugă atributului, și altele care sunt calculate după aplicarea tuturor primelor bonusuri (bonusuri de la aptitudini, de exemplu).

Deci, putem avea:

  • Prime bonusuri (adăugate la valoarea brut a atributului)
  • Bonusuri finale (adăugate atributului după ce au fost calculate toate celelalte)

S-ar putea să fi observat că nu separăm bonusuri care adaugă o valoare la atribut din bonusuri care măresc atributul cu un procentaj. Acest lucru se datorează faptului că modelați fiecare bonus pentru a putea schimba în același timp. Aceasta înseamnă că am putea avea un bonus care adaugă 5 la valoare și crește atributul cu 10%. Toate acestea vor fi tratate în cod.

Aceste două tipuri de bonusuri sunt doar frunzele copacului nostru. Ele sunt destul de mult ca Triunghi, Pătrat și Cerc clase din exemplul nostru anterior.

Încă nu am creat o entitate care să servească drept grup. Aceste entități vor fi propriile atribute! grup clasa în exemplul nostru va fi pur și simplu atributul însuși. Așa că vom avea un Atribut clasa care se va comporta ca orice atribut.

Acesta este modul în care un arbore de atribute ar putea arăta:

Acum că totul este hotărât, vom începe codul nostru?


Crearea claselor de bază

Vom folosi ActionScript 3.0 ca limbă pentru cod în acest tutorial, dar nu vă faceți griji! Codul va fi complet comentat ulterior, iar tot ceea ce este unic pentru limbă (și pentru platforma Flash) va fi explicat și vor fi furnizate alternative - deci, dacă sunteți familiarizat cu orice limbaj OOP, veți putea urmări acest lucru tutorial fără probleme.

Prima clasă pe care trebuie să o creăm este clasa de bază pentru orice atribut și bonusuri. Se va apela fișierul BaseAttribute.as, și crearea acestuia este foarte simplă. Iată codul, cu comentarii ulterior:

 pachet public class BaseAttribute private var _baseValue: int; private var _baseMultiplier: Număr; funcția publică BaseAttribute (valoare: int, multiplicator: Number = 0) _baseValue = value; _baseMultiplier = multiplicator;  funcția publică get baseValue (): int return _baseValue;  funcția publică obține baseMultiplier (): Număr return _baseMultiplier; 

După cum puteți vedea, lucrurile sunt foarte simple în această clasă de bază. Tocmai creăm _valoare și _multiplier câmpuri, le atribuie în constructor și fac două metode getter, câte una pentru fiecare câmp.

Acum trebuie să creăm RawBonus și FinalBonus clase. Acestea sunt pur și simplu subclase ale BaseAttribute, fără nimic adăugat. Puteți să vă extindeți pe cât doriți, dar pentru moment vom face doar aceste două subclase goale BaseAttribute:

RawBonus.as:

 pachet public class RawBonus extinde BaseAttribute funcția publică RawBonus (valoare: int = 0, multiplicator: număr = 0) super (valoare, multiplicator); 

FinalBonus.as:

 pachet public class FinalBonus extinde BaseAttribute funcția publică FinalBonus (valoare: int = 0, multiplicator: Number = 0) super (valoare, multiplicator); 

După cum puteți vedea, aceste clase nu au nimic în ele decât un constructor.


Clasa atributelor

Atribut clasa va fi echivalentul unui grup din modelul compozit. Poate deține bonusuri brute sau finale și va avea o metodă de calculare a valorii finale a atributului. Deoarece este o subclasă de BaseAttribute, _baseValue câmpul clasei va fi valoarea de pornire a atributului.

La crearea clasei, vom avea o problemă la calculul valorii finale a atributului: deoarece nu separăm bonusurile prime de la bonusurile finale, nu putem calcula valoarea finală, deoarece nu știm când să aplicați fiecare bonus.

Acest lucru poate fi rezolvat prin modificarea ușoară a modelului compozit de bază. În loc să adăugăm orice copil în același "container" în cadrul grupului, vom crea două "containere", unul pentru bonusurile prime și altul pentru bonusurile finale. Fiecare bonus va fi în continuare un copil Atribut, dar vor fi în locuri diferite pentru a permite calcularea valorii finale a atributului.

Cu asta am explicat, să ajungem la cod!

 pachet atributul public class extinde BaseAttribute private var _rawBonuses: Array; privat var _finalBonuses: Array; privat var _finalValue: int; funcția publică funcția (startValue: int) super (startValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  funcția publică addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  funcția publică addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  funcția publică removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  funcția publică removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  funcția publică calculateValue (): int _finalValue = baseValue; // Adăugarea valorii din raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; pentru fiecare (var bonus: RawBonus în _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Adăugarea valorii de la final var finalBonusValue: int = 0; var finalBonusMultiplier: Număr = 0; pentru fiecare (var bonus: FinalBonus în _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); returnează _finalValue;  funcția publică obține finalValue (): int return calculateValue (); 

Metodele addRawBonus (), addFinalBonus (), removeRawBonus () și removeFinalBonus () sunt foarte clare. Tot ce fac este să adauge sau să elimine tipul lor de bonus specific sau din matricea care conține toate bonusurile de acel tip.

Partea dificilă este calculateValue () metodă. În primul rând, rezumă toate valorile pe care bonusurile brute le adaugă atributului și, de asemenea, însumează toți multiplicatorii. Apoi, adaugă suma tuturor bonusurilor brute la atributul de pornire și apoi aplică multiplicatorul. Mai târziu, face același pas pentru bonusurile finale, dar de această dată aplicând valorile și multiplicatorii la valoarea atributului final jumătate calculată.

Și am terminat cu structura! Verificați pașii următori pentru a vedea cum ați utiliza și extindeți-l.


Extra Comportament: Bonusuri în Timp

În structura noastră actuală, avem doar bonusuri brute simple și finale, care în prezent nu au nicio diferență. În acest pas, vom adăuga un comportament suplimentar la FinalBonus clasa, pentru a face să arate mai mult ca bonusurile care ar fi aplicate prin activ abilități într-un joc.

Din moment ce, așa cum sugerează și numele, astfel de abilități sunt active doar pentru o anumită perioadă de timp, vom adăuga un comportament temporal asupra bonusurilor finale. Bonusurile brute ar putea fi utilizate, de exemplu, pentru bonusurile adăugate prin echipament.

Pentru a face acest lucru, vom folosi Cronometrul clasă. Această clasă este nativă din ActionScript 3.0 și tot ce se întâmplă este să se comporte ca un cronometru, începând cu 0 secunde și apoi să apeleze o funcție specificată după o anumită perioadă de timp, resetând înapoi la 0 și pornind din nou numărul, până când ajunge la limitele specificate numărul de contează din nou. Dacă nu le specificați, Cronometrul va continua să fie difuzată până când o opriți. Puteți alege când începe și cronometrul se oprește. Puteți să vă replicați comportamentul pur și simplu utilizând sistemele de sincronizare ale limbii dvs. cu codul suplimentar corespunzător, dacă este necesar.

Să trecem la cod!

 pachet import flash.events.TimerEvent; import flash.utils.Timer; clasa publica FinalBonus extinde BaseAttribute private var _timer: Timer; privat var _parent: Atribut; funcția publică FinalBonus (timpul: int, valoarea: int = 0, multiplicatorul: Număr = 0) super (valoare, multiplicator); _timer = Timer nou (timp); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  funcția publică startTimer (părinte: Atribut): void _parent = părinte; _timer.start ();  funcția privată onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (aceasta); 

În constructor, prima diferență este că bonusurile finale necesită acum timp parametru, care va arăta cât timp durează. În interiorul constructorului, creăm un Cronometrul pentru acea cantitate de timp (presupunând că timpul este în milisecunde) și adăugați un ascultător al evenimentului.

(Ascultătorii de evenimente sunt, în esență, ceea ce va face apelul temporizatorului să aibă funcția corectă când atinge acea anumită perioadă de timp - în acest caz, funcția care trebuie apelată este onTimerEnd ().)

Observați că nu am început încă cronometrul. Acest lucru se face în startTimer () care necesită și un parametru, mamă, care trebuie să fie Atribut. Această funcție necesită atributul care adaugă bonusul pentru a apela funcția respectivă pentru ao activa; la rândul său, acesta pornește cronometrul și îi spune bonusului care instanță să solicite eliminarea bonusului atunci când cronometrul a atins limita.

Partea de îndepărtare se efectuează în onTimerEnd () , care va cere părintelui setat să îl elimine și să oprească cronometrul.

Acum, putem folosi bonusurile finale ca bonusuri cronometrate, indicând faptul că vor dura doar pentru o anumită perioadă de timp.


Comportament suplimentar: atribute dependente

Un lucru văzut de obicei în jocurile RPG sunt atributele care depind de ceilalți. Să luăm, de exemplu, atributul "viteză de atac". Nu depinde doar de tipul de arma pe care îl folosiți, ci și de dexteritatea personajului.

În sistemul nostru actual, permitem doar bonusurile să fie copii de Atribut instanțe. Dar în exemplul nostru, trebuie să lăsăm un atribut să fie un copil al altui atribut. Cum putem face asta? Putem crea o subclasă de Atribut, denumit DependantAttribute, și dă acestei subclase întregul comportament de care avem nevoie.

Adăugarea de atribute ca și copii este foarte simplă: tot ce trebuie să facem este să creați un alt șir pentru a ține atribute și să adăugați un cod specific pentru calcularea atributului final. Deoarece nu știm dacă fiecare atribut va fi calculat în același mod (poate doriți să folosiți mai întâi dexteritatea pentru a schimba viteza de atac și apoi să verificați bonusurile, dar mai întâi să folosiți bonusuri pentru a schimba atacul magic și apoi să utilizați, de exemplu, inteligența), va trebui, de asemenea, să separăm calculul atributului final în Atribut clasa în diferite funcții. Să facem asta mai întâi.

În Attribute.as:

 pachet atributul public class extinde BaseAttribute private var _rawBonuses: Array; privat var _finalBonuses: Array; protejat var _finalValue: int; funcția publică funcția (startValue: int) super (startValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  funcția publică addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  funcția publică addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  funcția publică removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  funcția publică removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  funcția protejată applyRawBonuses (): void // Adăugarea valorii din raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; pentru fiecare (var bonus: RawBonus în _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  funcția protejată applyFinalBonuses (): void // Adăugarea valorii de la final var finalBonusValue: int = 0; var finalBonusMultiplier: Număr = 0; pentru fiecare (var bonus: RawBonus în _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  funcție publică calculateValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returnează _finalValue;  funcția publică obține finalValue (): int return calculateValue (); 

După cum puteți vedea prin liniile evidențiate, tot ce am făcut a fost crearea applyRawBonuses () și applyFinalBonuses () și le apelați la calcularea atributului final în calculateValue (). Am făcut și noi _finalValue protejate, astfel încât să putem schimba în subclase.

Acum, totul este pregătit pentru noi să creăm DependantAttribute clasă! Iată codul:

 pachet public class DependantAttribute extinde Atributul protejat var _otherAttributes: Array; funcția publică DependantAttribute (startValue: int) super (startValue); _otherAttributes = [];  funcția publică addAttribute (attr: Atribut): void _otherAttributes.push (attr);  funcția publică removeAttribute (attr: Atribut): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  funcția override publică calculateValue (): int // Codul atributului specific merge undeva aici _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); returnează _finalValue; 

În această clasă, addAttribute () și removeAttribute () funcțiile trebuie să vă fie familiare. Trebuie să acordați atenție celor suprasolicitate calculateValue () funcţie. Aici nu utilizăm atributele pentru calcularea valorii finale - trebuie să o faceți pentru fiecare atribut dependent!

Acesta este un exemplu despre cum ați face acest lucru pentru calcularea vitezei de atac:

 pachet public class AttackSpeed ​​extinde DependantAttribute funcția publică AttackSpeed ​​(startValue: int) super (startingValue);  funcția de suprascriere publică calculateValue (): int _finalValue = baseValue; // Fiecare 5 puncte în dexteritate adaugă 1 la viteza de atac var dexteritate: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (dexteritate / 5); applyRawBonuses (); applyFinalBonuses (); returnează _finalValue; 

În această clasă, presupunem că ați adăugat deja atributul de dexteritate ca fiind copilul lui Viteza de atac, și că este primul din _otherAttributes array (care este o mulțime de ipoteze de făcut, verificați concluzia pentru mai multe informații). După recuperarea dexterității, pur și simplu o folosim pentru a adăuga mai mult la valoarea finală a vitezei de atac.


Concluzie

Cu totul terminat, cum ai folosi această structură într-un joc? Este foarte simplu: tot ce trebuie să faceți este să creați atribute diferite și să le atribuiți fiecăruia Atribut instanță. După aceasta, este vorba despre adăugarea și eliminarea bonusurilor prin metodele deja create.

Atunci când un element este echipat sau utilizat și adaugă un bonus la orice atribut, trebuie să creați o instanță bonus de tipul corespunzător și apoi să o adăugați la atributul caracterului. După aceea, pur și simplu recalculează valoarea atributului final.

De asemenea, puteți extinde diferitele tipuri de bonusuri disponibile. De exemplu, puteți avea un bonus care modifică valoarea adăugată sau multiplicatorul în timp. De asemenea, puteți utiliza bonusuri negative (pe care codul curent le poate gestiona deja).

Cu orice sistem, există întotdeauna mai multe pe care le puteți adăuga. Iată câteva îmbunătățiri sugerate pe care le puteți face:

  • Identificați atributele după nume
  • Efectuați un sistem "centralizat" pentru gestionarea atributelor
  • Optimizați performanța (indiciu: nu trebuie să calculați întotdeauna valoarea finală în întregime)
  • Permiteți ca unele bonusuri să atenueze sau să consolideze alte bonusuri

Vă mulțumim pentru lectură!