Una delle branche più suggestive della computer science è probabilmente l'intelligenza artificiale, che molti associano alla capacità di un elaboratore elettronico di sviluppare un pensiero autonomo e che, invece, si occupa di modelli computazionali capaci di automatizzare la risoluzione di problemi o la risposta a stimoli esterni grazie a processi di approndimento e associazione, più o meno complessi.
In questo articolo esaminiamo alcuni semplici esempi di intelligenza artificiale nell'ambito dei videogiochi, forse il campo di applicazione dell'AI più vicino alla vita di tutti i giorni. Per capirci basta pensare a Pong con la barra del giocatore controllata dal computer che si muove in base alla direzione della pallina, o a Pacman con i fantasmi che inseguono il giocatore o fuggono da esso.
Questi sono esempi abbastanza basilari di intelligenza artificiale, ma praticamente ogni gioco può richiedere un diverso tipo di comportamento da parte degli elementi controllati dal computer, potrebbe quindi essere utile avere una libreria che ci permetta di utilizzare già alcuni comportamenti comuni senza che sia necessario crearne ogni volta uno ex-novo.
Ebbene, ancora una volta la community di Flash ci viene in aiuto, esiste infatti una serie di classi create da Colby Williams che può essere molto utile nella creazione di giochi e in particolare per l'associazione di alcuni comportamenti ai personaggi di un gioco. In realtà il suo progetto non è limitato solo alla parte di intelligenza artificiale ma, come possiamo vedere nella pagina Google Gode del progetto CheezeWorld ha creato altre classi, la maggior parte utili sempre nell'ambito di creazione di giochi.
Tutte le classi sono in Actionscript 3, non sono disponibili versioni in versione Actionscript 2 di questo framework. Gli esempi allegati all'articolo sono compatibili con Flash CS3, Flash CS4 e Flash CS5.
Come possiamo vedere nella pagina di presentazione della classe, i comportamenti messi a disposizione dalle classi prevedono anche curve e rotazioni, più difficili da realizzare rispetto a movimenti lineari.
Vediamo ora come utilizzare queste classi per creare in maniera molto rapida degli elementi con comportamenti "intelligenti" all'interno dei nostri filmati Flash!
Nella pagina seguente vedremo come scaricare ed installare il Framework
Download e installazione delle classi
Per prima cosa, dalla pagina dei download del progetto su Google Code scarichiamo il file SteeringBehaviors with examples.zip . Il file contiene un progetto Flex, ma vedremo come possiamo estrarre le classi per utilizzarle anche in Flash.
Esaminiamo la struttura del file ZIP scaricato: la cartella bin-debug
contiene il file ArtificialIntelligence.swf
, che è lo stesso presente nella pagina di presentazione delle classe e mostra i vari comportamenti disponibili.
La cartella src
è quella di maggior interesse poichè contiene tra le altre cose le classi da estrarre per usare la libreria. Nella cartella src
troviamo le cartelle AI
, assets
, com
e due file .as
; quello che ci interessa è la cartella com
, le altre directory infatti contengono gli elementi del filmato dimostrativo: assets
contiene l'swf dell'astronave, AI
contiene le classi delle varie schermate degli esempi, mentre i file ArtificialIntelligence.as
e Main.as
sono i file principali dell'esempio; è comunque utile dare un'occhiata alla struttura dell'esempio, dato che useremo questa struttura come riferimento per la stesura del nostro codice, tuttaviale classi di nostro interesse e in generale il framework risiede tutto nella cartella com
, che possiamo quindi estrarre e copiare nella cartella in cui creeremo il nostro esempio.
Principi base del framework
Il framework ci permette di descrivere i diversi comportamenti all'interno di "Schermate divise", questi oggetti Screen
sono organizzati in una struttura ad albero che possiamo definire a partire dalla classe Root.
Nella Root
viene inizializzato lo Sprite in cui prende vita l'ambiente del gioco ed alcuni eventi. A questo punto si possono inserire (con il metodo setScreen
) le diverse schermate. Gli elementi all'interno delle Screen vengono creati grazie all'oggetto GameFactory.
Possiamo quindi creare una nostra schermata con alcuni comandi (eventualmente estendendone altre già fatte) e poi richiamarla tramite il metodo setScreen
per eseguirla nell'ambito del framework. Quindi i passaggi, che poi approfondiremo con il codice, saranno questi:
- Otteniamo un'istanza per l'ambiente (un nuovo
GameFactory
) - Inseriamo gli eventuali elementi grafici
- Associamo agli elementi i diversi comportamenti
- Richiamiamo la nostra classe con
setScreen
Questi pochi passaggi, sono svolti attraverso una serie di classe ed elementi separati che interagiscono tra loro: questo richiede un tempo di preparazione del filmato leggermente superiore rispetto magari a soluzioni che prevedono una minor suddivisione dei compiti, tuttavia questo approccio consente una miglior gestione futura degli elementi e anche una maggior semplicità di modifica qualora volessimo variare alcuni comportamenti nel nostro filmato, come vedremo peraltro durante l'articolo.
Sempre per quanto concerne la separazione degli elementi (e quindi una più semplice manutenzione), in questo framework anche gli elementi grafici possono essere lasciati all'esterno del file principale: è infatti possibile esportare i vari SWF per gli elementi del gioco e caricarli poi all'interno del framework (e del FLA principale) grazie agli appositi comandi delle classi Assets
.
La struttura di un progetto
Analizzando più nello specifico i vari passaggi e compiti, la divisione che si può impostare con questo framework è di questo tipo:
- Creazione degli elementi grafici su uno o più file separati dal principale
- Impostazione di una classe per associare gli elementi grafici a delle variabili da usare nel framework
- Impostazione di una classe per la registrazione degli elementi nel motore di gioco, che interagirà con le variabili della classe precedente
- Creazione della classe principale delle schermate: creerà il motore di gioco e ne imposterà il rendering
- Creazione delle schermate, estenderanno la classe precedente e includeranno gli oggetti associando i comportamenti di intelligenza artificiale desiderati
- Classe che sarà associata come Document Class al FLA principale del progetto, conterrà le istruzioni per avviare la schermata desiderata
Per la struttura delle classi e dei file del nostro esempio ci ispireremo al filmato dimostrativo disponibile nell'archivio ZIP scaricato da Google Code, seppur con qualche variazione (principalmente cambieremo i nomi di alcune directory e avremo meno classi di tipo Screen
, poichè partiremo con 3 esempi tra i comportamenti disponibili).
Basandoci sulla struttura dell'esempio e tenendo presenti i passaggi appena descritti, avremo la seguente struttura:
Elemento | Posizione | Tipologia | Descrizione |
---|---|---|---|
assets |
/ |
cartella | contiene gli swf degli elementi grafici (separati dal file principale) |
IA |
/ |
cartella | contiene due sottocartelle: schermate (che conterrà a sua volta le classi degli esempi), e data |
data |
/IA/ |
cartella | conterrà le classi Assets ed Elementi , necessarie per associare gli oggetti grafici a delle variabili (questo avverrà in Assets.as ) e tramite esse registrare gli elementi nel motore di gioco (operazioni definite in Elementi.as ) |
schermate |
/IA/ |
cartella | conterrà la classe principale delle schermate (MainScreen.as )
in questa cartella avremo anche le classi delle schermate di esempio, 3 nel nostro caso, di cui la prima sarà |
Esempio.fla |
/ |
file | il file FLA principale |
Esempio.as |
/ |
file | che sarà la Document Class di Esempio.fla e conterrà i comandi per creare l'oggetto GameFactory e impostare il relativo Screen |
com |
/ |
cartella | contenente il framework, che mettiamo allo stesso livello del file FLA |
Impostiamo quindi le nostre cartelle, avviamo Flash e creiamo un nuovo file Actionscript 3 a cui diamo come document class Esempio
, quindi creiamo un nuovo file Actionscript che salveremo come Esempio.as
.
Creare gli elementi grafici
Prepariamo come prima cosa i simboli grafici che utilizzeremo per il nostro filmato. Nel file di esempio allegato all'articolo ci sono già un disegno in Flash (un quadrato) e un'immagine: basterà modificare i movieclip per ottenere un elemento grafico personalizzato. Per creare il file ex-novo seguiamo invece le operazioni seguenti.
Creiamo un nuovo file FLA Actionscript 3 e, al suo interno, andiamo a impostare un nuovo movieclip (Ctrl+F8 o Insert->New Symbol) impostando l'esportazione per Actionscript con classe Oggetto
.
Nota: se non vediamo le opzioni per l'esportazione Actionscript, premiamo il pulsante "Advanced" nel pannello di creazione del simbolo.
Disegnamo quindi all'interno del movieclip il simbolo grafico da utilizzare nel filmato e ripetiamo lo stesso procedimento per creare un altro movieclip, questa volta con classe Quadrato
.
Creati questi due clip salviamo il file nella cartella assets
, con nome assets.fla
ed esportiamo il filmato in modo da ottenere il file assets.swf
.
Ora realizziamo le due classi che si occuperanno di importare questi elementi grafici nel motore di gioco: la prima, Assets.as
(che sarà salvata in IA/data
) importerà semplicemente i simboli creati tramite l'SWF assets.swf
, mentre Elementi.as
(anch'essa in IA/data
) si occuperà di registrare i due elementi nel motore di gioco impostandone alcune proprietà (il motore di gioco verrà creato nella classe principale del package IA.schermate
che vedremo a breve).
In questo caso ci siamo basati su un solo SWF esterno per gli elementi grafici, chiaramente per progetti più ampi può essere utile suddividere la cosa in ulteriori file per facilitare la gestione dei vari elementi (per esempio un file per i nemici, un file per gli oggetti controllati dal giocatore, un file per gli elementi grafici, etc).
Vediamo allora il codice di Assets.as
, molto semplice:
package IA.data { public class Assets { [Embed(source="../../assets/assets.swf", symbol="Oggetto")] public static var Oggetto:Class; [Embed(source="../../assets/assets.swf", symbol="Quadrato")] public static var Quadrato:Class; } }
Con questa classe richiamiamo il simbolo esportato con classe Oggetto
dal file assets.swf
per poterlo utlizzare nel motore di gioco, dove verrà registrato grazie all'uso della classe Elementi.as
. Il simbolo esportato con classe Quadrato subirà il medesimo procedimento.
I due oggetti sono memorizzati in due variabili di tipo Class
. Useremo queste variabili (contenute nella classe Assets e rese accessibili anche alle altre classi grazie all'attributo public) per fare riferimento ai due oggetti.
La classe Elementi
si occuperà di caricare da Assets.as
le clip Oggetto
e Quadrato
e di associarle come grafiche ad alcuni elementi del motore di gioco, in modo da potervi poi assegnare i comportamenti di intelligenza artificiale.
In pratica all'interno del gioco verranno creati degli oggetti con determinate proprietà e comportamenti, a cui verrà poi associata la grafica ricavata dagli SWF. Anche questo meccanismo è comune a molti framework actionscript come ad esempio quelli per la fisica.
Ecco il codice della classe Elementi.as
:
package IA.data { import com.cheezeworld.GameFactory; import com.cheezeworld.entity.Boid; import com.cheezeworld.entity.BoidParams; import com.cheezeworld.entity.Entity; import com.cheezeworld.entity.EntityParams; import com.cheezeworld.entity.MovingEntity; import com.cheezeworld.rendering.EntityRenderer; import com.cheezeworld.rendering.SpriteRenderer; public class Elementi { public static function register( a_gameFactory:GameFactory ) : void { var bParams:BoidParams; var eParams:BoidParams; bParams = new BoidParams( { neighborDistance:100, maxSpeed:150, maxAcceleration:15, damping:.98, radius:15, maxTurnRate:360 } ); bParams.type = "Ogg1"; bParams.customClass = Boid; bParams.rendererClass = SpriteRenderer; bParams.rendererData.sprite = Assets.Oggetto; bParams.boundsBehavior = MovingEntity.BOUNDS_WRAP; a_gameFactory.registerEntityType( bParams ); eParams = new BoidParams( { neighborDistance:100, maxSpeed:150, maxAcceleration:15, damping:.98, radius:15, maxTurnRate:180 } ); eParams.type = "Ogg2"; eParams.customClass = Boid; eParams.rendererClass = SpriteRenderer; eParams.rendererData.sprite = Assets.Quadrato; eParams.boundsBehavior = MovingEntity.BOUNDS_WRAP; a_gameFactory.registerEntityType( eParams ); } } }
Notiamo due cose in particolare: la proprietà sprite
, cui abbiamo associato il valore Assets.Oggetto
(il nostro elemento grafico Oggetto
nella classe Assets
) per il primo oggetto e Assets.Quadrato
per il secondo, e i parametri usati all'interno dei comandi BoidParams
(esclusivo del package cheezeworld
).
Parametro (BoidParams) | Descrizione |
---|---|
neighborDistance |
stabilisce a quale distanza l'oggetto rileverà gli altri elementi |
maxSpeed e maxAcceleration |
impostano la velocità dell'oggetto |
damping |
stabilisce la decelerazione della velocità |
maxTurnRate |
stabilisce di quanti gradi potrà ruotare l'oggetto può ruotare in un secondo |
Un altro parametro che possiamo personalizzare è il boundsBehavior
, che stabilisce il comportamento che l'oggetto avrà nel caso in cui entri in collisione con i bordi del filmato.
Comportamento | Descrizione |
---|---|
BOUNDS_WRAP |
l'oggetto attraverserà i bordi rientrando dal lato opposto (uscendo dal bordo superiore rientrerà da quello inferiore, uscendo da destra rientrerà da sinistra e così via) |
BOUNDS_BOUNCE |
l'oggetto rimbalzerà contro i bordi dello stage |
BOUNDS_REMOVE |
l'oggetto uscirà dai bordi ma invece di rientrare sarà eliminato |
L'attributo type ci permette invece di impostare un nome con cui fare riferimento all'oggetto a cui è associato, come vedremo in seguito all'interno della classe DemoSeek.as
.
Il parametro della funzione register
, dichiarata nella classe, sarà l'istanza del sistema di gioco, che passeremo dalla classe delle schermate che vedremo nella seconda parte dell'articolo.
Creare l'ambiente di gioco e le schermate
Nella prima parte dell'articolo abbiamo impostato gli elementi necessari al nostro esempio, non resta che creare le schermate di gioco vere e proprie.
Tutte le schermate avranno in comune una classe, che chiamiamo MainScreen
, necessaria a contenere alcuni comandi generici per la creazione del motore di gioco e l'impostazione di alcuni eventi. Ogni schermata quindi incorporerà questa classe principale ma avrà poi al suo interno comandi specifici.
Creiamo il nostro file MainScreen.as
nel package IA.schermate
:
package IA.schermate { import IA.data.Elementi; import com.cheezeworld.GameFactory; import com.cheezeworld.entity.*; import com.cheezeworld.math.Vector2D; import com.cheezeworld.screens.AScreen; import com.cheezeworld.screens.IScreenItem; import com.cheezeworld.screens.Root; import flash.events.MouseEvent; public class MainScreen extends AScreen { public function MainScreen(a_parentScreen:IScreenItem=null) { super(a_parentScreen); _factory = new GameFactory(); Elementi.register( _factory ); _gameworld = _factory.createGameworld( new Vector2D(550, 400), new Vector2D(550, 400), this ); } public override function update(a_timePassed:int):void { _gameworld.update( a_timePassed ); } public override function dispose() : void { _gameworld.dispose(); _factory.dispose(); _gameworld = null; _factory = null; } protected var _gameworld:GameWorld; protected var _factory:GameFactory; } }
Questo codice crea un nuovo oggetto GameFactory
e registra gli assets (metodo register
) della classe Elementi
. Poi crea lo spazio grafico per il gioco con il metodo createGameWorld
.
A questo punto l'ambiente del gioco è pronto, non rimane che creare la classe DemoSeek
che abbiamo associato al metodo setScreen
della classe Esempio
.
L'inseguimento
Nella clase DemoSeek
vogliamo impostare un oggetto che segua il mouse, utilizzeremo quindi il comportamento Seek
associandolo ad un elemento. La classe estenderà MainScreen
che, come abbiamo visto poco fa, include i comandi per avviare il sistema.
In questa schermata utilizzeremo solo uno dei due asset creati. Ecco il codice:
package IA.schermate { import com.cheezeworld.AI.Behaviors.*; import com.cheezeworld.entity.*; import com.cheezeworld.rendering.MovingEntityRenderer; import com.cheezeworld.screens.IScreenItem; import com.cheezeworld.utils.Input; public class DemoSeek extends MainScreen { public function DemoSeek(a_parentScreen:IScreenItem=null) { super(a_parentScreen); var boid1:Boid; boid1 = _factory.getEntity( "Ogg1", _gameworld ) as Boid; boid1.newPos.Set( 250, 250 ); boid1.steering.addBehavior( new Seek( Input.instance.worldMousePos ) ); } } }
Come detto la classe estende MainScreen
, quindi crea un oggetto di tipo Boid
e lo associa all'elemento Ogg1
. L'istanza di quest'oggetto è richiesta al nostro _factory
(che è il GameFactory
, l'oggetto principale del motore di gioco).
Poi all'elemento viene assegnata una posizione (250, 250
) e gli viene associato il comportamento Seek. Questo comportamento costringe l'oggetto a dirigersi verso un certo punto (specificato come parametro nel costruttore). Nel nostro caso il punto scelto è Input.instance.worldMousePos
: la posizione del mouse all'interno del motore di gioco.
Se non abbiamo commesso errori, esportando il nostro file FLA dovremmo ottenere il seguente risultato:
Oggetto che segue il cursore del mouse
Cambiare il comportamento
A questo punto diventa piuttosto semplice variare il comportamento degi oggetti e utilizzare le altre modalità di intelligenza artificiale messe a disposizione dal framework.
Supponiamo di volere che il nostro oggetto "fugga" dal mouse invece di inseguirlo: basterà utilizzare il comando Flee al posto del comando Seek
. Creiamo allora la classe DemoFlee.as
all'interno della cartella schermate
, con il seguente codice:
package IA.schermate { import com.cheezeworld.AI.Behaviors.*; import com.cheezeworld.entity.*; import com.cheezeworld.screens.IScreenItem; import com.cheezeworld.utils.Input; public class DemoFlee extends MainScreen { public function DemoFlee(a_parentScreen:IScreenItem=null) { super(a_parentScreen); var boid1:Boid; boid1 = _factory.getEntity( "Ogg1", _gameworld ) as Boid; boid1.newPos.Set( 250, 250 ); boid1.steering.addBehavior( new Flee( Input.instance.worldMousePos, 200*200 ) ); } } }
Notiamo come (ad eccezione del nome della classe e il relativo costruttore) l'unica modifica sia il comando Flee
al posto del comando Seek
. Sostituiamo ora DemoSeek
con FleeSeek
, nella classe Esempio
, e questo sarà il risultato:
Oggetto che "fugge" dal cursore del mouse
Unire due comportamenti
Proviamo ora a mettere insieme due comportamenti: vogliamo un oggetto che non fugge dal mouse, ma da un altro elemento (che a sua volta insegue il mouse): possiamo utilizzare il comportamento Evade
e abbinarlo al comportamento Seek
: l'oggetto associato al seek
seguirà il mouse, e causerà l'eventuale fuga dell'oggetto associato all'evade
.
Il codice è molto simile ai due esempi precedenti, qui utilizzeremo anche il secondo asset creato in precedenza:
package IA.schermate { import com.cheezeworld.AI.Behaviors.*; import com.cheezeworld.entity.*; import com.cheezeworld.screens.IScreenItem; import com.cheezeworld.utils.Input; public class DemoEvade extends MainScreen { public function DemoEvade(a_parentScreen:IScreenItem=null) { super(a_parentScreen); var boid1:Boid; var boid2:Boid; boid1 = _factory.getEntity( "Ogg2", _gameworld ) as Boid; boid2 = _factory.getEntity( "Ogg1", _gameworld ) as Boid; boid1.newPos.Set( 250, 250 ); boid1.steering.addBehavior( new Seek( Input.instance.worldMousePos ) ); boid2.newPos.Set( 200, 200 ); boid2.steering.addBehavior( new Evade( boid1, 200*200 ) ); } } }
Possiamo notare come le variazioni rispetto al codice dell'esempio del comportamento Seek siano costituite dall'aggiunto di un nuovo elemento (boid2
), a cui viene poi associato il comportamento Evade
. Questo comportamento prevede due parametri: il primo è l'oggetto da cui fuggire (nel nostro caso boid1
, che è invece aggianciato al mouse tramite il comportamento Seek
) e la distanza a cui l'oggetto da cui fuggire deve trovarsi perchè venga scatenato il comportamento di fuga.
Una volta impostata la classe ci basterà modificare ancora una volta in Esempio.as
il richiamo, inserendo DemoEvade
al posto di DemoFlee
nel metodo setScreen
. Esportando il filmato otterremo questo:
Oggetto che "fugge" dal cursore da un altro elemento comandato tramite mouse