Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 15 di 53
  • livello principiante
Indice lezioni

Gestire lo stato

Il codice per semplificare l'organizzazione del gioco pensandolo come una 'macchina a stati finiti'
Il codice per semplificare l'organizzazione del gioco pensandolo come una 'macchina a stati finiti'
Link copiato negli appunti

Un gioco XNA visto ad un livello di astrazione molto alto puó essere equiparato ad una macchina a stati finiti, in cui gli stati sono quelli fondamentali del gioco (menu, livello 1, livello 2, ...) e le transizioni di stato corrispondono ad eventi come "livello i-esimo vinto", "livello i-esimo perso", "bottone 'Nuova Partita' cliccato", etc:

Figura 17. Gli 'stati' del gioco
Gli 'stati' del gioco

Realizziamo tale architettura in modo elegante ed estensibile creando uno o più componenti per ogni stato (più ad esempio alcuni components generali come per la gestione dell'input o altri servizi). I nostri componenti e servizi saranno:

public class SharedContext
{
  public SpriteBatch sprite_batch;
}

un servizio per mantenere gli oggetti condivisi da tutta l'applicazione;

public class ComponentsAndServicesGame : Microsoft.Xna.Framework.Game
{
  GraphicsDeviceManager graphics;
  public ComponentsAndServicesGame()
  {
    …
    Services.AddService(typeof(SharedContext), new SharedContext()
    {
      sprite_batch = null
    });
  }
  protected override void Initialize()
  {
    (Services.GetService(typeof(SharedContext)) as SharedContext).sprite_batch = new SpriteBatch(GraphicsDevice);
    this.StartMenu();
    base.Initialize();
  }
  protected override void Draw(GameTime gameTime)
  {
    GraphicsDevice.Clear(Color.CornflowerBlue);
    base.Draw(gameTime);
  }
}

il game che nel metodo Initialize invoca il metodo di creazione dei componenti del Menu e che registra il servizio degli oggetti condivisi;

public delegate void MenuAction();
public interface IMenu
{
  event MenuAction OnPlay;
  event MenuAction OnQuit;
}

il servizio del menu che esporta due eventi per il click del bottone Play e il click del bottone Quit;

public class Menu : Microsoft.Xna.Framework.DrawableGameComponent, IMenu
{
  public Menu(Game game)
    : base(game)
  {
    Game.Services.AddService(typeof(IMenu), this);
  }
  SpriteBatch sprite_batch;
  public override void Initialize()
  {
    var input = Game.Services.GetService(typeof(IInputManager)) as IInputManager;
    input.OnClick += new OnClick(input_OnClick);
    sprite_batch = (Game.Services.GetService(typeof(SharedContext)) as SharedContext).sprite_batch;
    base.Initialize();
  }
  void input_OnClick(Point position)
  {
    if (position.Y < Game.GraphicsDevice.Viewport.Height / 2)
    {
      if (OnPlay != null) OnPlay();
    }
    else
    {
      if (OnQuit != null) OnQuit();
    }
  }
  protected override void Dispose(bool disposing)
  {
    Game.Services.RemoveService(typeof(IMenu));
    base.Dispose(disposing);
  }
  public override void Draw(GameTime gameTime)
  {
    sprite_batch.Begin();
    sprite_batch.DrawString(Game.Content.Load<SpriteFont>("large_font"), "Play", new Vector2(50, 50), Color.White);
    sprite_batch.DrawString(Game.Content.Load<SpriteFont>("large_font"), "Quit", new Vector2(50, 290), Color.White);
    sprite_batch.End();
    base.Draw(gameTime);
  }
  public event MenuAction OnPlay;
  public event MenuAction OnQuit;
}

il componente del Menu che implementa il servizio IMenu appena visto, ascolta l'evento OnClick dell'InputManager (un componente con servizio associato per la gestione dell'input) e rilancia uno dei suoi due eventi OnPlay e OnQuit a seconda di dove è avvenuto il click. Il Menu disegna anche due semplici stringhe per rendere riconoscibili i due bottoni, e il disegno avviene con l'oggetto SpriteBatch estratto dal servizio di oggetti condivisi;

public delegate void LevelAction();
public class Level : Microsoft.Xna.Framework.DrawableGameComponent
{
  public Level(Game game)
    : base(game)
  {
  }
  SpriteBatch sprite_batch;
  public override void Initialize()
  {
    var input = Game.Services.GetService(typeof(IInputManager)) as IInputManager;
    input.OnClick += new OnClick(input_OnClick);
    sprite_batch = (Game.Services.GetService(typeof(SharedContext)) as SharedContext).sprite_batch;
    base.Initialize();
  }
  void input_OnClick(Point position)
  {
    if (OnFinish != null) OnFinish();
  }
  public override void Draw(GameTime gameTime)
  {
    var fps = (float)(1.0 / gameTime.ElapsedGameTime.TotalSeconds);
    sprite_batch.Begin();
    sprite_batch.DrawString(Game.Content.Load("small_font"), fps.ToString("0##.#"), new Vector2(50, 50), Color.White);
    sprite_batch.End();
    base.Draw(gameTime);
  }
  public event LevelAction OnFinish;
}

una classe Level che implementa un rudimentalissimo livello che quando termina lancia un evento OnFinish.

Questo sistema richiede un "collante" che ad ogni transizione di stato rimuova i componenti correnti e successivamente istanzi i componenti corretti per lo stato in cui transire. Questa funzionalità è affidata ad una classe che realizza alcuni metodi estensione della classe Game; i metodi estensione sono metodi statici che possono essere chiamati come normali metodi di istanza grazie all'uso della keyword this nella dichiarazione del loro primo parametro.

Creiamo una classe statica che si occuperà della gestione dello stato:

public static class StateManager

in questa classe creiamo un metodo che azzera lo stato corrente eliminando i componenti attivi al momento, che a loro volta dovranno rimuovere i servizi che offrono:

private static void Cleanup(this Game game)
{
  for (int i = 0; i < game.Components.Count; i++)
  {
    (game.Components[i] as GameComponent).Dispose();
    i--;
  }
}

Per entrare nel menu definiamo un metodo (che viene invocato nell'Initialize del game definito prima) che invoca il cleanup, attiva il componente dell'input, attiva il componente del menu e si mette in ascolto degli eventi OnPlay e OnQuit del menu; quando questi eventi vengono lanciati allora verrà attivata la transizione di stato appropriata o la chiusura del gioco:

public static void StartMenu(this Game game)
{
  game.Cleanup();
  game.Components.Add(new InputManager(game));
  var menu = new Menu(game);
  menu.OnQuit += () => game.Exit();
  menu.OnPlay += () => game.StartGame();
  game.Components.Add(menu);
}

Per avviare il gioco facciamo esattamente la stessa cosa: cleanup, componente dell'input, componente del livello, ascolto dell'evento OnFinish che torna al menu:

public static void StartGame(this Game game)
{
  game.Cleanup();
  game.Components.Add(new InputManager(game));
  var level = new Level(game);
  level.OnFinish += () => game.StartMenu();
  game.Components.Add(level);
}

In questo modo l'applicazione è ben separata in componenti distinte e facilmente manutenibili e stati e transizioni sono ben definiti. Inoltre è facile estendere la struttura dell'applicazione con un nuovo stato, in quanto basterà:

  • creare i nuovi componenti appropriati
  • creare il metodo di entrata nel nuovo stato che effettua il cleanup e istanzia tali componenti
  • aggiungere le transizioni per attivare correttamente il nuovo stato in base a determinati eventi di altri stati

Ti consigliamo anche