Introduzione

Nella lezione di oggi andremo a progettare un modello di dati rappresentante uno scenario videoludico abbastanza tradizionale, ovvero quello di un RPG fantasy. Ovviamente, ciò che approfondiremo, sarà di stampo estremamente didattico, ragion per cui non verranno affrontate tutta una serie di problematiche e complessità presenti nello sviluppo reale di un videogioco. L’esempio in questione ci permetterà, nella fattispecie, di applicare in un contesto pratico gli argomenti di cui si è parlato precedentemente, come ad esempio: classi, ereditarietà, polimorfismo, enum e properties.

Lo sviluppo verrà effettuato solo tramite C#, senza implementare Unity, per cui come interfaccia di gioco si avrà esclusivamente l’interfaccia testuale di Visual Studio Code. Il gameplay di base è molto semplice e, come già detto, ricalca in tutto e per tutto il modello classico di un RPG fantasy: il nostro personaggio giocante Hero dovrà affrontare una serie di nemici, descritti dalla classe Enemy, e per farlo potrà ricorrere a tutta una serie di oggetti disponibili nel suo inventario.

Struttura

Iniziamo dando una descrizione delle strutture dati che andremo ad utilizzare.

Partiamo dalla classe abstract Person, la quale ci modella il concetto base di persona: perché la dichiariamo come astratta? Principalmente perché all’interno del gioco non vogliamo poter istanziare una persona ma, anzi, vogliamo poter istanziare determinati tipi di persona. Questi tipi di persona, come riportato dal diagramma UML qui sopra, sono essenzialmente le due classi che estendono la classe di base Person, ovvero: il nostro eroe, definito dalla classe Hero, e il generico nemico che dovremo affrontare, definito dalla classe Enemy. Notiamo anche di come la classe Person venga messa in relazione a due enum: Race e Alignment. Race, la razza, ci rappresenta la classica razza da fantasy, come ad esempio: umano, orco, goblin o elfo; mentre Alignment, l’allineamento, ci delinea l’atteggiamento morale ed etico, come ad esempio: buono, cattivo o neutrale. Entrambi questi enum ci serviranno a dare più credibilità alle istanze di Hero ed Enemy che andremo a creare, consentendoci di variare razza ed allineamento a seconda dei nostri gusti personali.

Nella classe Hero, che abbiamo visto estendere Person, possiamo notare di come ci sia anche una relazione verso un’altra classe, ovvero la classe Inventory.
Questa classe Inventory, come dice il nome, ci rappresenta l’inventario di oggetti disponibili al nostro protagonista, i quali a livello di codice vengono definiti come una lista di InventoryItem. Questi InventoryItem sono degli elementi che servono ad associare la quantità di un determinato oggetto con l’oggetto di riferimento, quest’ultimo rappresentato nell’esempio dalla classe Item.
La classe Item non è astratta, è una classe concreta, per cui ci consente di avere nell’inventario degli oggetti generici e non specifici. Vediamo, però, che la classe Item ha anche tre classi che la estendono e che, in questo caso, la “specializzano” in tre sottocategorie: Weapon, Armor e Spell. Ognuna di queste sottoclassi, per cui, eredita gli attributi di Item e ne aggiunge di altri a seconda del tipo.

Avremo, infine, una classe chiamata BattleManager, la quale è in relazione con Hero, Enemy ed Item. In questa classe andremo ad implementare il flusso di attacco e difesa, consentendoci così di gestire il combattimento tra il protagonista Hero ed un generico Enemy.

Le classi Shop, TradingManager e ShopOwner verranno tralasciate per il momento.

Implementazione classe Person

Chiaramente, ci sono un’infinità di modi per realizzare un’implementazione di questa gerarchia di classi, quella che andremo a vedere è soltanto un esempio di quella che potrebbe essere un’implementazione tipo.

Come già detto in precedenza, la classe Person è stata definita come astratta al fine di impedire di istanziare direttamente da codice una classe di tipo Person, favorendo così la creazione di sottoclassi istanziabili come Hero o Enemy.

Nel caso di esempio, come attributi della classe sono stati inseriti, prima di tutto: un nome, una razza ed un allineamento; a questi si vanno poi ad aggiungere quattro valori di tipo float per definire: la vita attuale dell’istanza, il mana attuale, la vita massima e il mana massimo.

Implementazione classe Hero

Questa classe estende la classe Person e, nel nostro caso, trattasi della classe rappresentante il protagonista, verrà istanziata una sola volta a runtime. Estendendo Person, questa classe eredita di conseguenza tutti gli attributi precedentemente descritti nell’esempio di implementazione di Person, ai quali va poi ad aggiungere una serie di parametri aggiuntivi. Nel costruttore di Hero, per cui, avrò i parametri standard della classe Person (nome, razza, allineamento, indicatori vita e mana) e, nel caso d’esempio, passerò anche la property intrinseca della classe Hero, ovvero l’Inventory, la quale ci rappresenterà l’inventario del nostro eroe. L’inventario viene definito come read only visto che, una volta istanziato, quell’inventario non verrà più reinizializzato ma sarà sempre lo stesso. Tra i metodi aggiunti alla classe Hero, ci sono una serie di funzioni atte al consumo di items, aggiunta di items all’inventario e quant’altro.

Implemetazione classe Enemy

Per quanto riguarda la classe Enemy, non c’è molto da dire. Nell’esempio viene implementato un costruttore che accetta come parametri in ingresso: un nome, una razza ed assegna in maniera assolutamente arbitraria l’allineamento caotico malvagio. Ovviamente avrebbe più senso inizizializzare l’allineamento con un criterio diverso, magari basandolo sulla razza passata in input ma, per un motivo puramente didattico, lo passeremo fisso.

Arrivati a questo punto abbiamo quindi modellato la parte superiore dell’UML, avendo così il concetto astratto di Person ereditato da due sottoclassi: Hero ed Enemy.

Implemetazione classe Inventory

Qui di seguito elenchiamo i passi atti all’implementazione della classe Inventory, passando prima per la classe Item con le sue specializzazioni e poi per la classe InventoryItem.

iniziamo a ragionare sugli Item, cercando di capire quali attributi definiscono un Item, ovvero: nome e descrizione.

Questi attributi, però, ci definiscono un Item molto generico, per cui vi è la necessità di specializzarlo, nell’esempio viene specializzato con delle armi, le quali possiedono ognuna un determinato attacco. Per cui, ogni classe Weapon avrà le property di intrinseche di Item più la property damage, la quale rappresenta il danno inflitto dall’arma.

Quando si pensa alle armi, si pensa anche alle armature, per cui oltre alla specializzazione di Item in Weapon, si è deciso di specializzarlo anche in una classe che definisce un’armatura, la quale chiaramente offre una determinata difesa. É stata quindi creata una classe Armor che va ad aggiungere alle properties della classe Item la property dell’ammontare di difesa offerto.

Oltre a queste due specializzazioni, ne è stata implementata una terza, ovvero una classe per le magie, le quali hanno un danno (damage) ed una quantità di mana necessario affinché possano essere utilizzate. Questo ci ha fatto quindi creare la classe Spell, la quale avrà quindi tutte le caratteristiche di Item più le proprietà pertinenti al danno e alla quantità di mana. 

Una volta modellate queste 4 classi, ci possiamo porre il problema di come mettere questi Item nell’inventario.

A questo punto, ad esempio se vado da un ipotetico mercante a comprare un’arma, cosa succede: con le classi che abbiamo realizzato al momento possiamo creare un’istanza di Weapon, ad esempio, che necessita di essere inserita nell’inventario. Fintanto che ne istanziamo solo una, non ci sono problemi ma, nel momento in cui ne vogliamo acquistare più di una, ci accorgiamo di come la quantità possa essere maggiore di uno e di avere quindi necessità di una classe di supporto. Una volta appurato questo, mi immagino quindi di creare una classe InventoryItem, in modo tale da associare ad ogni Item una determinata quantità. Una volta definita, la nostra classe InventoryItem possiede una quantità ed un riferimento ad un Item, per cui, grazie ad essa, riusciamo ad esprimere, passandoli tramite costruttore, il connubio oggetto-quantità. Il parametro della quantità è di fatto opzionale, per cui nel momento in cui non lo dovessimo passare, daremo per scontato che il valore associato sia uno. Come ultima aggiunta, è conveniente inserire nella classe una serie di metodi per interagire con le sue proprietà, come ad esempio aumentare e diminuire la quantità dell’Item associato, capire se un Item è esaurito etc…

Alla fine, la classe Inventory, avrà quindi al suo interno una lista di InventoryItem, inizializzata dentro al costruttore di Item nella classe Hero. Come classe avrà anch’essa al suo interno una serie di metodi per gestire l’inventario con certe accortezze, come ad esempio: se possediamo già un Item nell’inventario, aggiungeremo la quantità e basta; se consumiamo un determinato Item, andremo a cercare nell’inventario se esiste per poi fare il decremento della quantità, la quale a sua volta se arriverà a zero ci delineerà la rimozione dell’Item dall’inventario.

Modellare tutto tramite classi, è un vantaggio in fatto di robustezza del codice e manutenibilità del suddetto, per cui, anche se si pensa che una classe sia eccessiva per esprimere un concetto, è sempre consigliato implementarla per trarne comunque giovamento.