Game Development: Architettura Component-Based

Dopo la prima introduzione al Game Development iniziamo ad affrontare qualche tema interessante. Mi sono preso un po’ di tempo per realizzare questo articolo perché sebbene semplice, contiene alcuni concetti delicati che ad una prima lettura possono sembrare inutili sebbene in realtà possano veramente cambiarvi la vita (almeno quella informatica :D). L’articolo fa uso di immagini ed esempi presi da questo topic, quindi è bene dare a Cesare ciò che è Cesare e il giusto riconoscimento al suo autore.

Uno degli aspetti più complessi della progettazione di un videogioco è la formalizzazione del mondo. Con formalizzazione del mondo si intende il processo di sintetizzare in modo formale i vari aspetti di un ambiente o modello che si vuole rappresentare, elencando gli oggetti rilevanti e le relazioni fra gli stessi.

Videogiochi anche banali hanno un gran numero di oggetti. Prendiamo ad esempio un platform come Super Mario Bros. Il gioco è concettualmente semplice ma contiene un bel gruzzolo di oggetti: Mario, i mostri (ognuno con un suo comportamento), i proiettili (martelli e palline di fuoco), tubi, attivatori, power-up e molto altro. Tutti questi oggetti hanno i loro comportamenti caratteristici, i loro effetti e le loro proprietà. Per questo strutturare un “diagramma delle classi” che catturi il mondo da modellare in modo preciso può diventare un lavoro complesso e frustrante.

L’APPROCCIO GERARCHICO

Supponiamo che voi e il vostro team stiate progettando un piccolo gioco RPG-like. Se siete programmatori con un po’ di esperienza e avete dato esami di “Progettazione del Software” nella vostra vita, la prima cosa che vi verrà in mente di fare è di progettare la gerarchia delle classi con lo scopo di duplicare il meno possibile il codice necessario e relazionare gli oggetti fra loro in modo che siano coerenti con l’idea che abbiamo in mente.

Una volta individuati gli elementi principali del vostro gioco (ad esempio tavoli, mobilia, porte, NPC, oggetti e il giocatore) si comincia con il vedere quali proprietà/abilità/caratteristiche hanno in comune disponendoli all’interno di una gerarchia in cui il figlio eredita tutte le caratteristiche del padre (ed eventualmente ne aggiunge di nuove).

Con riferimento all’esempio del gioco RPG, gli oggetti si possono dividere fra oggetti “fissi” (come le porte, i tavoli e gli scaffali) e oggetti “dinamici” che possono muoversi e interagire (persone, animali e pozioni). A loro volta gli oggetti dinamici si dividono fra “item” (pozioni e armi) e personaggi in grado di parlare (come gli NPC, negozianti, mostri e il giocatore). Il risultato è molto probabilmente lo schema nella figura precedente. Lo schema funziona, è coerente con il mondo che progettavate e potete cominciare ad usarlo per programmare il vostro gioco. Tutti sono felici.

Poiché è innaturale e improbabile che tutto il gioco sia definito nei minimi dettagli prima di cominciare a programmare è possibile (diciamo certo) che ad un certo punto dello sviluppo i game designer vi chiamino dicendo: “Abbiamo pensato di inserire in questa mappa una porta parlante!” Voi nello schema precedente avete assunto che le porte non parlino, quest’idea dei game designer è interessante ma va in conflitto con il mondo che avete modellato! Sfortunatamente la porta parlante è una parte fondamentale della storia che i game designer si rifiutano di modificare. Cosa fate?

Per vostra sfortuna lo schema precedente è fisso! Le porte, per come le avete definite non si muovono e soprattutto non parlano. Lo schema gerarchico è rigido e pieno di assunzioni. Le soluzioni a questo punto sono poche:

  • Aggirare il problema: la porta è in realtà un NPC con le sembianze di una porta, con vita infinita e inamovibile che “muore” quando viene aperta per poi “resuscitare” quando viene chiusa. Inquietante. No. Questa soluzione non è sufficientemente robusta ed elegante.
  • Decidete che le porte possono parlare. Per fare ciò viene duplicato il codice di Talkable_Entity all’interno di Door_Entity. Questo però duplica il codice andando contro almeno sessantamila principi della programmazione (oltre che vanificare lo scopo dello schema precedente) il che non è desiderabile. In particolare stiamo sporcando il codice di tutte le porte per far parlare una porta che compare una ed una sola volta nel gioco!
  • Si può spostare l’abilità di parlare da Talkable_Entity a Entity (o ad una super-classe qualsiasi in comune fra la porta e le Talkable_Entity). Ma questo non ha molto senso! In questo modo tutti gli oggetti possono potenzialmente parlare! Non solo la porta che voglio io ma anche scaffali, sedie e latrine.
  • Oppure si può modificare l’intero schema in modo che tenga conto anche dell’evenienza di una porta parlante con conseguente riscrittura di gran parte del codice.

Poiché la porta parlante sarà solo la prima delle decine di eccezioni e entità fantasiose partorite dai vostri game designer, scoprirete presto che vi troverete a riscrivere il codice del vostro motore di gioco decine di volte solo per riadattarlo ad uno schema gerarchico in continua evoluzione!

In un gioco mediamente complesso vi ritroverete infine con centinaia e centinaia di classi, con fumose dipendenze e gerarchie. La complessità tecnica del gioco va alle stelle. Voi impazzite e il gioco finisce nel dimenticatoio.

L’APPROCCIO COMPONENT-BASED

La soluzione però esiste e per afferrarla è necessario dimenticarsi completamente di tutto ciò che sapete sulla progettazione del software. Per il solo fatto di parlarvene potrei essere bastonato dai vostri professori.

Ora seguitemi bene perché la questione è semplice ma sfuggente! Tornando al problema precedente una soluzione non ortodossa deriva dalla soluzione numero 3. Spostiamo la capacità di parlare da Talkable_Entity a Entity e aggiungiamo al costruttore un flag (ad esempio canTalk) in modo tale da poter scegliere durante la sua inizializzazione se l’oggetto può parlare oppure no. Una cosa di questo tipo:

Class Entity
{
   ....
   ...
   Talkable talkSystem;
   boolean canTalk;

   ...
   public Entity(boolean canTalk, Talkable talkSystem){
        this.canTalk = canTalk;
        this.talkSystem = talkSystem;

   }

   public Talkable getTalk()
   {
       if(canTalk)
       return talkSystem;
   }

..
}

A questo punto però ci rendiamo conto che possiamo usare la stessa tecnica per ogni componente dei nostri oggetti. Possiamo spostare in Entity la capacità di un oggetto di muoversi, la capacità di sparare, la capacità di essere raccolta, la capacità di ferire il giocatore, la capacità di essere distrutta e così via. Potete aggiungere ogni componente che vi pare basta aggiungere a Entity un flag e il rispettivo ComponentSystem.

Coa abbiamo ottenuto con questo? Ora abbiamo un unica classe per tutti gli oggetti del gioco (Entity) più una classe per ogni componente.

Notate che siamo passati da un approccio classico in cui gli oggetti sono caratterizzati dalla relazione is-a, oggetti diversi sono identificati dalla loro classe e dalle relazioni di ereditarietà che li lega, ad un’approccio basato sulla relazione “has-a“, in cui ogni oggetto è identificato dalle componenti che possiede.

Ovvero: ogni oggetto è definito unicamente dall’aggregato delle sue componenti!

Lo schema qui sopra rappresenta l’approccio a componenti di un piccolo gioco FPS e può aiutarvi a chiarificare il concetto. Primo: tutti gli oggetti nel gioco appartengono ad un unica classe (solitamente chiamata Entity. Secondo ogni entità può contenere una o più Componenti che definiscono le funzionalità del singolo oggetto. Ad esempio una granata ha una posizione, può muoversi, viene renderizzata, può essere un target e subisce l’influenza della fisica del gioco. Similmente il giocatore avrà molte capacità in comune con la granata ma non può essere un target (il giocatore non può spararsi da solo). E così via.

IN PRATICA

Una volta capito il concetto di approccio a componenti vediamo come può venir implementato. Cominciamo dalla classe Component. Ai fini di rendere l’architettura il più elastica, scalabile e coerente possibile, tutte le componenti devono mostrare la stessa interfaccia.

public abstract class Component
    {
        /* Tutte le componenti hanno una Entity come genitore*/
        public abstract Entity getParent();
        public abstract void setParent(Ent p);

        /* Tutte le componenti hanno un tipo unico (position, movement, etc..) */
        public abstract int getType();

        /* Tutte le componenti possono fare qualcosa per modificare la logica di gioco.
           Ogni classe Entity invoca update() su tutte le componenti. */

        public abstract void update(GameTime delta);

        /* Tutte le componenti possono renderizzare qualcosa sullo schermo.
           Ogni classe Entity invoca render() su tutte le componenti.*/

        public abstract void render(GameTime delta);

        /* Se una componente ha necessità di risorse quali immagini, suoni, ecc..
           possono essere caricati da questo metodo. Quando una Entity
           viene inizializzata essa carica le risorse di tutte le sue componenti.*/

        public abstract void loadAssets();

        /*Quando un'entità viene rimossa vanno rimosse anche le componenti
          collegate. */

        public abstract void die();
    }

Questa è una classe astratta e quindi tutti i metodi vanno “riempiti” dalle singole componenti che ereditano dalla classe madre “Components”. Il tipo di un componente (come Talkable, Physics, Movements, e così via) serve ad identificare il componente e può essere identificato da un intero, come in questo esempio di classe Components, oppure in qualunque altro modo vogliate (stringhe, decimali, pittogrammi e chi più ne ha più ne metta).

Ora vediamo la classe Entity. Poiché la classe Entity altro non fa che fungere da “aggregatore” di componenti, l’implementazione risulta molto elementare:

Primo, la classe contiene un hash-table (o qualunque altra struttura efficiente) in grado di memorizzare la lista delle componenti dell’entità. Secondo implementa questi due metodi:

public boolean hasComponent(int identifier)
{
   return component_table.Contains(identifier);
}
public Component getComponent(int identifier)
{
   if(hasComponent(identifier))
     return component_table.Get(identifier);

   else return null;
}

La classe Entity inoltre fa da tramite per la comunicazione fra componenti (anche se esistono approcci in cui due componenti possono comunicare direttamente).

VANTAGGI

Supposto per assurdo che quello che vi ho raccontato non bastasse, ecco una lista di vantaggi dell’architettura component-based.

  • Scalabilità: non dovrete più conoscere a priori tutti gli elementi del vostro gioco, vi basta sapere quali componenti vi servono. Aggiungere oggetti o componenti si riduce ad aggiungere una classe e aggregare nel modo che più vi aggrada i componenti esistenti!
  • Flessibilità: l’architettura è molto versatile. Talmente versatile che potete creare e modificare oggetti a runtime e farlo fare anche a chi non è pratico di programmazione (sfruttando ad esempio dei file XML in cui ogni oggetto viene definito semplicemente come una lista di componenti). Magari in qualche altro articolo vi farò vedere un esempio.
  • Riusabilità: è molto comune che due giochi abbiano componenti in comune. Con il sistema component-based vi basta importare le classi componenti da un gioco all’altro e tutto funzionerà alla perfezione.

CONTRO

Ogni architettura ha i suoi vantaggi e svantaggi. Nel caso dell’architettura component-based il principale svantaggio consiste nel rispondere alla domanda “Questa particolare entità che oggetto è?”. Poiché gli oggetti sono tutti Entity e l’unica cosa che permette di discriminare fra loro sono le componenti che possiedono, se voglio identificare un oggetto devo mettermi la coscienza in pace e prepararmi ad una serie stremante di if-else per controllare la presenza di un particolare sotto-insieme di componenti. Nell’approccio tradizionale tutto questo si ridurrebbe nel farsi restituire il nome della classe dell’oggetto.

Nello sviluppo di videogiochi questo non è un grande problema ed è comunque controbilanciato dagli innumerevoli vantaggi che l’architettura component-based offre. Però se volete usare quest’approccio (o uno ibrido) per qualche altra tipologia di applicazioni vi conviene tenere a mente questo problema e valutare se sia conveniente o meno per il vostro scenario.

One comment on “Game Development: Architettura Component-Based

  1. Probabilmente il tuo libro/prof di “Progettazione del soft.” non è dei migliori. Almeno nel c++ sviluppare classi basandosi quasi esclusivamente sull’ereditarietà non è assolutamente consigliato.