Da un punto di vista di Ingegneria del Software, è ben noto che una ragionevole modularizzazione delle nostre applicazioni è una delle chiavi di una base di codice chiara e di facile manutenzione. Nel caso di XNA peró, sembra quasi che siamo costretti ad usare esclusivamente la classe Game in cui dobbiamo infilare l'intero codice del nostro gioco.
Ciò non è del tutto vero, fortunatamentein XNA abbiamo un sistema di componenti pensato per separare le funzionalità che richiedono caricamento (Initialize
/LoadContent
), aggiornamento (Update
) e disegno (Draw
).
In questo modo possiamo pensare di avere dei componenti diversi, interessati solo ad alcune funzionalità di XNA:
Componente | Funzionalità |
---|---|
Menu | (caricamento, aggiornamento, disegno) |
AI/fisica/logica | (caricamento, aggiornamento) |
rendering della scena | (caricamento, aggiornamento, disegno) |
rendering della UI | (caricamento, aggiornamento, disegno) |
Possiamo creare un componente in due modi: se ci interessano solo i metodi Initialize
e Update
, ereditiamo la classe GameComponent, mentre se ci interessano anche i metodi LoadContent
e Draw
ereditiamo la classe DrawableGameComponent.
Dobbiamo poi scrivere i metodi per le rispettive classi e, nel costruttore del nostro gioco, attiviamo questi componenti: i loro metodi saranno così invocati assieme a quelli del Game aggiungendoli alla collezione Components, tramite il suo metodo Add
.
Un esempio di semplice GameComponent che si limita a stampare sulla console diagnostica le invocazioni dei suoi metodi è questo:
public class DiagnosticsComponent : Microsoft.Xna.Framework.GameComponent
{
public DiagnosticsComponent(Game game) : base(game) { }
public override void Initialize()
{
System.Diagnostics.Debug.WriteLine("Initialize Component");
base.Initialize();
}
public override void Update(GameTime gameTime)
{
System.Diagnostics.Debug.WriteLine("Update Component");
base.Update(gameTime);
}
}
Aggiungiamo questo componente nel costruttore del nostro gioco :
Components.Add(new DiagnosticsComponent(this));
e l'output risultante dall'esecuzione dell'applicazione diventa:
Initialize Initialize Component LoadContent Update Update Component Update Update Component Draw Update Update Component Draw ... Update Update Component Draw Update Update Component Draw Update Update Component Servizi
I componenti, per come li abbiamo visti finora, non offrono una grande funzionalità: un componente non è in grado di comunicare in modo strutturato e ordinato con il resto dell'applicazione, e questo ci costringe a:
- creare componenti che svolgono il loro lavoro in modo del tutto indipendente dal resto del gioco, quindi con funzionalità piuttosto limitata;
- creare componenti che comunicano in modo completamente ad-hoc con il resto del gioco e tra di loro, con perdita di flessibilità.
I servizi in XNA
Il sistema di servizi che XNA ci mette a disposizione serve proprio per risolvere questo problema in modo brillante, ossia avere componenti in grado di comunicare con il resto del sistema senza perdita di flessibilità e con uno schema standard e pulito.
Un componente eredita una serie di interfacce dotate ciascuna dei metodi, degli eventi e delle proprietà pertinenti ad una certa funzionalità (detta servizio) che il componente offre. Il componente registra che è lui ad offrire quel servizio aggiungendo alla proprietà Services del Game il binding (collegamento) tra lui e il servizio tramite il metodo AddService.
Rispetto al nostro esempio corrente, vediamo com aggiungere un servizio al nostro sistema. Creiamo una interfaccia che rappresenta il servizio di diagnostica:
public interface IDiagnostics
{
bool Logging { get; set; }
}
implementiamo questa interfaccia nella classe del nostro componente:
public class DiagnosticsComponent : Microsoft.Xna.Framework.GameComponent, IDiagnostics
{
public bool Logging { get; set; }
nel costruttore del componente registriamo il servizio:
public DiagnosticsComponent(Game game) : base(game)
{
game.Services.AddService(typeof(IDiagnostics), this);
}
e trasformiamo le chiamate di diagnostica in:
System.Diagnostics.Debug.WriteLineIf(Logging, [messaggio]);
Nel metodo Dispose
del componente rimuoviamo il binding tra il componente e il servizio, per evitare che rimanga spazzatura non ripulita:
protected override void Dispose(bool disposing)
{
Game.Services.RemoveService(typeof(IDiagnostics));
base.Dispose(disposing);
}
Quando vogliamo ottenere il servizio di diagnostica dall'interno del nostro game, non dovremo fare altro che:
var diagnostics_service = Services.GetService(typeof(IDiagnostics)) as IDiagnostics;
dove il cast finale è necessario perché il metodo GetService ritorna un Object
.