The XNA-Way #4: Caricare Organizzare e disegnare immagini

Oggi voglio parlare di una cosa abbastanza critica nel caso si voglia realizzare un videogame 2D.
Perchè il 2D? Prima di tutto perchè è di più semplice realizzazione (per il 3D ci sono molte più cose da considerare, e la realizzazione degli asset è molo più complicata).
Quello che voglio affrontare oggi è come ho deciso di organizzare le risorse/immagini 2D che carico all’interno dei miei progetti e perchè ho scelto di fare tale implementazione. Illustrerò poi come si utilizza il tutto per recuperare e renderizzare l’immagine a schermo.

Penso che chiunque si sia trovato a scrivere del codice per XNA la prima cosa che abbia provato a fare sia stata quella di caricare un’immagine e di renderizzarla a schermo. Niente di più semplice.
Vi faccio vedere un attimo come si fa prima di andare avanti.
Prima l’immagine che userò nell’esempio

Immagine usata nell’esempio

Scaricare l’immagine ed inseritela nel Conten Project (si chiama come il vostro Game Project solo con un Content in più).
Ed ora il codice:

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
 
        Texture2D myImg;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            Components.Add(new ScreenShotter(this));
        }
 
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            myImg = Content.Load("orma");
        }
 
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin();
            spriteBatch.Draw(myImg, Vector2.Zero, Color.White);
            spriteBatch.End();
            base.Draw(gameTime);
        }
    }

La cosa è molto semplice in effetti: si crea un oggetto Texture2D per contenere il riferimento all’immagine che vogliamo renderizzare. Nel metodo LoadContent si carica il file (si usare il ContentManager di XNA per caricare i file). Poi nel metodo Draw si disegna il tutto. Il metodo Draw offre numerosi overLoad, con i quali è possibile riposizionare l’immagine da disegnare in ogni punto dello schermo, ruotarla, renderizzare solo un pezzo e/o scalarla. Tutta roba che illustrerò in un altro articolo.

Tornando a come organizzare i nostri asset 2D, ci rendiamo conto che fino a quando lavoriamo con progetti giocattolo, come questo, è facile tenere a mente le immagini che abbiamo caricato, per cosa e dove usarle. Ma come fare quando abbiamo a che fare con dei progetti più ampi, che possono aver bisogno di centinaia di immagini.
L’unica cosa da fare è creare un archivio degli asset caricati, un archivio che sia riferibile da ogni punto strategicamente e logicamente interessante del progetto. Come visto questo è possibile farlo con i services.

Soffermiamoci un attimo sulla parte “da dove arrivano le immagini”?
Io almeno non ho affatto voglia di scrivermi a mano tutti i file di configurazione di un mio progetto, così come non ho la voglia o la pazzia di scrivere a mano come sono composti i livelli o qualsiasi altro elemento attivo o strutturale del mio gioco. Di solito mi faccio un editor “apposito” per editare i componenti del gioco. Pensate a come fa il famoso RPG-Maker.

[important]ne approfitto per dire che a breve comincerò a scrivere una serie di tutorial incentrati sulla creazione di un proprio rpg-Maker con XNA. Sul mio vecchio blog avevo messo delle immagini a riguardo, ma non avevo mai pubblicato sorgenti o simili.[/important]

Seguendo questa linea logica avremo allora a disposizione diversi file che rappresentano come è composto i nostro gioco più altri file che rappresentano/listano/elencano le risorse/asset che verranno utilizzati nel progetto.
Quando il tutto diventa complesso, quando si hanno centinaia di file è impensabile di utilizzare un riferimento per nome per identificare una risorsa, questo è perchè alla fine potremmo rischiare di inserire 2 risorse diverse con lo stesso nome, e questo genera ambiguità, ma c’è anche il rischio che il nome associato ad una certa risorsa venga [erroneamente] cambiato, e quindi ci troveremo con il dover mettere mano ad ogni riga di codice dove abbiamo utilizzato quel particolare nome.
Una buona soluzione per organizzare le risorse in modo che non ci siano doppie chiavi di accesso (i nomi) è quella di utilizzare un dictionary (o una struttura simile, tipo hashtable o quello che vi pare) in cui si lega una chiave ad un valore. Cioè abbiamo una coppia del tipo dove c’è il vincolo che non ci siano valori doppioni per le key.
Per semplificare le cose e per rendere il tutto più semplice per il framework io preferisco utilizzare delle chiavi primarie numeriche.
Vediamo quindi come organizzare il tutto.
Ecco una prima versione dell’interfaccia che utilizzeremo per il nostro servizio.

namespace GameLibrary.Interfaces
{
    public interface Asset_ArchiveI<T>
    {
        long Load(string path);
        long Load(long id, string path);
        T Get(long id);
    }
}

Ha 3 metodi semplici semplici: i due metodi load differiscono per il fatto che uno accetta direttamente l’id da assegnare alla risorsa che vogliamo caricare, mentre l’altro lo genera in automatico.
Questa differenza è data dal fatto che in un caso stiamo caricando risorse ex-novo e non ci interessa che id avranno, nell’altro c’è la possibilità che stiamo caricando le risorse leggendo da un file di configurazione, le quali quindi possono avere già un id proprio, che deve restare uguale.
Per garantire che la generazione automatica non dia due volte lo stesso valore ho deciso che utilizzerò poi il timestamp come chiave, da qua il motivo per cui uso delle chiavi di tipo long.
L’interfaccia è parametrica rispetto a T, e questo ci permetterà di riutilizzarla con qualsiasi altra classe che useremo come container per i nostri asset. E con una semplice modifica sarà adattabile ad ogni tipo (Texture2D, SoundEffect, etc).
I metodi load ritornano un long perchè nel caso l’id sia generato in automatico devo pur essere in grado di sapere a quale oggetto far riferimento.
Vediamo il codice per il gameComponent che implementa tale interfaccia:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
using GameLibrary.Interfaces;
 
namespace GameLibrary.Component
{
    public class Texture2D_Archive : Microsoft.Xna.Framework.GameComponent, Asset_ArchiveI<Texture2D>
    {
        //collezione delle texture2D
        SortedList<long, Texture2D> collection;
 
        public Texture2D_Archive(Game game)
            : base(game)
        {
            //lo aggiungo ai servizi
            game.Services.AddService(typeof(Asset_ArchiveI<Texture2D>), this);
            //lo disattivo: mi serve solo come contenitore, non mi serve che venga eseguito nulla
            Enabled = false;
            collection = new SortedList<long, Texture2D>();
        }
 
        public long Load(string path)
        {
            //uso il timestamp attuale come chiave, in modo da non avere due entry con lo stesso ids
            return Load(DateTime.Now.Ticks, path);
        }
 
        public long Load(long id, string path)
        {
            try
            {
                collection.Add(id, Game.Content.Load<Texture2D>(path));
                return id;
            }
            catch (Exception e)
            {
                return -1;
            }
        }
 
        public Texture2D Get(long id)
        {
            Texture2D ris;
            if (collection.TryGetValue(id, out ris))
                return ris;
            return null;
        }
    }
}
</csharp>
Come avevo detto in precedenza uso appunto il TimeStamp per la generazione automatica degli id.
C'è solo una cosa che mi preme far notare: come possiamo vedere il componente viene disattivato (<em><strong>Enabled = false</strong></em>), questo perchè non ci serve che il componente esegua nulla. L'esecuzione di  un metodo update in questo container non servirebbe a nulla. Quello che ci serve è che il componente sia accessibile da ogni altro component del progetto (e questo è reso possibile dal fatto che lo consideriamo come servizio) e che permetta di caricare e recuperare gli asset (reso possibile dai metodi dell'interfaccia).
Nulla però ci vieta in futuro di creare versioni più specializzate e particolari (metticaso con delle procedure particolari passate come argomento in fase di inizializzazione e che permettono di personalizzare alcuni aspetti del caricamento o recupero di alcuni oggetti) oppure di estendere il GameComponent in un DrawableGameComponent e fargli eseguire alcune operazioni ripetitive (per esempio può essere utilizzato in fase di debug per stampare alcune stats sugli asset gestiti).
 
Ma come si utilizza tutto questo?
Vediamo il codice del Game
<pre lang="csharp">
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
using GameLibrary.Component;
using GameLibrary.Interfaces;
 
namespace MyGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
 
        //riferimento all'id dell'immagine
        long id;
        //riferimento al componente per l'archivio delle texture
        Asset_ArchiveI<Texture2D> textureAsset;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            Components.Add(new FPS_Counter(this, true));
            Components.Add(new SimpleInputDevice(this));
            //creo il component
            textureAsset = new Texture2D_Archive(this);
            //lo aggiungo
            Components.Add((Texture2D_Archive)textureAsset);
        }
 
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            id = textureAsset.Load("orma");
        }
 
        protected override void Draw(GameTime gameTime)
        {
            //recupero la texture
            Texture2D myImg = textureAsset.Get(id);            
            GraphicsDevice.Clear(Color.CornflowerBlue);
            //comincio a disegnare
            spriteBatch.Begin();
            spriteBatch.Draw(myImg, Vector2.Zero, Color.White); //renderizzo
            spriteBatch.End();//fine
            base.Draw(gameTime);
        }
    }
}

Non mi sembra nulla di così trascendentale no? ^^
Certo che adesso, con questo esempio giocattolo, il tutto sembra fin troppo laborioso solo per disegnare un’immagine a schermo. Ma pensate di avere centinaia di asset da gestire, oggetti creati dinamicamente, ognuno dei quali recupera in automatico l’asset di cui necessita e lo usa (renderizza in caso di un’immagine o lo esegue in caso di un suono) e allora vedrete che un’organizzazione simile non sarà affatto così malvagia e spiacevole!

Per ora è tutto!

Come al solito vi allego i file del progetto
IndieGearLab_04.rar

Se ci sono domande contattatemi pure!
A presto.

Lascia un commento

We use cookies to ensure that we give you the best experience on our website.
Ok