Skip to main content

Perché dovresti evitare l’ereditarietà nel tuo videogioco (e preferire i componenti)

Tra qualche giorno sarà passato un anno dal lancio di Luminetic Land (il mio primo videogioco) su App Store. Sviluppare un gioco in ogni suo aspetto espone a tante problematiche dello sviluppo software, una delle più ricorrenti era: come mantenere il codice il più chiaro ed essenziale possibile?

 

Luminetic Land

 

Il problema del codice ripetuto

Generalmente parte del codice che rappresenta un videogioco è dedicato alle entità che approssimativamente possono coincidere con gli sprite. In Luminetic Land sono presenti queste entità:

  • Bomb
  • Boulder
  • ColoredBomb
  • ColoredMagnet
  • Crate
  • Grass
  • Ground
  • IceBlock
  • Lamp
  • Light
  • Magnet
  • MagnetLightSensitive
  • OscillatingToken
  • PipeTop
  • Stone
  • Token
  • TractorBeam

Ad ognuna di queste entità corrisponde una classe in Objective-C.

Molte di queste entità condividono delle funzionalità. Ad esempio quelle evidenziate in rosso qui di seguito sono entità che possono essere teletrasportate quando finiscono in un tubo.

  • Bomb
  • Boulder
  • ColoredBomb
  • ColoredMagnet
  • Crate
  • Grass
  • Ground
  • IceBlock
  • Lamp
  • Light
  • Magnet
  • MagnetLightSensitive
  • OscillatingToken
  • PipeTop
  • Stone
  • Token
  • TractorBeam

La prima soluzione: usare l’ereditarietà. Invece di ripetere il metodo prepareForTeleport in ognuna di queste classi mi sembrava una buona idea:

  1. creare una classe astratta Teleportable (solo concettualmente dato che in Objective-C non esistono classi astratte)
  2. aggiungere il metodo prepareForTeleport a Teleportable
  3. lasciare che le 6 classe evidenziate in precedenza estendessero Teleportable

Dopo questa soluzione la struttura delle classi appariva organizzata nel modo seguente.

  • Boulder
  • ColoredMagnet
  • Grass
  • Ground
  • Light
  • Magnet
  • MagnetLightSensitive
  • OscillatingToken
  • PipeTop
  • Token
  • TractorBeam
  • Teleportable
    • Bomb
    • ColoredBomb
    • Crate
    • IceBlock
    • Lamp
    • Stone

La situazione era esattamente quella voluta, infatti:

  1. non c’era replicazione di codice: prepareForTeleport era solo nella classe “astratta” Teleportable
  2. potevo facilmente individuare se una classe rappresentasse un’entità teletraportabile: bastava verificare se la classe estendesse Teleportable

Naturalmente c’erano tante altre funzionalità ripetute quindi lo stesso approccio poteva essere ripetuto. Ad esempio alcune entità sono “sensibili alla luce”, ovvero devono eseguire un qualche comportamento se vengono illuminate da un fascio di luce

Le entità sensibili alla luce sono quelle in blu:

  • Boulder
  • ColoredMagnet
  • Grass
  • Ground
  • Light
  • Magnet
  • MagnetLightSensitive
  • OscillatingToken
  • PipeTop
  • Token
  • TractorBeam
  • Teleportable
    • Bomb
    • ColoredBomb
    • Crate
    • IceBlock
    • Lamp
    • Stone

Il problema con l’ereditarietà

A questo punto il problema con l’ereditarietà iniziava a prendere forma. Vediamo come cosa stava succedendo. MagnetLightSensitive e ColoredBomb sono 2 classi che condividono una porzione di codice, in particolare il metodo highlightedBy.

Seguendo l’approccio precedendo bisogna:

  1. creare una classe “astratta” LightSensitive in cui inserire il metodo highlightedBy
  2. rendere MagnetLightSensitive e ColoredBomb sottoclassi di LightSensitive

Tuttavia c’è un vincolo: ColoredBomb deve continuare a estendere Teleportable.

Quindi dove bisognerebbe collocare LightSensitive all’interno della gerarchia delle classi?

Una soluzione è questa:

  • Boulder
  • ColoredMagnet
  • Grass
  • Ground
  • Light
  • Magnet
  • OscillatingToken
  • PipeTop
  • Token
  • TractorBeam
  • Teleportable
    • LightSensitive
      • ColoredBomb
      • MagnetLightSensitive
    • Bomb
    • Crate
    • IceBlock
    • Lamp
    • Stone

Questo approccio presenta almeno 2  problemi:

  1. MagnetLightSensitive adesso ha Teleportable nella sua lista di superclassi ma questo è un errore, questa classe non dovrebbe essere di tipo Teleportable e non dovrebbe ereditare il codice di questa classe.
  2. Da questo momento ogni classe di tipo LightSensitive avrà anche le funzionalità di tipo Teleportable. Questi 2 concetti sono slegati in teoria ma a causa di una cattiva organizzazione delle classi ora sono strettamente connessi.

Provando altre “combinazioni” si giunge comunque a una situazioni inconsistenti (o potenzialmente tale). Rendere Teleportable sottoclasse di LightSensitive ad esempio produrrà una situazione analoga.

I componenti

Durante questa fase dello sviluppo trovai in rete l’articolo Evolve Your Hierarchy che Mick West scrisse nel 2007 in cui spiegava come durante lo sviluppo della serie Tony Hawk si era trovato in una situazione simile alla mia.

 

Tony Hawks

 

Ancora meglio, proponeva una soluzione che mi portò a riorganizzare il codice in un modo tale da:

  1. avere zero codice replicato
  2. ogni entità può avere tutte e sole le funzionalità di cui ha bisogno (ogni permutazione è possibile)
  3. ogni funzionalità è isolata dalle altre, questo rende il refactoring e il debugging molto veloce

L’idea si articola su 3 concetti:

  1. si crea un component per ogni funzionalità
  2. si crea un protocol (interface per chi parla Java) per ogni component
  3. un entità deve semplicemente incorporare (relazione has-a) i component relativi alle funzionalità che vuole offrire e essere conforme (is-a) ai relativi protocolli / (implementare le relative interfacce)

Il primo passo è stato quello di individuare le funzionalità che almeno 2 entità condividevano e creare un protocollo (interfaccia) per ognuna di esse:

  1. LightSensitive
  2. Teleportable
  3. TouchSensitive
  4. CollisionSensitive

Poi i component associati ai protocolli:

  1. LightSensor
  2. Teleporter
  3. TouchEnabler
  4. PhysicsSprite

Seguendo l’esempio precedente ora:

  1. Teleportable dichiara prepareForTeleport
  2. Teleporer implementa prepareForTeleport

 

Light
Sensitive
Teleportable Touch
Sensitive
Collision
Sensitive
Bomb
Boulder
ColoredBomb
ColoredMagnet
Crate
Grass
Ground
IceBlock  ✓
Lamp
Light
Magnet
MagnetLightSensitive
OscillatingToken
PipeTop
Stone
Token
TractorBeam
LightSensor Teleporer TouchEnabler PhysicsSprite

 

Una volta impostata la struttura del codice in questo modo creare nuove entità (oltre che mantenere quelle già esistenti) è diventato molto più semplice. Durante la creazione di una nuova entità bastava dichiarare a quali protocolli dovesse essere conforme e poi seguire gli errori del compilatore fino ad aggiungere i componenti necessari e collegarli correttamente.

Ad esempio ora IceBlock:

  1. è conforme a (implementa) Teleportable
  2. contiene al suo interno un oggetto Teleporter
  3. ha un metodo prepareForTeleport che richiama il metodo analogo del Teleporter che contiene

Ora ogni entità in Luminetic Land è un aggregatore di componenti.

Conclusione

La versione finale di Luminetic Land contiene tante altre ottimizzazioni. Ad esempio in alcuni casi aveva comunque senso utilizzare l’ereditarietà (ColoredMagnet estende chiaramente Magnet e così via) ma si tratta di dettagli poco rilevanti ai fini di questo articolo. Spero che il problema e la soluzione esposti qui  siano di aiuto a chi si trova a dover impostare una struttura di classi per il proprio gioco.

Luminetic Land è disponibile gratuitamente su App Store.

 

https://www.youtube.com/watch?v=SvH0Kjk7b8s

Trainer • Developer • Writer

Luca Angeletti

Trainer • Developer • Writer

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *