Uso del modello di disegno composito per un sistema di attributi RPG

Intelligenza, Forza di volontà, Carisma, Saggezza: oltre ad essere qualità importanti che dovresti avere come sviluppatore di giochi, questi sono anche attributi comuni usati nei giochi di ruolo. Calcolare i valori di tali attributi, applicando i bonus a tempo e tenendo conto dell'effetto degli oggetti equipaggiati, può essere complicato. In questo tutorial, ti mostrerò come utilizzare un pattern composito leggermente modificato per gestirlo al volo.

Nota: Sebbene questo tutorial sia stato scritto usando Flash e AS3, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


introduzione

I sistemi di attributi sono molto comunemente usati nei giochi di ruolo per quantificare i punti di forza, i punti deboli e le abilità dei personaggi. Se non hai familiarità con loro, sfoglia la pagina di Wikipedia per una panoramica decente.

Per renderli più dinamici e interessanti, gli sviluppatori spesso migliorano questi sistemi aggiungendo abilità, elementi e altre cose che influiscono sugli attributi. Se vuoi farlo, avrai bisogno di un buon sistema in grado di calcolare gli attributi finali (prendendo in considerazione ogni altro effetto) e gestire l'aggiunta o la rimozione di diversi tipi di bonus.

In questo tutorial, esploreremo una soluzione per questo problema utilizzando una versione leggermente modificata del modello di progettazione Composite. La nostra soluzione sarà in grado di gestire i bonus e funzionerà su qualsiasi set di attributi che definisci.


Qual è il modello composito?

Questa sezione è una panoramica del modello di progettazione Composite. Se già lo conosci, potresti saltare a Modellando il nostro problema.

Il modello composito è un modello di progettazione (un modello di progettazione generale ben noto, riutilizzabile) per suddividere qualcosa di grande in oggetti più piccoli, in modo da creare un gruppo più grande gestendo solo gli oggetti piccoli. Rende facile rompere grandi blocchi di informazioni in blocchi più piccoli e facilmente trattabili. Essenzialmente, è un modello per usare un gruppo di un particolare oggetto come se fosse un singolo oggetto stesso.

Utilizzeremo un esempio ampiamente utilizzato per illustrare ciò: pensiamo a un'applicazione di disegno semplice. Vuoi che ti consenta di disegnare triangoli, quadrati e cerchi e trattarli in modo diverso. Ma vuoi anche che sia in grado di gestire gruppi di disegni. Come possiamo farlo facilmente?

Il modello composito è il candidato perfetto per questo lavoro. Trattando un "gruppo di disegni" come un disegno stesso, si potrebbe facilmente aggiungere qualsiasi disegno all'interno di questo gruppo, e il gruppo nel suo complesso sarebbe ancora visto come un singolo disegno.

In termini di programmazione, avremmo una classe base, Disegno, che ha i comportamenti di default di un disegno (puoi spostarlo, cambiare i livelli, ruotarlo e così via) e quattro sottoclassi, Triangolo, Piazza, Cerchio e Gruppo.

In questo caso, le prime tre classi avranno un comportamento semplice, che richiede solo l'input dell'utente degli attributi di base di ogni forma. Il Gruppo la classe, tuttavia, avrà metodi per aggiungere e rimuovere forme, oltre a eseguire un'operazione su tutte (ad esempio, cambiando il colore di tutte le forme in un gruppo contemporaneamente). Tutte e quattro le sottoclassi sarebbero comunque trattate come a Disegno, quindi non devi preoccuparti di aggiungere un codice specifico per quando vuoi operare su un gruppo.

Per ottenere questo in una rappresentazione migliore, possiamo vedere ogni disegno come un nodo in un albero. Ogni nodo è una foglia, ad eccezione di Gruppo nodi, che possono avere figli - che sono a loro volta disegni all'interno di quel gruppo.


Una rappresentazione visiva del modello

Seguendo l'esempio dell'app di disegno, questa è una rappresentazione visiva della "domanda di disegno" che abbiamo pensato. Nota che ci sono tre disegni nell'immagine: un triangolo, un quadrato e un gruppo formato da un cerchio e un quadrato:

E questa è la rappresentazione ad albero della scena corrente (la radice è lo stage dell'applicazione di disegno):

E se volessimo aggiungere un altro disegno, che è un gruppo di un triangolo e un cerchio, all'interno del gruppo che abbiamo attualmente? Lo aggiungeremo semplicemente come aggiungeremmo qualsiasi disegno all'interno di un gruppo. Ecco come apparirà la rappresentazione visiva:

E questo è ciò che l'albero sarebbe diventato:

Ora, immagina che stiamo andando a costruire una soluzione al problema degli attributi che abbiamo. Ovviamente, non avremo una rappresentazione visiva diretta (possiamo solo vedere il risultato finale, che è l'attributo calcolato dati i valori grezzi e i bonus), quindi inizieremo a pensare nel Pattern Composito con la rappresentazione ad albero.


Modellando il nostro problema

Per poter modellare i nostri attributi in un albero, dobbiamo rompere ogni attributo nelle parti più piccole che possiamo.

Sappiamo che abbiamo dei bonus, che possono aggiungere un valore grezzo all'attributo o aumentarlo di una percentuale. Ci sono bonus che si aggiungono all'attributo e altri che vengono calcolati dopo che tutti quei primi bonus sono stati applicati (bonus da abilità, per esempio).

Quindi, possiamo avere:

  • Bonus grezzi (aggiunti al valore grezzo dell'attributo)
  • Bonus finali (aggiunti all'attributo dopo che è stato calcolato tutto il resto)

Potresti aver notato che non stiamo separando i bonus che aggiungono un valore all'attributo dai bonus che aumentano l'attributo di una percentuale. Questo perché modelliamo ciascun bonus per poterlo modificare contemporaneamente. Ciò significa che potremmo avere un bonus che aggiunge 5 al valore e aumenta l'attributo del 10%. Questo verrà gestito nel codice.

Questi due tipi di bonus sono solo le foglie del nostro albero. Sono praticamente come il Triangolo, Piazza e Cerchio classi nel nostro esempio di prima.

Non abbiamo ancora creato un'entità che servirà da gruppo. Queste entità saranno gli attributi stessi! Il Gruppo la classe nel nostro esempio sarà semplicemente l'attributo stesso. Quindi avremo un Attributo classe che si comporterà qualunque attributo.

Ecco come potrebbe apparire un albero di attributi:

Ora che tutto è deciso, dovremo iniziare il nostro codice?


Creazione delle classi base

In questo tutorial utilizzeremo ActionScript 3.0 come lingua per il codice, ma non preoccuparti! Il codice verrà completamente commentato in seguito, e tutto ciò che è unico per la lingua (e la piattaforma Flash) verrà spiegato e verranno fornite alternative, quindi se si ha familiarità con qualsiasi linguaggio OOP, si sarà in grado di seguirlo. tutorial senza problemi.

La prima classe che dobbiamo creare è la classe base per qualsiasi attributo e bonus. Il file verrà chiamato BaseAttribute.as, e la creazione è molto semplice. Ecco il codice, con commenti in seguito:

 package public class BaseAttribute private var _baseValue: int; private var _baseMultiplier: Number; funzione pubblica BaseAttribute (valore: int, moltiplicatore: Number = 0) _baseValue = value; _baseMultiplier = moltiplicatore;  public function get baseValue (): int return _baseValue;  public function get baseMultiplier (): Number return _baseMultiplier; 

Come puoi vedere, le cose sono molto semplici in questa classe base. Creiamo solo il _valore e _moltiplicatore campi, assegnarli nel costruttore e creare due metodi getter, uno per ogni campo.

Ora abbiamo bisogno di creare il RawBonus e FinalBonus classi. Queste sono semplicemente sottoclassi di BaseAttribute, con niente aggiunto. Puoi espanderci quanto vuoi, ma per ora faremo solo queste due sottoclassi vuote BaseAttribute:

RawBonus.as:

 package public class RawBonus estende BaseAttribute public function RawBonus (value: int = 0, moltiplicatore: Number = 0) super (valore, moltiplicatore); 

FinalBonus.as:

 package public class FinalBonus estende BaseAttribute public function FinalBonus (value: int = 0, moltiplicatore: Number = 0) super (valore, moltiplicatore); 

Come puoi vedere, queste classi non hanno nulla in loro ma un costruttore.


La classe di attributo

Il Attributo la classe sarà l'equivalente di un gruppo nel Pattern composito. Può contenere qualsiasi premio grezzo o finale e avrà un metodo per calcolare il valore finale dell'attributo. Dal momento che è una sottoclasse di BaseAttribute, il _baseValue il campo della classe sarà il valore iniziale dell'attributo.

Quando creiamo la classe, avremo un problema nel calcolo del valore finale dell'attributo: poiché non stiamo separando i bonus grezzi dai bonus finali, non c'è modo di calcolare il valore finale, perché non sappiamo quando applica ogni bonus.

Questo può essere risolto modificando leggermente il Pattern composito di base. Invece di aggiungere qualsiasi bambino allo stesso "contenitore" all'interno del gruppo, creeremo due "contenitori", uno per i bonus raw e l'altro per i bonus finali. Ogni bonus sarà ancora un figlio di Attributo, ma sarà in posti diversi per consentire il calcolo del valore finale dell'attributo.

Con quello spiegato, andiamo al codice!

 package public class L'attributo estende BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; private var _finalValue: int; funzione pubblica Attributo (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): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  public function calculateValue (): int _finalValue = baseValue; // Aggiunta di valore da raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; per ogni (var bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Aggiunta del valore dalla variabile finale finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; per ogni (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 (); 

I metodi addRawBonus (), addFinalBonus (), removeRawBonus () e removeFinalBonus () sono molto chiari. Tutto ciò che fanno è aggiungere o rimuovere il loro specifico tipo di bonus da o verso l'array che contiene tutti i bonus di quel tipo.

La parte difficile è il calculateValue () metodo. Innanzitutto, riassume tutti i valori che i bonus grezzi aggiungono all'attributo e riassume anche tutti i moltiplicatori. Dopodiché aggiunge la somma di tutti i valori bonus non elaborati all'attributo di partenza, quindi applica il moltiplicatore. Successivamente, esegue lo stesso passo per i bonus finali, ma questa volta applicando i valori e i moltiplicatori al valore finale dell'attributo calcolato a metà.

E abbiamo finito con la struttura! Controlla i prossimi passi per vedere come utilizzeresti ed estenderlo.


Comportamento extra: bonus a tempo

Nella nostra attuale struttura, abbiamo solo bonus raw e finali semplici, che al momento non hanno alcuna differenza. In questo passaggio, aggiungeremo un comportamento extra al FinalBonus classe, in modo da renderlo più simile ai bonus che verrebbero applicati attivo abilità in un gioco.

Poiché, come suggerisce il nome, tali abilità sono attive solo per un certo periodo di tempo, aggiungeremo un comportamento temporale sui bonus finali. I bonus grezzi potrebbero essere utilizzati, ad esempio, per i bonus aggiunti tramite l'attrezzatura.

Per fare questo, useremo il Timer classe. Questa classe è nativa di ActionScript 3.0 e funziona come un timer, a partire da 0 secondi e quindi chiamando una funzione specificata dopo un intervallo di tempo specificato, reimpostando su 0 e ricominciando il conteggio, fino a quando non raggiunge il valore specificato numero di conteggi di nuovo. Se non li specifichi, il Timer continuerà a funzionare finché non lo fermerai. Puoi scegliere quando il timer inizia e quando si ferma. Puoi replicare il suo comportamento semplicemente usando i sistemi di cronometraggio della tua lingua con il codice aggiuntivo appropriato, se necessario.

Passiamo al codice!

 package import flash.events.TimerEvent; import flash.utils.Timer; public class FinalBonus estende BaseAttribute private var _timer: Timer; private var _parent: Attribute; funzione pubblica FinalBonus (tempo: int, valore: int = 0, moltiplicatore: Number = 0) super (valore, moltiplicatore); _timer = new Timer (time); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  public function startTimer (parent: Attribute): void _parent = parent; _timer.start ();  funzione privata onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (questo); 

Nel costruttore, la prima differenza è che i bonus finali ora richiedono un tempo parametro, che mostrerà per quanto tempo durano. All'interno del costruttore, creiamo a Timer per quel lasso di tempo (supponendo che il tempo sia in millisecondi) e aggiungi un listener di eventi ad esso.

(Gli ascoltatori di eventi sono fondamentalmente ciò che farà sì che il timer chiami la funzione giusta quando raggiunge quel determinato periodo di tempo - in questo caso, la funzione da chiamare è onTimerEnd ().)

Si noti che non abbiamo ancora avviato il timer. Questo è fatto nel startTimer () metodo, che richiede anche un parametro, genitore, quale deve essere un Attributo. Questa funzione richiede l'attributo che aggiunge il bonus per chiamare quella funzione per attivarla; a sua volta, questo avvia il timer e dice al bonus quale istanza chiedere di rimuovere il bonus quando il timer ha raggiunto il suo limite.

La parte di rimozione è fatta nel onTimerEnd () metodo, che chiederà al genitore dell'insieme di rimuoverlo e fermare il timer.

Ora, possiamo usare i bonus finali come bonus a tempo, a indicare che dureranno solo per un certo periodo di tempo.


Comportamento extra: attributi dipendenti

Una cosa che si vede comunemente nei giochi RPG sono attributi che dipendono dagli altri. Prendiamo, ad esempio, l'attributo "velocità di attacco". Non dipende solo dal tipo di arma che si usa, ma quasi sempre anche dalla destrezza del personaggio.

Nel nostro sistema attuale, consentiamo solo ai bonus di essere figli Attributo le istanze. Ma nel nostro esempio, dobbiamo lasciare che un attributo sia figlio di un altro attributo. Come possiamo farlo? Possiamo creare una sottoclasse di Attributo, chiamato DependantAttribute, e dare a questa sottoclasse tutto il comportamento di cui abbiamo bisogno.

Aggiungere attributi come bambini è molto semplice: tutto ciò che dobbiamo fare è creare un altro array per contenere gli attributi e aggiungere un codice specifico per il calcolo dell'attributo finale. Dato che non sappiamo se ogni attributo sarà calcolato nello stesso modo (potresti voler usare prima la destrezza per cambiare la velocità di attacco, e poi controllare i bonus, ma prima usa i bonus per cambiare l'attacco magico e poi usa, per esempio, intelligenza), dovremo anche separare il calcolo dell'attributo finale nel Attributo classe in diverse funzioni. Facciamolo prima.

Nel Attribute.as:

 package public class L'attributo estende BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; protected var _finalValue: int; funzione pubblica Attributo (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): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  funzione protetta applyRawBonuses (): void // Aggiunta di valore da raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; per ogni (var bonus: RawBonus in _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  funzione protetta applyFinalBonuses (): void // Aggiunta del valore dalla variabile finale finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; per ogni (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 (); 

Come puoi vedere dalle linee evidenziate, tutto ciò che abbiamo fatto è stato creare applyRawBonuses () e applyFinalBonuses () e chiamali quando calcoli l'attributo finale in calculateValue (). Abbiamo anche fatto _finalValue protetto, quindi possiamo cambiarlo nelle sottoclassi.

Ora, tutto è pronto per noi per creare il DependantAttribute classe! Ecco il codice:

 package public class DependantAttribute estende l'attributo protected var _otherAttributes: Array; funzione pubblica 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);  public override function calculateValue (): int // Il codice dell'attributo specifico va da qualche parte qui _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In questa classe, il Aggiungi attributo() e removeAttribute () le funzioni dovrebbero essere familiari a voi. È necessario prestare attenzione alla sovrascrittura calculateValue () funzione. Qui, non usiamo gli attributi per calcolare il valore finale - devi farlo per ogni attributo dipendente!

Questo è un esempio di come lo faresti per calcolare la velocità di attacco:

 package public class AttackSpeed ​​extends DependantAttribute public function AttackSpeed ​​(startingValue: int) super (startingValue);  public override function calculateValue (): int _finalValue = baseValue; // Ogni 5 punti in destrezza aggiunge 1 alla velocità dell'attacco var destrezza: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (destrezza / 5); applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

In questa classe, assumiamo che tu abbia già aggiunto l'attributo destrezza come figlio di AttackSpeed, e che è il primo del _otherAttributes array (sono molte le assunzioni da fare, controlla la conclusione per maggiori informazioni). Dopo aver recuperato la destrezza, la usiamo semplicemente per aggiungere altro al valore finale della velocità di attacco.


Conclusione

Con tutto finito, come useresti questa struttura in un gioco? È molto semplice: tutto ciò che devi fare è creare attributi diversi e assegnarli a ciascuno di essi Attributo esempio. Dopodiché, si tratta di aggiungere e rimuovere bonus tramite i metodi già creati.

Quando un oggetto è equipaggiato o usato e aggiunge un bonus a qualsiasi attributo, è necessario creare un'istanza bonus del tipo corrispondente e quindi aggiungerla all'attributo del personaggio. Successivamente, ricalcola semplicemente il valore dell'attributo finale.

Puoi anche espandere i diversi tipi di bonus disponibili. Ad esempio, potresti avere un bonus che modifica il valore aggiunto o il moltiplicatore nel tempo. Puoi anche usare i bonus negativi (che il codice attuale può già gestire).

Con qualsiasi sistema, c'è sempre più che puoi aggiungere. Ecco alcuni miglioramenti suggeriti che potresti apportare:

  • Identifica gli attributi per nome
  • Crea un sistema "centralizzato" per la gestione degli attributi
  • Ottimizza le prestazioni (suggerimento: non è sempre necessario calcolare il valore finale interamente)
  • Consentire ad alcuni bonus di attenuare o rafforzare altri bonus

Grazie per aver letto!