The XNA-Way #7: Sprite Sheet pt2

Dopo l’articolo sul piccolo programmino che ho scritto, lo SpriteOrganizer, vediamo ora come utilizzare in un progetto (nel mio caso realizzato con XNA) i file di output del programma.

Per l’immagine “compattata”, quella cioè dove tutti i frame sono disposti regolarmente ed uniformemente in una singola immagine, vi rimando agli altri articoli che ho scritto

Qua spiegherò solo come utilizzare in XNA il file di configurazione generato dal programma.

Come immagine campione userò questa

Gotenks

(scusate, forse sono i residui di quando da piccolo guardavo Dragon Ball, o forse perchè quelli di Dragon Ball Hyper Dimension sono tra gli sprite con più animazioni :P)

Apriamo SpriteOrganizer, carichiamo l’immagine e selezioniamo i frame della prima riga. Così:

Risultato

Salviamo il tutto, e nel mio caso il file si chiama Gothenks.sconf.

Fin qua facile. Ora dobbiamo pensare a come strutturare il codice per poter caricare i dati contenuti nel file, e per utilizzarli al meglio.
Il file si dovrebbe presentare più o meno così:

<!--?xml version="1.0" encoding="utf-8"?-->
 
  path
 
    4
    5
    49
    57
 
    56
    5
    50
    57
 
....

Nel progetto che potete scaricare da uno dei miei vecchi articoli, i Simple_SpriteSheet erano definiti come un’immagine, di cui ne fornivamo il numero di righe e colonne per poter agevolmente dividerla in parti eguali e calcolare il rettangolo da disegnare.
Qua non è necessario, dato che nel file è già contenuta l’informazione che ci serve, e cioè la posizione assoluta in pixel dei frame e quando è grande il rettangolo che li contiene.
Il recupero delle informazioni non si può certo fare per ogni frame in fase di rendering, ma va fatto in fase di inizializzazione/avvio del gioco. Quindi ci serve qualcosa che faccia da reader.
Lo sprite sheet dovrà però memorizzare direttamente come sono fatti i frame, quindi mi servirà una lista di frame/rettangoli. Prima identificavo un frame con il suo ID, che era la posizione di questo nel file (considerando la tabella di frame come se fosse distesa). Dovrò usare un ID anche qua, ma questa volta sarà la posizione del frame nella lista.

Come possiamo notare nel Simple_SpriteSheet avevamo un calcolo del bounding rectangle del frame più complicata, ma occupazione di spazio molto limitata (giusto le variabili per il numero di righe/colonne e per identificare l’immagine a cui facciamo riferimento), mentre con questa nuova gestione che andiamo ora ad implementare avremo un calcolo del rettangolo inesistente (dato che i dati ci vengono forniti in input) però avremo un’occupazione di spazio molto maggiore: se ci pensate in una sola immagine ci possono anche essere decine o centinaia di frame (grandi o piccoli che siano) e adesso ce li dobbiamo memorizzare tutti.
Quindi attenzione a cosa volete ottenere e ai limiti/requisiti memoria a vostra disposizione.

Per mantenere costante la “logica del tutto” ho dovuto fare alcune modifiche alla vecchia struttura del progetto:

  • Ho aggiunto la seguente interfaccia, in modo da avere una visione “generalizzata” degli sprite, complessi o semplici che siano
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
     
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using GameLibrary.Interfaces;
     
    namespace GameLibrary.Interfaces
    {
        public interface SpriteSheetI
        {
            int FrameNumber { get; }
            long ID_Texture { get; }
            Rectangle GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment);
            void GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment, ref Rectangle rec);
            Rectangle GetRectangle(Texture2D text, int ID_Fragment);
            void GetRectangle(Texture2D text, int ID_Fragment, ref Rectangle rec);
        }
    }
  • Ho modificato la classe Simple_SpriteSheet in modo che implementi l’interfaccia sopra definita (la implementa già di suo, ma nell’intestazione della classe va definita questa relazione esplicitamente)
  • Ho aggiunto un nuovo store, denominato Generic_SpriteSheet_Store il quale conterrà la versione generica SpriteSheetI (in questo modo può contenere sia i Simple_SpriteSheet sia i File_SpriteSheet che definirò sotto)
  • Ho cambiato l’Animation_Manager in modo che lavori solo utilizzando il Generic_SpriteSheet_Store (anche se per correttezza si dovrebbe parametrizzare la scelta di quale store utilizzare, ma ancora devo trovare un modo più adeguato per fare quest, magari con un generic…)
  • Ho creato la classe File_SpriteSheet
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
     
    using System.Xml;
     
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
     
    using GameLibrary.Interfaces;
     
    namespace GameLibrary.Classes
    {
        public class File_SpriteSheet : SpriteSheetI
        {
            //Lista dei frame
            protected List<Rectangle> FrameList;
     
            public File_SpriteSheet() { }
     
            public File_SpriteSheet(long id_texture, string filePath)
                : this(id_texture, filePath, "frame", "x", "y", "w", "h") 
            { }
     
            //permette di personalizzare la struttura del file xml che viene letto, così da poter leggere (con poche modifiche o con la sola modifica del nome dei tag)
            //altri file xml con la stessa struttura
            public File_SpriteSheet(long id_texture, string filePath, string frameTagName, string xTagName, string yTagName, string widthTagName, string heightTagName)
            {
                this.ID_Texture = id_texture;
                FrameList = new List<Rectangle>();
                //leggo il documento xml passato
                if (System.IO.File.Exists(filePath))
                {
                    XmlDocument doc = new XmlDocument();
                    doc.Load(filePath);
                    foreach (XmlElement elm in doc.GetElementsByTagName(frameTagName))
                    {
                        FrameList.Add(new Rectangle(
                            int.Parse(elm.GetElementsByTagName(xTagName)[0].InnerText),
                            int.Parse(elm.GetElementsByTagName(yTagName)[0].InnerText),
                            int.Parse(elm.GetElementsByTagName(widthTagName)[0].InnerText),
                            int.Parse(elm.GetElementsByTagName(heightTagName)[0].InnerText)));
                    }
                }
            }
     
            public long ID_Texture { get; protected set; }
            public int FrameNumber { get { return FrameList.Count; } }        
     
            //il parametro store non seriverebbe, ma per consistenza dell'interfaccia va mantenuto
            public Rectangle GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment)
            {
                return (ID_Fragment < 0 || ID_Fragment >= FrameList.Count) ? Rectangle.Empty : FrameList[ID_Fragment];
            }
     
            public void GetRectangle(Asset_StoreI<Texture2D> store, int ID_Fragment, ref Rectangle rec)
            {
                rec = (ID_Fragment < 0 || ID_Fragment >= FrameList.Count) ? Rectangle.Empty : FrameList[ID_Fragment];
            }
     
            public Rectangle GetRectangle(Texture2D text, int ID_Fragment)
            {
                return (ID_Fragment < 0 || ID_Fragment >= FrameList.Count) ? Rectangle.Empty : FrameList[ID_Fragment];
            }
     
            public void GetRectangle(Texture2D text, int ID_Fragment, ref Rectangle rec)
            {
                rec = (ID_Fragment < 0 || ID_Fragment >= FrameList.Count) ? Rectangle.Empty : FrameList[ID_Fragment];
            }
        }
    }

    Il funzionamento mi pare banale, ma tanto per ribadire le cose ovvie:

    • Nel costruttore inizializzo i campi dell’oggetto e leggo il file xml che lo definisce (se tale file esiste)
    • Nei metodi getRectangle non faccio altro che accedere alla lista dei frame e recuperare il corrispettivo elemento (come preannunciato prima non ci sono computazioni, solo accesso in memoria)

La cosa mi pare appunto abbastanza semplice.

Come far funzionare il tutto ora?
Copiamo l’immagine nel ContentProject, importiamo il file nel progetto MyGame, ed impostiamo nelle sue proprietà la voce Copia nella directory di output -> Copia Sempre.
Il file Game1.cs va modificato come segue:

....
Generic_SpriteSheet_Store gssStore; // al posto del simple_spritesheet_store
....
public Game1()
        {
.......
            //aggiungo lo spriteSheet Store
            Components.Add((gssStore = new Generic_SpriteSheet_Store(this)));
            //aggiungo l'animation manager
            Components.Add((aniManager = new Animation_Manager(this)));
.....
        }
 
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            //aggiungo e carico la texture
            long id = textureAsset.Load("Gotenks");
            //creo ed aggiungo lo sprite Sheet
            gssStore.Add(0, new File_SpriteSheet(id, Environment.CurrentDirectory + "\\gotenks.sconf"));
            //creo ed aggiungo l'animazione
            aniManager.Add(0, new Simple_Animation(0, new int[] { 0, 1, 2, 3, 4}, false));
        }
....

Per il resto rimane tutto uguale.

Quando lanciate il gioco e cliccate con il mouse vi dovrebbe apparire per circa un secondo l’animazione.
Ma c’è un “difetto”: l’animazione è come se “vibrasse”. Questo accade perchè i vari frame non sono tutti della stessa dimensione ma variano di pochi pixel tra di loro. Quindi non è un vero difetto, dato che il funzionamento logico è corretto (il rettangolo viene posizionato alle coordinate del mouse, viene renderizzata la parte giusta di immagine, quello che varia è la dimensione del frame, e quindi è giusto che ci sia questa impressione di tremore).
Come ovviare a questo tremolio? Si dovrebbe centrare il render del frame in modo che un punto del rettangolo rimanga fisso (di solito io tendo a scegliere come punto da mantenere fisso quello dove logicamente i piedi, così da dare l’impressione che il personaggio sia fermo, e che muova solo il busto).
Come si fa questo? Con delle semplice sottrazioni/addizioni sulla posizione del frame da renderizzare.
Ma lascio a voi la prova.
Se avete difficoltà nel farlo non esitate a contattarmi.

Vi allego i file del progetto.
IndieGearLab_07.rar

Alla prossima 🙂

Lascia un commento

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