Resistere per ben due post all’inserimento di snippet di codice non è stato facile e proprio per questo motivo iniziamo oggi ad addentrarci in qualche esempio di utilizzo delle API del sistema che gestisce la nostra simulazione. Come giustamente mi è stato fatto notare da Paolo, l’uso del framework NSteer (oggetto dei post precedenti) potrebbe vincolare troppo la libertà in eventuali future evoluzioni del sistema di simulazione. Per questo motivo cercheremo di astrarre, laddove possibile, la concreta implementazione di NSteer degli “attori” che partecipano alla simulazione, mantenendo focalizzata la nostra attenzione soprattutto sulle interazioni tra gli oggetti descritti dalle interfacce introdotte nel post precedente. In altri termini, quando parleremo ad esempio di “Scena” faremo sì riferimento agli aspetti peculiari di un oggetto che implementa l’interfaccia IScene definita all’interno di NSteer, ma non necessariamente all’implementazione dell’unico tipo di scena definito in NSteer, ossia la GdiScene.
Già da una prima frettolosa analisi del sistema che realizzeremo appare evidente che, in un verso o nell’altro, dovremo “allestire” un’istanza di simulazione per ciascuna "sfida” giocata all’interno di NHooligans. E’ facile immaginare inoltre che il servizio (in senso generico, non in senso Windows Service o WebService) che costituirà l’host per la simulazione in questione dovrà costruire le condizioni sulle quali la simulazione costruirà il proprio contesto di esecuzione, condizioni quali la definizione dei partecipanti (ossia degli agenti), le caratteristiche fisiche e cinematiche di ciascun oggetto definito all’interno del “mondo” della simulazione (massa, accelerazione e velocità massime, ecc.) nonché ovviamente ciò che dà effettivamente un senso alla simulazione: il “programma” seguito da ciascun agente per muoversi e, soprattutto, gonfiare di botte i propri simili della squadra avversaria.
Quale che sia la conformazione del servizio “host”, la simulazione da allestire seguirà inevitabilmente i passi illustrati di seguito, che fanno riferimento ad una situazione facilmente riscontrabile anche nel demo di NSteer incluso nei sorgenti allegati nel primo post di questa sezione.
La composizione degli oggetti che partecipano alla realizzazione di una simulazione è illustrata nel diagramma che segue, in cui l’host è rappresentato da un Form Winforms denominato con grande fantasia “Form1”:

I primi servizi da inizializzare sono quelli che descrivono il “mondo” in cui la simulazione ha luogo e la “scena” che ne darà una rappresentazione (presumibilmente ma non necessariamente grafica), attraverso il retrieving di un riferimento alle interfacce IWorldService e IScene.
Qualora, come avviene tipicamente, si voglia confinare il raggio di azione degli agenti ad una regione limitata, dovremo creare un’istanza di comportamento che, applicata a ciascun agente, ne limiterà il movimento. Il tipo di comportamento in questione è già definito in NSteer e prende il nome di RectangleWorldGlobalBehavior. Per inizializzarlo è sufficiente impostare punto di partenza (di tipo PointF, ossia punto 2D con coordinate di tipo float) e dimensione (di tipo SizeF) della sua proprietà “Region”, come illustrato nel seguente snippet:
RectangleWorldGlobalBehavior region = new RectangleWorldGlobalBehavior(this.simulationComponents);
region.Region = new RectangleF(new PointF(0, 0), worldService.World.WorldSize);
Per inizializzare il servizio di gestione della “vicinanza" tra agenti, rappresentato dall’interfaccia INeighborhoodService, è necessario creare un’istanza del contenitore utilizzato dal sistema per accedere alla informazioni posizionali relative degli agenti. All’interno di NSteer sono definiti due diversi contenitori (che implementano l’interfaccia INeighborhood, di cui il più performante è il BinLatticeNeihborhood, basato su una struttura bidimensionale di “Bins”, ossia di caselle caratterizzate da una coppia di indici che ne descrive la posizione all’interno del contenitore (in termini di riga e colonna) e da una lista di agenti presenti in quella casella. Un’inizializzazione tipica del BinLatticeNeihborhood è la seguente:
INeighborhoodService neighborhood = this.simulationComponents.NeighborhoodService;
BinLatticeNeihborhood bins = new BinLatticeNeihborhood(
this.simulationComponents,
16, 16,
this.sceneControl.Width,
this.sceneControl.Height);
neighborhood.Neighborhood = bins;
Gli ostacoli presenti all’interno della simulazione possono essere facilmente creati ed inseriti nel servizio che li gestisce attraverso uno snippet di questo tipo, in cui vengono definiti due ostacoli circolari in posizioni diverse e di raggio rispettivamente 50 e 15:
IObstacleService obstacleService = this.simulationComponents.ObstacleService;
CircleObstacle obstacle = new CircleObstacle(new PointF(100, 100), 50);
obstacleService.ObstacleManager.AddObstacle(obstacle);
CircleObstacle obstacle2 = new CircleObstacle(new PointF(200, 200), 15);
obstacleService.ObstacleManager.AddObstacle(obstacle2);
Arriviamo finalmente alla creazione di un agente, caratterizzato nello snippet che segue dai seguenti aspetti salienti:
- Corpo con comportamento assimilabile ad un punto dotato di massa
- Area visiva “conica” (o meglio, in 2D, angolare)
- Comportamento composito di tipo “prioritario”, in cui viene assunto come comportamento dell’agente il primo, all’interno dei comportamenti “componenti”, per il quale la norma dell’accelerazione sia maggiore di una certa soglia
- Comportamento componente di tipo “comportamento composito pesato”, in cui i vari comportamenti componenti vengono sommati ciascuno con un proprio “peso”. In altri termini tale comportamento farà sì che vengano contemporaneamente tenuti in considerazione criteri diversi di movimento facendo sì che la “somma” di tutti i criteri propenda in favore dei criteri ritenuti più importanti
- Comportamento componente (del composito pesato) di tipo ObstacleAvoidance (per evitare gli ostacoli) con peso 10
- Comportamento componente (sempre del composito pesato) di tipo LocalBehavior (per rimanere nell’area di confine) con peso 5
- Comportamento componente (del composito prioritario) di tipo FlockBehavior che permette all’agente di rimanere accanto ai propri “simili”. Poiché il comportamento composito prioritario prevede un ordine di priorità (decrescente) dato dall’ordine di inserimento, il FlockBehavior apporterà il proprio contributo solo se l’agente non si trova a ridosso di un ostacolo o del confine dell’area permessa
- Comportamento componente (del composito prioritario) di tipo SeekBehavior che spinge l’agente verso il “target” assegnato (nel nostro esempio la posizione del cursore del mouse)
- Velocità massima pari a 3 (non chiediamoci l’unità di misura)
Agent agent = new Agent(this.simulationComponents);
agent.Body = new PointMassBody();
agent.Vision = new ConeVision();
// Comportamento composito "prioritario"
PriorityBehavior behavior = new PriorityBehavior();
agent.Behavior = behavior;
// Comportamento componente del prioritario e composito "pesato"
WeightedSumBehavior wo = new WeightedSumBehavior();
behavior.Behaviors.Add(wo);
// Comportamento componente del pesato di tipo "confine"
LocalBehavior region = new LocalBehavior(this.region);
wo.AddBehavior(region,5);
// Comportamento componente del pesato di tipo "evita gli ostacoli"
ObstacleAvoidanceBehavior obstacleAvoidance = new ObstacleAvoidanceBehavior();
obstacleAvoidance.Probe = new TridentObstacleProbe();
wo.AddBehavior(obstacleAvoidance,10);
// Comportamento componente del prioritario di tipo "stormo"
FlockBehavior flock = new FlockBehavior();
behavior.Behaviors.Add(flock);
// Comportamento componente del prioritario di tipo "insegui"
MouseTargetPredictor mouse = new MouseTargetPredictor();
mouse.Style = MouseTargetPredictorStyle.Move;
mouse.OwnerControl = this.sceneControl;
SeekBehavior seek = new SeekBehavior();
seek.TargetPredictor = mouse;
seek.TargetTracker = new ArrivalTargetTracker();
behavior.Behaviors.Add(seek);
// Accelerazione, Velocità e Posizione iniziali
agent.Body.Update(new PointF(), new PointF(0, 0), new PointF(sceneControl.Width / 2, sceneControl.Height / 2));
// Velocità massima
UniformPointSaturator velsat = new UniformPointSaturator();
velsat.MaxNorm = 3f;
agent.Body.VelocitySaturator = velsat;
A questo punto uno step della simulazione può essere calcolato utilizzando il metodo SimulateTurn() dell’oggetto Simulator. Nell’esempio allegato a NSteer tale metodo viene chiamato ogni qual volta il controllo contenuto all’interno del Form che costituisce l’interfaccia utente dell’applicazione necessita di essere ridisegnato. Tale necessità viene inoltre “forzata” in corrispondenza del gestore di evento dell’evento Idle dell’oggetto Application di Winforms, come illustrato nello snippet seguente, in cui la simulazione viene rispettivamente avviata e bloccata da due pulsanti denominati (guarda caso) “startButton” e “stopButton”:
private void startButton_Click(object sender, EventArgs e)
{
this.stopButton.Enabled = true;
this.startButton.Enabled = false;
Application.Idle+=new EventHandler(Application_Idle);
}
private void stopButton_Click(object sender, EventArgs e)
{
Application.Idle -= new EventHandler(Application_Idle);
this.stopButton.Enabled = false;
this.startButton.Enabled = true;
}
private void Application_Idle(Object sender, EventArgs e)
{
this.sceneControl.Invalidate();
}
Per oggi ci fermiamo qui, ma per mantenere viva l’attenzione su quanto ci aspetta propongo non uno, ma addirittura due “approfondimenti” pratici di quanto abbiamo visto.
Il primo è la realizzazione di un ostacolo di tipo rettangolare da integrare in NSteer, al quale potrebbe seguire, per i più solerti, l'implementazione di un ostacolo di tipo "polirettangolare", ossia definito da un insieme di rettangoli adiacenti.
Il secondo spunto, probabilmente più divertente, prevede la realizzazione di una semplice applicazione Winforms che, utilizzando NSteer, definisca un'area rettangolare di dimensioni 100x100 unità logiche con al centro un ostacolo circolare di raggio 30. In questo ambiente, 10 agenti con posizione iniziale (0,0), velocità e accelerazione nulla e velocità massima 3 devono arrivare nel più breve numero di step di simulazione possibili all'angolo opposto, dove per tempo più breve si intende quello dell'ultimo arrivato.
Come premio (o come penitenza?) i primi (o i soli) che posteranno del materiale interessante in merito ai due approfondimenti proposti avranno la facoltà di scegliere per primi i componenti dello sviluppo di NHoolingans da realizzare tra quelli che verranno resi a breve disponibili.