Molti nemici dei videogiochi sembrano intelligenti pur seguendo regole molto semplici: pattugliano, notano il giocatore, lo inseguono, attaccano, fuggono quando sono feriti. Lo strumento più usato per organizzare questi comportamenti è la macchina a stati finiti (finite state machine, FSM): un modello in cui l'entità si trova sempre in uno solo di un numero limitato di stati, e passa dall'uno all'altro in base a condizioni precise.
Gli elementi di una macchina a stati
- Stati: le modalità di comportamento, come «pattuglia», «inseguimento», «attacco», «fuga».
- Transizioni: le regole che portano da uno stato all'altro, per esempio «se vede il giocatore, passa a inseguimento».
- Azioni: ciò che il nemico fa mentre si trova in uno stato, a ogni aggiornamento.
Un esempio per una guardia:
| Stato | Comportamento | Transizioni |
|---|---|---|
| Pattuglia | Cammina tra punti prefissati | Vede il giocatore → Inseguimento |
| Inseguimento | Si muove verso il giocatore | A distanza di attacco → Attacco; perde il giocatore → Ricerca |
| Attacco | Colpisce a intervalli | Giocatore lontano → Inseguimento; salute bassa → Fuga |
| Ricerca | Va all'ultima posizione nota | Vede il giocatore → Inseguimento; tempo scaduto → Pattuglia |
| Fuga | Si allontana dal giocatore | Salute recuperata → Pattuglia |
Disegnare questa tabella, o un diagramma con cerchi e frecce, prima di scrivere codice aiuta a scoprire casi dimenticati: che cosa fa la guardia se viene colpita alle spalle mentre pattuglia?
La versione più semplice
Per pochi stati basta una variabile e un costrutto di selezione:
switch (stato) {
case PATTUGLIA:
muovi_verso(prossimo_punto)
if (vede_giocatore()) stato = INSEGUIMENTO
break
case INSEGUIMENTO:
muovi_verso(giocatore)
if (distanza(giocatore) < portata) stato = ATTACCO
else if (!vede_giocatore()) stato = RICERCA
break
...
}
È leggibile e veloce. Il limite emerge quando gli stati crescono: il blocco diventa lungo e le operazioni da fare entrando o uscendo da uno stato si sparpagliano.
Ingresso, aggiornamento, uscita
Una struttura più robusta tratta ogni stato come un oggetto con tre metodi:
entra(): eseguito una sola volta all'ingresso, per esempio per avviare l'animazione, emettere un suono di allarme o azzerare un timer;aggiorna(dt): eseguito a ogni passo, contiene il comportamento e verifica le transizioni;esci(): eseguito all'uscita, per ripulire, per esempio fermare un effetto sonoro in ciclo.
La macchina si limita a chiamare esci() sullo stato attuale, sostituirlo e chiamare entra() sul nuovo. In questo modo ogni stato è autonomo, testabile e riutilizzabile tra nemici diversi.
Percezione: vedere e sentire
Le transizioni dipendono da ciò che il nemico «percepisce». Le verifiche più comuni:
- distanza: il giocatore è entro un certo raggio?
- cono visivo: l'angolo tra la direzione dello sguardo e la direzione verso il giocatore è minore di metà dell'ampiezza del cono? Si calcola con il prodotto scalare tra vettori normalizzati.
- linea di vista: un raggio dal nemico al giocatore incontra muri? Si verifica con un raycast sulla mappa.
- udito: rumori come spari o passi di corsa generano eventi con un raggio entro cui i nemici reagiscono.
Questi controlli possono essere costosi. Non serve eseguirli a ogni fotogramma: farli alcune volte al secondo, distribuendo i nemici su fotogrammi diversi, è quasi sempre sufficiente e rende anche il comportamento più umano, con un piccolo tempo di reazione.
Evitare le oscillazioni
Un problema tipico: il giocatore si trova esattamente al limite della distanza di attacco e il nemico alterna attacco e inseguimento a ogni fotogramma. Due rimedi:
- isteresi: soglie diverse per entrare e uscire, per esempio entra in attacco sotto i 2 metri ed esce sopra i 2,5;
- durata minima: uno stato non può essere abbandonato prima di un certo tempo.
Macchine a stati gerarchiche
Quando un nemico ha molti stati, alcuni condividono transizioni. Per esempio, da qualsiasi stato di combattimento, se la salute scende sotto una soglia, deve fuggire. Ripetere la stessa transizione ovunque è scomodo e fragile.
Una macchina a stati gerarchica raggruppa gli stati in superstati. Il superstato «combattimento» contiene «inseguimento», «attacco» e «schivata», e definisce una sola volta la transizione verso «fuga». Gli stati interni ereditano il comportamento comune.
Quando passare ad altro
Le macchine a stati funzionano molto bene per nemici con comportamenti chiari e un numero ragionevole di stati. Quando le combinazioni esplodono, o si vuole dare priorità dinamiche a molti obiettivi, si valutano alternative come i behavior tree o i sistemi basati sull'utilità. Spesso però una FSM ben disegnata è tutto ciò che serve, e la sua prevedibilità è un pregio: il giocatore impara i comportamenti dei nemici e li sfrutta, cosa che fa parte del divertimento.
In sintesi
- Una macchina a stati finiti tiene il nemico in un solo stato alla volta, con transizioni esplicite.
- Una tabella o un diagramma degli stati aiuta a scoprire i casi dimenticati.
- Metodi di ingresso, aggiornamento e uscita rendono ogni stato autonomo.
- Percezione per distanza, cono visivo e linea di vista guida le transizioni.
- Isteresi e stati gerarchici evitano oscillazioni e duplicazioni.