✚ Sviluppo videogiochi

Entity Component System: organizzare il codice di un gioco

Cos'è l'architettura Entity Component System, perché si preferisce all'ereditarietà profonda e come entità, componenti e sistemi collaborano.

Quando un gioco cresce, la domanda più difficile non riguarda la grafica o la fisica, ma l'organizzazione del codice. Come rappresentare un nemico che vola, uno che spara, uno che vola e spara, una cassa che si può distruggere e una che si può anche spingere? Il modello Entity Component System (ECS) nasce proprio per rispondere a questa domanda in modo flessibile.

Il problema dell'ereditarietà profonda

L'approccio intuitivo, soprattutto per chi viene dalla programmazione a oggetti classica, è costruire una gerarchia di classi:

Oggetto
 └─ Personaggio
     ├─ Giocatore
     └─ Nemico
         ├─ NemicoVolante
         └─ NemicoCheSpara

Il guaio arriva quando serve un nemico che vola e spara. Bisogna duplicare codice, ricorrere all'ereditarietà multipla o spostare tutto nella classe base, che diventa enorme e piena di comportamenti che la maggior parte delle sottoclassi non usa. Dopo qualche mese di sviluppo la gerarchia diventa rigida: ogni nuova idea del game designer richiede di ristrutturarla.

Composizione al posto di ereditarietà

ECS ribalta la prospettiva. Invece di chiedersi «che cosa è» un oggetto, ci si chiede «che cosa ha». Il modello si basa su tre concetti.

Entità

Un'entità è soltanto un identificatore, spesso un numero intero. Non contiene dati né logica: è un'etichetta a cui si agganciano componenti.

Componenti

Un componente è un piccolo contenitore di dati puri, senza comportamento:

  • Posizione { x, y }
  • Velocita { vx, vy }
  • Sprite { immagine, fotogramma }
  • Salute { punti }
  • Volante { quota }
  • Arma { cadenza, danno }

Un nemico che vola e spara è semplicemente un'entità con Posizione, Velocita, Sprite, Salute, Volante e Arma. Una cassa distruttibile ha Posizione, Sprite e Salute. Nessuna gerarchia, nessuna duplicazione.

Sistemi

Un sistema contiene la logica e opera su tutte le entità che possiedono una certa combinazione di componenti. Il sistema di movimento, per esempio, elabora tutte le entità con Posizione e Velocita:

per ogni entita con (Posizione p, Velocita v):
    p.x += v.vx * dt
    p.y += v.vy * dt

Il sistema di rendering considera le entità con Posizione e Sprite; quello dei danni le entità con Salute. Ogni sistema fa una cosa sola e la fa su tutti gli oggetti interessati.

I vantaggi pratici

Flessibilità

Aggiungere un comportamento significa aggiungere un componente. Se il game designer vuole che le casse possano essere spinte, basta dare loro Velocita e un componente Spingibile, poi scrivere il sistema corrispondente. Si possono anche aggiungere o togliere componenti durante il gioco: un nemico congelato perde temporaneamente Velocita, e il sistema di movimento smette automaticamente di spostarlo.

Prestazioni

Nelle implementazioni orientate ai dati, i componenti dello stesso tipo sono memorizzati in array contigui. Quando il sistema di movimento scorre tutte le posizioni e le velocità, il processore legge memoria sequenziale e sfrutta al meglio la cache. Con migliaia di entità la differenza rispetto a oggetti sparsi in memoria e raggiunti tramite puntatori può essere notevole.

Separazione delle responsabilità

Dati e logica sono separati in modo netto. Il salvataggio del gioco, per esempio, diventa più semplice: basta serializzare i componenti, che sono dati puri. Anche il test è più facile, perché un sistema si può verificare fornendogli poche entità costruite a mano.

Dove ECS è meno comodo

Non è una soluzione universale. Alcuni aspetti richiedono attenzione:

  • Relazioni tra entità: un'arma impugnata da un personaggio, un vagone agganciato a un treno. Si gestiscono con componenti che contengono l'identificatore di un'altra entità, ma bisogna verificare che l'entità collegata esista ancora.
  • Ordine dei sistemi: il sistema di input deve girare prima di quello di movimento, il movimento prima delle collisioni. L'ordine va definito esplicitamente.
  • Logica molto specifica: il boss finale con un comportamento unico può essere più leggibile come script dedicato che come insieme di componenti generici.
  • Curva di apprendimento: per un gioco piccolo, con poche decine di oggetti, ECS può sembrare una complicazione inutile.

ECS nei motori più diffusi

I motori adottano idee simili in forme diverse. Unity offre il modello classico a GameObject e MonoBehaviour, basato su componenti con logica al loro interno, e un'architettura orientata ai dati separata per i progetti che ne hanno bisogno. Godot si basa su un albero di nodi, che è una forma di composizione diversa ma con obiettivi simili. Esistono poi librerie ECS indipendenti per molti linguaggi, utili se si lavora con un framework leggero.

Un esempio di progettazione

Supponiamo di dover realizzare un piccolo sparatutto a scorrimento. Una possibile suddivisione:

Sistema Componenti richiesti
Input ControllatoDalGiocatore, Velocita
Movimento Posizione, Velocita
Sparo Arma, Posizione
Collisioni Posizione, Collisore
Danni Salute, eventi di collisione
Rendering Posizione, Sprite
Pulizia DaDistruggere

Il sistema di pulizia, eseguito per ultimo, rimuove le entità marcate durante il fotogramma. Distruggere entità mentre altri sistemi le stanno ancora scorrendo è una fonte classica di errori, ed è meglio rimandare l'operazione alla fine del ciclo.

In sintesi

  • ECS sostituisce le gerarchie di classi con la composizione: un oggetto è definito da ciò che ha.
  • Le entità sono identificatori, i componenti dati puri, i sistemi la logica.
  • Aggiungere comportamenti significa aggiungere componenti, anche durante il gioco.
  • I componenti in array contigui migliorano le prestazioni con molte entità.
  • Ordine dei sistemi, relazioni e distruzione delle entità vanno progettati con cura.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche