The XNA-Way #5: Sprite Sheet

Quest’oggi voglio invece dedicarmi agli Sprite Sheed, risorsa importantissima per chi vuole sviluppare un qualsiasi videogame 2D.

Gli sprite sheet sono utilissimi perchè permettono di raccogliere in un singolo file immagine i frame di un’animazione. Questo porta a dover fare un po’ di lavoro per quanto riguarda recuperare i il frammento/frame dell’animazione da renderizzare, ma per il framework sottostante è un vantaggio, perchè la risorsa in questione è tutta allocata in memoria.

Capire come funzionano gli sprite sheet è il primo passo per creare ed utilizzare delle buone animazioni.
Quindi cominciamo.
Prima di tutto vi do l’immagine che utilizzerò nel tutorial:

Tante piccole orme numerate… tanto per tenere il conto di quello che faremo


Come potete vedere ho numerato le orme nell’immagine, proprio per tenere bene a mente quale parte dell’immagine renderizzeremo in seguito.

Come potete vedere i vari frame/frammenti (per me queste due parole sono dei sinonimi) sono tutti grandi uguali, sono tutti alla stessa distanza tra di loro e dai bordi dell’immagine. Proprio per questo la gestione di uno spriteSheet come questo è molto semplificata.
Ed anche per questo la classe che andrò a creare sarà chiama Simple_SpriteSheet (questo perchè in seguito, in un articolo successivo realizzerò un altro classe per la gestione degli sprite sheet con la quale poter gestire gli sprite sheet che io chiamo alla membro di segugio cioè quelli in cui per qualche malaugurata ragione i frammenti delle animazioni non hanno tutti la stessa grandezza e/o non hanno tutti la stessa distanza tra loro, etc).

Uno sprite sheet si caratterizza in questo caso dal numero di colonne e di righe, dati essenziali per poter poi recuperare la fetta di immagine che ci interessa.
Con una semplice moltiplicazione possiamo calcolare il numero di frame presenti nel file (righe * colonne).

Una cosa molto importante è quella di definire dei metodi che permettono di calcolare il rettangolo/l’area dell’immagine nel quale si trova un certo frame, identificato dal suo id progressivo.

Vediamo quindi subito il codice, dato che credo che sia più facile leggerlo che spiegarlo ai 4 venti.

[warning]Ho fatto delle modifiche ai nomi di alcune classi, per esempio invece che Asset_Archive ho un Asset_Store, così come ho commentato la gestione delle eccezioni nel componente relativo all’archivio delle texture, questo per costringermi (e costringervi) a far si che ogni dato che inserisco e recupero dagli store sia sempre valido al 100%.
Se la cosa vi disturba potete sempre rimettere la gestione delle eccezioni.[/warning]

Prima di creare la classe, nella mia GameLibrary ho creato una cartella chiama Classes dove andrò a mettere tutte le classi che mi rappresentano oggetti strutturati, come in questo caso gli spriteSheet.

using System;
using System;
using System.Collections.Generic;
using System.Text;
 
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
 
using GameLibrary.Interfaces;
 
namespace GameLibrary.Classes
{
    /// <summary>
    /// Simple_SpriteSheet perchè adatto a gestire solo quegli spritesheet la cui struttura del file immagine è "perfetta"
    /// cioè quei file in cui ogni frame dell'animazione occupa lo stesso spazio, sono tutti distanziati in modo
    /// eguale tra loro e dai bordi dell'immagine.
    /// In questo caso vedo uno spriteSheet come suddiviso in una matrice di frammenti (fragments)
    /// ognuno di quali ha un suo id che lo identifica (l'id è progessivo, parte da 0 e arriva
    /// al numero di frame presenti nell'immagine -1).
    /// </summary>
    public class Simple_SpriteSheet
    {
        //indica il numero di righe in cui è diviso l'immagine
        public int Rows { get; protected set; }
        //indica il numero di colonne in cui è divisa l'immagine
        public int Cols { get; protected set; }
        //ritorna il numero di frame contenuti nello spriteSheet
        public int FrameNumber { get { return Rows * Cols; } }
        //indica l'id della texture a cui questo spriteSheet fa riferimento (cioè a quelle memorizzate in un asset_archive per esempio)
        public long ID_Texture { get; protected set; }
 
        //Costruttore di default, tornerà utile quando avremo bisogno di serializzare/deserializzare (per esempio tramite XML) i dati relativi ad uno spriteSheet
        public Simple_SpriteSheet() : this(0, 0, -1) { }
 
        //Costruttore con settaggio dei parametri
        public Simple_SpriteSheet(int rows, int cols, long ID_Texture)
        {
            this.Rows = rows;
            this.Cols = cols;
            this.ID_Texture = ID_Texture;
        }
 
        /// <summary>
        ///  Ritorna il rettangolo in cui è occupato dal frammento specificato dall'id passato
        /// </summary>
        /// <param name="text">Texture da utilizzare</param>
        /// <param name="ID_Fragment">Id del frame</param>
        /// <returns>Rettangolo occupato dal frame</returns>
        public Rectangle GetRectangle(Texture2D text, int ID_Fragment)
        {
            //texture non trovata
            if (text == null) return Rectangle.Empty;
            /*l'ID_Fragment è come se fosse l'id di un elemento di un array
             * con queste formule lego la visione a vettore unidimensionale a quella matriciale
             */
            int x = ID_Fragment % Cols;
            int y = ID_Fragment / Cols;
            //calcolo quanto sono lunghi i lati di un frammento/frame
            int xStep = text.Width / Cols;
            int yStep = text.Height / Rows;
 
            return new Rectangle(xStep * x, yStep * y, xStep, yStep);
        }
 
        /// <summary>
        /// Imposta nel rettangolo passato per riferimento il rettangolo occupato dal frammento specificato dall'id passato
        /// </summary>
        /// <param name="text">Texture da utilizzare<</param>
        /// <param name="ID_Fragment">Id del frame</param>
        /// <param name="rec">Rettangolo per il risultato</param>
        public void GetRectangle(Texture2D text, int ID_Fragment, ref Rectangle rec)
        {
            if (text == null) rec = Rectangle.Empty;
            else
            {
                int x = ID_Fragment % Cols;
                int y = ID_Fragment / Cols;
                int xStep = text.Width / Cols;
                int yStep = text.Height / Rows;
                //imposto i valori
                rec.X = xStep * x;
                rec.Y = yStep * y;
                rec.Width = xStep;
                rec.Height = yStep;
            }
        }
 
        /// <summary>
        /// Ritorna il rettangolo in cui è occupato dal frammento specificato dall'id passato
        /// </summary>
        /// <param name="store">Assest_store da utilizzare per cercare l'immagine</param>
        /// <param name="ID_Fragment">Frammento di cui vogliamo recuperare il rettangolo</param>
        /// <returns>Il rettangolo del frammento specificato</returns>
        public Rectangle GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment)
        {
            //valore ID non corretto
            if (store == null || ID_Fragment < 0 || ID_Fragment > FrameNumber) return Rectangle.Empty;
 
            return GetRectangle(store.Get(ID_Texture), ID_Fragment);
        }
 
        /// <summary>
        /// Imposta nel rettangolo passato per riferimento il rettangolo occupato dal frammento specificato dall'id passato
        /// </summary>
        /// <param name="store">Assest_store da utilizzare per cercare l'immagine</param>
        /// <param name="ID_Fragment">Frammento di cui vogliamo recuperare il rettangolo</param>
        /// <param name="rec">Riferimento per il risultato</param>
        public void GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment, ref Rectangle rec)
        {
            if (store == null || ID_Fragment < 0 || ID_Fragment > FrameNumber) rec = Rectangle.Empty;
            else GetRectangle(store.Get(ID_Texture), ID_Fragment, ref rec);
        }
    }
}

Come potete vedere ho fornito ben 4 prototipi diversi del metodo per recuperare il Rectangle di un frammento. Questo perchè nella mia mente ho previsto come possibilità il fatto che questi oggetti vengano utilizzati sia assieme agli Asset_Store da me definiti che in modo separato.
Poi c’è a chi piace avere il risultato tramite un ref più che tramite un valore ritornato dal metodo. E quindi accontentiamo pure loro 😛
Il core dei metodi GetRectangle è sempre il solito: presa l’immagine e l’id del frammento di cui vogliamo il rettangolo non facciamo altro che passare dalla visione a vettore (array monodimensionale) data dalla progressione degli id, alla visione matriciale (e non “visione matriciana” come diceva un mio professore tanto per scherzare) dove abbiamo le due coordinate (per riga e per colonna).
Una volta che abbiamo le due coordinate non facciamo altro che calcolare i lati di un singolo frammento per poi calcolare e restituire il rettangolo che ci interessa.

Quindi, come detto, nulla di così trascendentale.

Ma come si utilizza tutto questo?
Vediamo un semplice esempio.

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;
using GameLibrary.Classes;
 
namespace MyGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        //riferimento all'inputDevice
        SimpleInputDevice input;
 
        //frammento da renderizzare
        int frag = 0;
 
        //riferimento all'id dell'immagine
        long id;
        //ultimo valore della rotellina
        int value = 0;
        //riferimento al componente per l'archivio delle texture
        Asset_StoreI<Texture2D> textureAsset;
        //singolo spriteSheet
        Simple_SpriteSheet sss;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            Components.Add(new FPS_Counter(this, true));
            Components.Add((input = new SimpleInputDevice(this)));
            //creo il component
            textureAsset = new Texture2D_Store(this);
            //lo aggiungo
            Components.Add((Texture2D_Store)textureAsset);
 
            Components.Add(new Simple_SpriteSheet_Store(this));
 
            //rende visibile il mouse
            IsMouseVisible = true;
        }
 
        protected override void Initialize()
        {
            value = input.ActualMouseState.ScrollWheelValue;
            base.Initialize();
        }
 
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            id = textureAsset.Load("orma_SpriteSheet");
            //creo lo sprite sheet
            sss = new Simple_SpriteSheet(3, 4, id);
        }
 
        protected override void Update(GameTime gameTime)
        {
            //se abbiamo fatto scroll aggiorno il valore di frag
            if (value < input.ActualMouseState.ScrollWheelValue)
                frag++;
            else if (value > input.ActualMouseState.ScrollWheelValue)
                frag--;
            value = input.ActualMouseState.ScrollWheelValue;
            //controllo che il valore rimanga tra 0 ed il numero di frame -1
            frag = frag < 0 ? 0 : frag >= sss.FrameNumber ? sss.FrameNumber - 1 : frag;
            base.Update(gameTime);
        }
 
        protected override void Draw(GameTime gameTime)
        {
            //recupero la texture
            Texture2D myImg = textureAsset.Get(id);
            Rectangle srcRec = sss.GetRectangle(myImg, frag);
            GraphicsDevice.Clear(Color.CornflowerBlue);
            //comincio a disegnare
            spriteBatch.Begin();
            //sfrutto uno degli overload di Draw per disegnare solo il pezzo di immagine dato dal rettangolo
            spriteBatch.Draw(myImg, input.ActualMousePosition, srcRec, Color.White); //renderizzo
            spriteBatch.End();//fine
            base.Draw(gameTime);
        }
    }
}

anche qua nulla di così difficile.
Se lo eseguiamo e proviamo a muovere la rotellina del mouse vedremo che in automatico cambiare la parte di immagine disegnata (ce ne accorgeremo perchè cambia il numero impresso sull’orma).
Bello vero?
Ora immaginate l’uso che possiamo farne per la creazione delle animazioni 😛
Ed è proprio quello di cui mi voglio occupare in uno dei prossimi articoli.

Prima di concludere voglio farvi vedere un’altra semplice cosa: nel precedente articolo avevo creato quello che ora chiamo Asset_StoreI, per la gestione degli asset.
Ecco come si presenta un GameComponent (aggiunto anch’esso come servizio) che si occupa di memorizzare gli SpriteSheet.

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;
using GameLibrary.Classes;
 
namespace GameLibrary.Component
{
    public class Simple_SpriteSheet_Store : Microsoft.Xna.Framework.GameComponent, Asset_StoreI<Simple_SpriteSheet>
    {
        //collezione degli spriteSheet
        SortedList<long, Simple_SpriteSheet> collection;
 
        public Simple_SpriteSheet_Store(Game game)
            : base(game)
        {
            //lo aggiungo ai servizi
            game.Services.AddService(typeof(Asset_StoreI<Simple_SpriteSheet>), this);
            //lo disattivo: mi serve solo come contenitore, non mi serve che venga eseguito nulla
            Enabled = false;
 
            collection = new SortedList<long, Simple_SpriteSheet>();
        }
 
        public void Add(long key, Simple_SpriteSheet value)
        {
            collection.Add(key, value);
        }
 
        public void Remove(long key)
        {
            collection.Remove(key);
        }
 
        public void Clear()
        {
            collection.Clear();
        }
 
        public long Load(string path)
        {
            throw new NotImplementedException();
        }
 
        public long Load(long key, string path)
        {
            throw new NotImplementedException();
        }
 
        public Simple_SpriteSheet Get(long key)
        {
            return collection[key];
        }
    }
}

Che ve ne pare?
Come vede questo sistema per la creazione di Asset_Store personalizzati non è poi così male! Si estende ed adatta facilmente e velocemente alle nostre esigenze. E c’è ancora moltissimo spazio per eventuali personalizzazioni.
I metodi Load non sono implementati perchè al livello attuale non ce ne è bisogno, ma nulla vita di personalizzarli in futuro secondo le proprio esigenze.

Vi allego i codici di questo articolo:
IndieGearLab_05.rar

Come sempre fatemi sapere se ci sono dubbi o commenti ^^

Alla prossima!

Lascia un commento

Your email address will not be published.

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