The XNA-Way #6: Animazioni

Dopo i Simple_SpriteSheet vediamo un semplice modo per la creazione delle animazioni, dove i frame che la compongono saranno appunti i frammenti di immagine che recupereremo dai nostri spriteSheet.
Per fare questo avremo bisogno di spriteSheet ben fatti e ben strutturati, che possono essere recuperati da molti siti.

Se non si vuole fare molta fatica a cercare spriteSheet possiamo affidarci ai numeri forum a tema sulle varie versioni di RPG Maker, tra cui posso consigliare questo
http://www.rpgrevolution.com/forums/
dopo la registrazione sarà possibile scaricare le risorse offerte.
Per questo articolo userò la seguente immagine come riferimento per gli esempi.

Immagine dell’animazione di una skill

Vediamo di cominciare!

[notice]Io ho rinominato l’immagine come “skill.png” dopo averla aggiunta al Content Project, in modo da avere un path più breve e facilmente utilizzabile.[/notice]

Se sostituiamo nel codice del precedente articolo l’immagine che vi ho aggiunto sopra, e facciamo scroll con la rotellina del mouse, vedremo che “l’animazione prende vita”, anche se siamo noi manualmente ad eseguirla.

[notice]Se notate il colore di sfondo dell’immagine non scompare, non è trasparente, e sinceramente da noia… ma vedremo in seguito come fare. Non c’è nulla di sbagliato. Dovreste ottenere un qualcosa di simile a questo.

Come viene renderizzata l’immagine


[/notice]

Ora ci troviamo davanti ad un bivio però…
Pensiamo un attimo a cosa è un’animazione: un’animazione la possiamo vedere sia come una successione di frame presi tutti da uno stesso spriteSheet sorgente oppure la possiamo vedere come una successione di frame provenienti da più spriteSheet che concorrono tutti alla creazione dell’intera animazione.
A seconda di quale scelta facciamo ci troveremo di fronte a due implementazioni completamente diverse. Nel secondo caso i frame possono anche avere dimensione diversa tra loro! E quindi come dobbiamo regolarci con la posizione dei frame? Vanno tutti centrati? Hanno una posizione relativa (al centro dell’animazione) in teoria diversa tra loro?
Con questa gestione sicuramente c’è la possibilità di creare delle animazioni più complesse e belle, ma la cosa si fa complicata.

Facciamo come nel caso degli spriteSheet: la classe che ho illustrato è stata chiamta Simple_SpriteSheet proprio perchè la gestione degli spriteSheet è molto semplificata, e così farò anche ora. Andrò a considerare solo la versione più semplice della gestione delle animazioni. La gestione “complicata” delle animazioni la lascerò per quando scriverò un editor apposito (se ci pensate c’è da spararsi nello scrivere a mano come è fatta un’animazione parametrizzando e configurando la posizione dei frame, etc).

Ecco la classe per la definizione della nostra Simple_Animation

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace GameLibrary.Classes
{
    public class Simple_Animation
    {
        /// <summary>
        /// ID dello Sprite Sheet a cui l'animazione fa riferimento
        /// </summary>
        public long SpriteSheet_ID { get; protected set; }
 
        /// <summary>
        /// Sequenza dei frame che compongono l'animazione
        /// </summary>
        public List<int> FrameList { get; protected set; }
 
        /// <summary>
        /// Lunghezza dell'animazione
        /// </summary>
        public int FrameCount { get { return FrameList.Count; } }
 
        /// <summary>
        /// Specifica che l'animazione va ripetuta all'infinito
        /// </summary>
        public bool Loop { get; protected set; }
 
        public Simple_Animation() { FrameList = new List<int>(); }
 
        /// <summary>
        /// Crea la descrizione dell'animazione
        /// </summary>
        /// <param name="spriteSheet_ID">Id dello sprite sheet a cui fare riferimento (memorizzato in uno store apposito)</param>
        /// <param name="frameList">Lista dei frame che definiscono l'animazione</param>
        /// <param name="loop">true indica che l'animazione va ripetuta all'infinito</param>
        public Simple_Animation(long spriteSheet_ID, int[] frameList, bool loop)
            : this()
        {
            this.SpriteSheet_ID = spriteSheet_ID;
            foreach (int elm in frameList)
                this.FrameList.Add(elm);
 
            this.Loop = loop;
        }
    }
}

Molto semplice: questa classe descrive come è fatta un’animazione (NOTA: per rendere le cose più semplici in fase di creazione delle animazioni si potrebbe prevedere un campo per il nome da dare all’animazione, in modo che l’umano che è in noi sia facilitato nei suoi compiti). Un’animazione nello specifico fa riferimento ad uno SpriteSheet, il file immagine che raccoglie i frame dell’animazione. Specifica inoltre la sequenza di frame che compongono l’animazione, e con sequenza di frame intendo la sequenza degli id che identificano i singoli frame dell’animazione. Inoltre ho aggiunto un campo definire se l’animazione deve essere ripetuta all’infinito o se alla sua terminazione può essere rimossa.

Ok fino a qua chiaro. Ma come detto questa è la classe che descrive come sono fatte le animazioni. Ora pensiamo agli oggetti che serviranno per riprodurre un’animazione.
Di solito io tendo ad denominare tali oggetti che “fanno qualcosa” con lo stesso nome degli oggetti descrittivi più l’aggettivo “Active”. La mia classe sarà quindi Simple_Active_Animation.
Ecco il codice per essa:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using Microsoft.Xna.Framework;
 
namespace GameLibrary.Classes
{
    public class Simple_Active_Animation
    {
        /// <summary>
        /// Numero del frame corrente dell'animazione
        /// </summary>
        public int CurrentFrame { get; set; }
 
        /// <summary>
        /// Tempo passato dall'ultimo cambio di frame
        /// </summary>
        public int ElapsedTime { get; set; }
 
        /// <summary>
        /// Se true indica che l'animazione è terminata
        /// </summary>
        public bool Ended { get { return CurrentFrame >= Simple_Animation.FrameCount; } }
 
        /// <summary>
        /// Riferimento alla descrizionde dell'animazione
        /// </summary>
        public Simple_Animation Simple_Animation { get; protected set; }
 
        /// <summary>
        /// Posizione dell'animazione da riprodurre
        /// </summary>
        public Vector2 Position { get; set; }
 
        //serve a definire quando deve durare un frame (in millisecondi) e detemina la durata dell'animazione
        private int elapsedIntervall;
 
        /// <summary>
        /// Crea un'animazione attiva da riprodurre
        /// </summary>
        /// <param name="Simple_Animation">Riferimento alla descrizione dell'animazione</param>
        /// <param name="elapsedIntervall">Definisce la durata di un frame in millisecondi</param>
        public Simple_Active_Animation(Simple_Animation Simple_Animation, int elapsedIntervall) : this(Simple_Animation, elapsedIntervall, Vector2.Zero) { }
 
        /// <summary>
        /// Crea un'animazione attiva da riprodurre
        /// </summary>
        /// <param name="Simple_Animation">Riferimento alla descrizione dell'animazione</param>
        /// <param name="elapsedIntervall">Definisce la durata di un frame in millisecondi</param>
        /// <param name="position">Posizione dell'animazione</param>
        public Simple_Active_Animation(Simple_Animation Simple_Animation, int elapsedIntervall, Vector2 position)
        {
            this.Simple_Animation = Simple_Animation;
            this.elapsedIntervall = elapsedIntervall;
            this.Position = position;
        }
 
        /// <summary>
        /// Aggiorna lo stato dell'animazione
        /// </summary>
        /// <param name="delta">Numero di millisecondi che sono trascorsi</param>
        public void Update(int delta)
        {
            ElapsedTime += delta; //aggiungo il tempo trascrorso
            if (ElapsedTime >= elapsedIntervall) //devo cambiare frame
            {
                ElapsedTime = 0;
                CurrentFrame++;
            }
            //Sono giunto alla termine e l'animazione è in loop?
            if (CurrentFrame >= Simple_Animation.FrameCount && Simple_Animation.Loop)
                CurrentFrame = 0;
        }
    }
}

Mi serve di memorizzare per ogni elemento attivo a quale animazione fa riferimento, e per fare questo ne memorizzo l’id.
Ogni animazione attiva starà eseguendo/renderizzando un particolare frame, e per questo mi serve di memorizzare quale è il frame attuale da usare. Inoltre devo poter cambiare il frame attuale dopo un certo periodo di tempo fissato, e per rendere possibile questo ho deciso di memorizzare il tempo passato dall’ultimo cambio frame: questo valore sarà incrementato ad ogni ciclo update-draw del gioco e quando avrà raggiunto una certa soglia sarà azzerato e nel contempo passeremo al frame successivo.
Inoltre c’è la questione del dove posizionare l’animazione, e la questione della posizione è un po’ complicata e per questo voglio aprire una piccola parentesi concettuale.
[notice]Quando si parla di coordinate spaziali, sia bidimensionali che tridimensionali dobbiamo distinguere tra le coordinate world e le view.
Con le prime di solito si tende ad indicare le coordinate che fanno riferimento alla posizione di un certo oggetto all’interno del mondo del nostro gioco/progetto.
Le coordinate view sono invece relative a quello che viene renderizzato/mostrato a schermo o nella finestra del gioco. Di solito le coordinate view hanno le dimensioni della risoluzione della finestra di gioco.
Nel processo di rendering (o meglio uno dei passi del processo di rendering) è quello di proiettare un oggetto nella vista, decidere se è visibile o meno, e quindi renderizzarlo. La posizione che passiamo al metodo Draw dello spriteBath lavora con le coordinate view, e quindi indica dove andrà a finire l’oggetto sullo schermo.

Per fare il processo di proiezione di un oggetto si usano delle funzioni denominate World-To-View abbreviata in WTV (e le loro inverse View-To-World abbreviata con VTW) per passare da un sistema di coordinate all’altro.
In questo esempio io NON HO USATO nessuna di esse: il Vector2 Position nella Simple_Active_Animation è la posizione finale dell’animazione sullo schermo, quindi sono già le coordinate view.

Però vi invito a considerare quanto segue: pensiamo che l’animazione sia collegata ad un certo elemento attivo di un nostro videogioco (per esempio un villain). Questo elemento avrà la sua posizione nel nostro livello (che potrà cambiare nel tempo per via della IA che lo fa agire a seconda dello stato in cui si trova) e con la funzione WTV ne faremo la proiezione nelle coordinate View. Saremo quindi in grado di determinare se l’oggetto è visibile o meno, e se chiamare la primitiva di rendering o meno (e questo comporterà un risparmio di tempo).[/notice]

Vediamo ora come utilizzare le animazioni.
Riprendiamo gli Asset_Store.
Questa volta unificherò un asset_store con un DrawableGameComponent per la gestione e riproduzione delle animazioni (ricordate quando ho scritto che si potevano creare degli Asset_Store che potevano eseguire delle operazioni particolari?).

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
{
    /// <summary>
    /// Oltre ad essere il componente che verrà utilizzato per renderizzare le animazioni a schermo sarà anche il componente adibito 
    /// alla gestione/caricamento delle descrizioni delle varie animazioni
    /// </summary>
    public class Animation_Manager : Microsoft.Xna.Framework.DrawableGameComponent, Asset_StoreI<Simple_Animation>
    {
        //Collezione delle descrizioni delle animazioni
        private SortedList<long, Simple_Animation> animations;
 
        //Collezione delle animazioni da riprodurre
        private List<Simple_Active_Animation> activeAnimations;
 
        //Spritebatch per il render della animazioni di questo component
        SpriteBatch spriteBatch;
 
        //riferimento allo store degli SpriteSheet
        Asset_StoreI<Simple_SpriteSheet> spriteSheet_store;
        //riferimento allo store delle texture
        Asset_StoreI<Texture2D> texture_Store;
 
#if DEBUG
        //Codice disponibile solo in modalità debug: riferimento all'input manager
        SimpleInputDeviceI input;
#endif
 
        public Animation_Manager(Game game)
            : base(game)
        {
            //creo le collezioni
            animations = new SortedList<long, Simple_Animation>();
            activeAnimations = new List<Simple_Active_Animation>();
            //aggiungo il servizio
            game.Services.AddService(typeof(Asset_StoreI<Simple_Animation>), this);
            //recupero i servizi degli altri componenti
            spriteSheet_store = game.Services.GetService(typeof(Asset_StoreI<Simple_SpriteSheet>)) as Asset_StoreI<Simple_SpriteSheet>;
            texture_Store = game.Services.GetService(typeof(Asset_StoreI<Texture2D>)) as Asset_StoreI<Texture2D>;
 
#if DEBUG
            //recupero il servizio per l'input
            input = game.Services.GetService(typeof(SimpleInputDeviceI)) as SimpleInputDeviceI;
#endif
        }
 
        protected override void LoadContent()
        {
            //credo lo spriteBatch di questo drawableGameComponent
            spriteBatch = new SpriteBatch(GraphicsDevice);
            base.LoadContent();
        }
 
        public override void Update(GameTime gameTime)
        {
            //tempo trascorso
            int delta = gameTime.ElapsedGameTime.Milliseconds;
#if DEBUG
            //se il pulsante sinistro del mouse è cliccato aggiungi un'animazione da riprodurre
            if (input.ActualMouseState.LeftButton == ButtonState.Pressed && input.LastMouseState.LeftButton == ButtonState.Released 
                && animations.Count != 0)
            {
                activeAnimations.Add(new Simple_Active_Animation(animations[animations.Keys[0]], 40, input.ActualMousePosition));
            }
#endif
            //aggiorna lo stato di tutte le animazioni gestite
            for (int i = activeAnimations.Count - 1; i >= 0; i--)
            {
                activeAnimations[i].Update(delta);
                if (activeAnimations[i].Ended)
                    activeAnimations.RemoveAt(i);
            }
            base.Update(gameTime);
        }
 
        public override void Draw(GameTime gameTime)
        {
            //comincia la fase di disegno
            spriteBatch.Begin();
            //per ogni animazione attiva
            foreach (Simple_Active_Animation saa in activeAnimations)
            {
                //NOTA: è qua che andrebbe inserito un controllo tipo
                //"renderizza solo se è all'interno dello schermo"
 
                //recupero la descrizione dell'animazione
                Simple_Animation sa = saa.Simple_Animation;
                //recupero lo spriteSheet
                Simple_SpriteSheet sss = spriteSheet_store.Get(sa.SpriteSheet_ID);
                //recupero la texture
                Texture2D text = texture_Store.Get(sss.ID_Texture);
                //calcolo il rettangolo
                Rectangle rec = sss.GetRectangle(text, sa.FrameList[saa.CurrentFrame]);
                //disegno
                spriteBatch.Draw(text, saa.Position, rec, Color.White);
            }
            //fine
            spriteBatch.End();
            base.Draw(gameTime);
        }
 
        public void Add(long key, Simple_Animation value)
        {
            animations.Add(key, value);
        }
 
        public void Remove(long key)
        {
            throw new NotImplementedException();
        }
 
        public void Clear()
        {
            throw new NotImplementedException();
        }
 
        public long Load(string path)
        {
            throw new NotImplementedException();
        }
 
        public long Load(long key, string path)
        {
            throw new NotImplementedException();
        }
 
        public Simple_Animation Get(long key)
        {
            return animations[key];
        }
    }
}

Nella parte finale del Component c’è l’implementazione dei metodi che ci interessano dell’interfaccia Asset_StoreI.
Come potete vedere questa volta il component non viene disattivato, quindi sarà effettivamente eseguito!
Ci sono delle parti con delle direttive del pre-compilatore, ma non credo che sia necessario commentarle ulteriormente. Il codice mi pare sufficientemente commentato e chiaro. Quindi non mi voglio dilungare troppo.

Voglio invece passare all’utilizzo diretto del codice che ho appena scritto.
Vediamo il Game1.cs

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;
 
        //riferimento al componente per l'archivio delle texture
        Asset_StoreI<Texture2D> textureAsset;
        //riferimento allo store degli spriteSheet
        Simple_SpriteSheet_Store sssStore;
        //riferimento all'animation manager
        Animation_Manager aniManager;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            Components.Add(new FPS_Counter(this, true));
            Components.Add(new ScreenShotter(this));
            Components.Add((input = new SimpleInputDevice(this)));
            //creo il component
            textureAsset = new Texture2D_Store(this);
            //lo aggiungo
            Components.Add((Texture2D_Store)textureAsset);
            //aggiungo lo spriteSheet Store
            Components.Add((sssStore = new Simple_SpriteSheet_Store(this)));
            //aggiungo l'animation manager
            Components.Add((aniManager = new Animation_Manager(this)));
 
            //rende visibile il mouse
            IsMouseVisible = true;
        }
 
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            //aggiungo e carico la texture
            long id = textureAsset.Load("skill");
            //creo ed aggiungo lo sprite Sheet
            sssStore.Add(0, new Simple_SpriteSheet(4, 5, id));
            //creo ed aggiungo l'animazione
            aniManager.Add(0, new Simple_Animation(0, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, 17, 18, 19 }, false));
        }
 
        protected override void Update(GameTime gameTime)
        {
            //permette l'uscita dal gioco premendo il tasto escape
            if (input.IsDown(Keys.Escape)) this.Exit();
            base.Update(gameTime);
        }
 
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }
    }
}

Quando si esegue il tutto il risultato sarà che ogni volta che faremo click con il tasto sinistro del mouse verrà creata un’istanza di una Simple_Active_Animation che verrà riprodotta e poi verrà eliminata.
Se facciamo click diverse volte il risultato sarà simile a questo!

Risultato finale dell’animation Manager

Bello vero?

Ora però lascio a chi legge come sfida quella di eliminare dal render lo sfondo nero dei frame 😛
Vediamo chi ce la fa! Io la mia implementazione ce l’ho. Vediamo cosa tirate fuori voi.
Avrete in palio una chiave di un gioco da riscattare su Stem se ci riuscite!
Non c’è trucco non c’è inganno. Di codici riscattabili ne ho tanti e li regalerò di volta in volta con piccole sfide tipo queste 🙂

Vi allego il codice!
IndieGearLab_06.rar

Per ogni chiarimento commentate o contattatemi.
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