- Animazioni in XNA -
 
COSA SERVE PER QUESTO TUTORIAL
Download | Chiedi sul FORUM | Glossario cognizioni basiche di C#
Come creare un personaggio animato

LA CLASSE ANIMATION
Una classe contenente tutte le informazioni che riguardano l'animazione.

Nel precedente tutorial abbiamo visto come è strutturato un gioco di XNA, come gestire la logica di gioco e le funzionalità basiche per il disegno; oggi ci concentreremo principalmente su come creare uno "personaggio" animato e come dargli altre funzionalità basiche come la possibilità di ruotare o di ridimensionarsi. In concreto creeremo una stellina animata e rotante nell'area di gioco alla pressione del tasto sinistro del mouse. Per fare questo dovremo creare due classi: la classe Character che conterrà la lista delle animazioni disponibili per il personaggio, la sua posizione e i metodi Update e Draw, e la classe Animation che servirà per mantenere ed elaborare informazioni sull'animazione come frame corrente, angolo di rotazione corrente, numero di volte che l'animazione è stata riprodotta e così via.
Iniziamo dalla classe Animation, dotata delle seguenti funzionalità:

  • memorizza angolo di rotazione (campo currentRotation) e frame corrente (campo currentFrame);
  • tiene conto del numero di volte che l'animazione è stata eseguita (campo repeated); se si eccede il limite massimo di esecuzioni (impostabile tramite la proprietà MaxRepeat), impedisce l'avanzamento dell'animazione, come vedremo tra poco;
  • memorizza il tempo in cui l'animazione è stata avviata (impostato quando viene chiamato il metodo Start), così da poter effettuare i calcoli che permettono di sapere quale frame e quale angolo di rotazione utilizzare;
  • mantiene impostazioni sull'animazione come la velocità (ovvero frame al secondo, proprietà fps), l'immagine da cui prendere i frame (proprietà Frames), la larghezza di un singolo frame (proprietà FrameWidth), la direzione in cui riprodurre l'animazione (proprietà Backward), la velocità angolare (proprietà AngularVelocity), l'angolo iniziale di rotazione (proprietà InitialAngle), il centro della rotazione (proprietà RotationPoint) e il fattore di scala (proprietà Scale);
  • calcola il numero totale di frame presenti nell'immagine effettuando la divisione tra la sua larghezza e la larghezza del singolo frame (proprietà TotalFrames);
  • calcola l'area (rettangolare) dell'immagine contenente il frame corrente (proprietà CurrentRectangle);

class Animation: ICloneable {

    // Numero di volte che l'animazione è stata ripetuta
    private int repeated;
    public int Repeated { get { return repeated; } }
    
    // Tempo in cui l'animazione è iniziata
    private TimeSpan startTime;
    
    // Numero massimo di ripetizioni dell'animazioni
    // dopo le quali Update restituirà false
    private int maxRepeat = int.MaxValue;
    
    // Angolo di rotazione corrente in radianti
    private double currentRotation = 0;
    public double CurrentRotation { get { return currentRotation; } }
    
    // Indice del frame corrente
    private int currentFrame = -1;
    public int CurrentFrame { get { return currentFrame; } }
    
    // Proprietà per la velocità con cui si devono
    // succedere i frame
    public float fps { get; set; }
    // Immagine contenente tutti i frame
    public Texture2D Frames { get; set; }
    // Larghezza del singolo frame
    public int FrameWidth { get; set; }
    // Se true indica che l'animazione deve essere
    // riprodotta in senso inverso
    public bool Backward { get; set; }
    // Velocità angolare con cui deve ruotare l'animazione
    public double AngularVelocity { get; set; }
    // Angolo iniziale di rotazione
    public double InitalAngle { get; set; }
    // Punto rispetto al quale effettuare la rotazione
    public Vector2 RotationPoint { get; set; }
    // Il fattore di ingrandimento dell'animazione
    public float Scale { get; set; }

    // [...] Costruttori tralasciati

    // Proprietà che restituisce il rettangolo relativo
    // all'immagine contenente i frame da dissgnare
    public Rectangle CurrentRectangle {
        get {
            return new Rectangle(this.FrameWidth * this.currentFrame, 0, this.FrameWidth, this.Frames.Height);
        }
    }

    // Proprietà per conoscere il numero totale di frame
    public int TotalFrames { get { return (this.Frames.Width / this.FrameWidth); } }

    // [...]

}

Vi è poi il metodo Start che null'altro fa che impostare il tempo di avvio dell'animazione a quello passatogli e quindi azzerare tutte le variabili interne:


// Avvia l'animazione, vengono azzerati angolo di 
// rotazione, numero di ripetizioni, frame corrente
// e tempo di avvio
public void Start(GameTime now) {
    this.repeated = 0;
    this.startTime = now.TotalGameTime;
    this.currentFrame = 0;
    this.currentRotation = 0;
}

Infine abbiamo il metodo Update che si occupa di aggiornare tutte le variabili interne a seconda del tempo trascorso dall'inizio dell'animazione:


// Aggiorna le variabili riguardanti l'animazione
// a seconda del tempo trascorso
public bool Update(GameTime now)
{
    // Se non è ancora stato chiamato il metodo Start
    // lo chiamiamo ora
    if (this.currentFrame == -1) this.Start(now);
    // Calcoliamo il numero di secondi trascorsi 
    // dall'avvio dell'animazione
    double totalSeconds = (now.TotalGameTime - startTime).TotalSeconds;

    // Calcoliamo il numero totale di frame (teoricamente)
    // disegnati finora
    double frameDrawn = (totalSeconds * fps);

    // Calcoliamo il numero di volte che l'animazione è
    // stata riprodotta
    repeated = (int)Math.Truncate(frameDrawn / this.TotalFrames);

    // Calcoliamo il frame da disegnare prendendo il resto
    // della divisione del totale dei frame disegnati
    // per il numero di frame
    currentFrame = (int)Math.Truncate(frameDrawn % this.TotalFrames);

    // Calcoliamo l'angolo di rotazione corrente sommando
    // all'angolo iniziale il prodotto del tempo trascorso
    // per la velocità angolare
    currentRotation = InitalAngle + totalSeconds * AngularVelocity;

    // Per far scorrere l'animazione all'incontrario 
    // semplicemente prendiamo il corrisponde opposto
    if (this.Backward) currentFrame = this.TotalFrames - currentFrame - 1;

    // Restituisce true se non è stato superato il numero 
    // di ripetizioni massimo
    return (Repeated < maxRepeat);
}

Per calcolare il numero di volte che l'animazione è stata riprodotta semplicemente dividiamo il numero totale di frame disegnati per i frame totali dell'animazione, per stabilire quale sia il frame corrente prendiamo il resto della divisione tra il numero totale di frame disegnati e il totale dei frame. Il numero totale di frame si ottiene semplicemente moltiplicando il tempo trascorso dall'inizio dell'animazione per il numero di frame al secondo. L'angolo di rotazione si ottiene in maniera simile: si somma all'angolo iniziale il prodotto della velocità angolare per il tempo trascorso.
Se è stato scelto di riprodurre l'animazione all'incontrario non si fa altro che prendere il frame opposto a quello che è risultato dai calcoli (es. se esce il primo si prende l'ultimo, se esce il penultimo si prende il secondo e così via). La funzione restituisce true se non è stato superato il numero di ripetizioni massime impostato.
Ora che abbiamo illustrato tutte le funzionalità della classe Animation possiamo passare a descrivere la classe che la utilizza: Character.

LA CLASSE ANIMATION
Una classe contenente tutte le informazioni che riguardano l'animazione.

Iniziamo dunque dalla classe Character:


class Character : DrawableGameComponent
{

    private SpriteBatch spriteBatch;

    // Indice dell'animazione corrente
    private int currentAnimationIndex = 0;
    // Array contenente tutte le animazioni del Character
    public Animation[] Animations { get; set; }
    // Posizione corrente del Character
    public Vector2 Position { get; set; }

    // Costruttori
    public Character(Game game, Vector2 InitialPosition) : base(game) { this.Position = InitialPosition; }
    public Character(Game game) : this(game, new Vector2(0, 0)) { }

    protected override void LoadContent()
    {
        base.LoadContent();

        // Otteniamo lo spriteBatch del gioco
        spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
    }

    // [...]

    public Animation CurrentAnimation
    {
        get { return Animations[currentAnimationIndex]; }
        set
        {
            currentAnimationIndex = Array.IndexOf(Animations, value);
            if (currentAnimationIndex == -1) throw new IndexOutOfRangeException();
        }

    }

}

Per prima cosa notiamo che essa eredita da DrawableGameComponent, il che significa che è un componente del gioco (vedremo in seguito come aggiungerlo) e che dispone quindi di tutti i metodi che abbiamo visto essere propri di Game, come Draw, Update, Initialize, LoadContent e così via. Tra la sue proprietà principali vi sono Animations che imposta o restituisce la lista delle animazioni disponibili (memorizzate sotto forma di un semplice array) e CurrentAnimation che restituisce quella che tra queste è correntemente in esecuzione, indicata dal campo currentAnimationIndex. L'ultima proprietà disponibile è Position che indica semplicemente la posizione in cui si trova l'oggetto da disegnare. Vediamo i metodi principali come sono implementati a partire dal metodo Update:


public override void Update(GameTime gameTime)
{
    base.Update(gameTime);

    // Aggiorna le variabili dell'animazione
    while (!this.CurrentAnimation.Update(gameTime))
    {
        // L'animazione è finita, passiamo alla successiva
        Animation oldAnimation = CurrentAnimation;
        CurrentAnimation = Animations[(currentAnimationIndex + 1) % Animations.Length];
        CurrentAnimation.Start(gameTime);
        CurrentAnimation.InitalAngle = oldAnimation.CurrentRotation;
    }
}

Ricordiamo che il metodo Update serve per gestire la logica di gioco, al contrario di Draw che si occupa di creare l'output grafico. Sostanzialmente questa funzione non fa altro che chiamare il metodo Update dell'oggetto Animation corrente (che abbiamo visto poco fa), comunicandogli le informazioni sul tempo di gioco cosicché possa stabilire quale frame mostrare. Questa chiamata è posta come condizione di un ciclo while poiché Update restituisce false se ha terminato il numero di esecuzioni massimo impostato, e in questo specifico caso abbiamo deciso di adottare la politica di passare all'animazione successiva se si verifica la condizione.


public override void Draw(GameTime gameTime)
{
    base.Draw(gameTime);

    // Disegnamo il character con tutte le informazioni
    // che ci vengono da this.CurrentAnimation, eccetto
    // la posizione corrente
    spriteBatch.Draw(this.CurrentAnimation.Frames,
        this.Position, this.CurrentAnimation.CurrentRectangle,
        Color.White, (float)this.CurrentAnimation.CurrentRotation, 
        this.CurrentAnimation.RotationPoint,
        this.CurrentAnimation.Scale, SpriteEffects.None, 0);
}

Il metodo Draw è relativamente semplice, non fa altro che utilizzare tutte le funzionalità (o quasi) del metodo SpriteBatch.Draw: il primo parametro indica l'immagine da cui prendere l'area da disegnare, il secondo la posizione dove disegnare, il terzo l'area dell'immagine da disegnare, il quarto un filtro da utilizzare sul colore (Color.White non applica alcuna trasformazione), il quinto l'angolo di rotazione (in radianti), il sesto il punto rispetto al quale effettuare la rotazione, l'ottavo il fattore di ingrandimento e gli ultimi due non hanno alcun effetto in questo caso.

LA CLASSE GAME
La classe principale del gioco che lo descrive al livello di astrazione maggiore.

La classe Game, quella principale del gioco e che lo gestisce, non ha grandi particolarità rispetto al precedente tutorial. Al di fuori del metodo Update, l'unica novità rilevante è aver impostato il mouse visibile nel costruttore impostando la proprietà IsMouseVisible su true. Passiamo subito al cuore della classe:


public class CreaStellineGame : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    // Immagine contenente i frame dell'animazione
    Texture2D framesStellina;

    // Stato del tasto sinistro del mouse dell'ultimo
    // Update
    ButtonState lastMouseState = ButtonState.Released;
    // Generatore di numeri casuali usato durante tutto
    // il gioco
    Random gameRandom = new Random();

    // [...]

    // Gestiamo la logica di gioco
    protected override void Update(GameTime gameTime)
    {
        if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Escape))
            this.Exit();

        // Prendiamo lo stato corrente del mouse
        MouseState mouse = Mouse.GetState();

        // Se è stato rilasciato il mouse
        if (mouse.LeftButton == ButtonState.Released && lastMouseState == ButtonState.Pressed) {

            // Creiamo l'animazione della stellina che si
            // ingrandisce
            Animation cresce = new Animation(framesStellina, 190, 25, 1);
            // Impostiamo una velocità angolare casuale
            cresce.AngularVelocity = (float) gameRandom.NextDouble() * 4 - 2;
            // Impostiamo una dimensione casuale tra le dimensioni
            // originali dell'immagine e la loro metà
            cresce.Scale = (float)gameRandom.NextDouble() * 0.5f + 0.5f;

            // Cloniamo l'animazione appena creata e impostiamo
            // che vada al contrario
            Animation decresce = (Animation) ((ICloneable) cresce).Clone();
            decresce.Backward = true;
            
            // Inizializziamo la stellina
            Character stellina = new Character(this, new Vector2(mouse.X, mouse.Y));
            // Impostiamo le animazioni della stellina
            stellina.Animations = new Animation[] { cresce, decresce };

            // Aggiungiamo la stellina appena creata ai
            // componenti del gioco
            this.Components.Add(stellina);
        }

        // Aggiorniamo qual è lo stato del tasto sinistro
        lastMouseState = mouse.LeftButton;

        base.Update(gameTime);
    }
    
    // [...]
}

Il metodo Update entra in azione quando viene rilasciato il tasto sinistro del mouse, ovvero quando si passa dallo stato di pressione a quello di rilascio tra una chiamata di Update e la successiva. Per creare una nuova stellina cominciamo a chiamare il costruttore di Character passandogli l'immagine contenente i frame precedentemente caricata (framesStellina) nel metodo LoadContent, la larghezza del singolo frame, i frame da mostrare al secondo e il numero massimo di ripetizioni. Impostando il numero massimo di ripetizioni faremo in modo che ogni volta che è stata completata un'animazione si passerà alla successiva. Impostiamo poi su valori casuali la velocità angolare e il fattore di ridimensionamento. A questo punto cloniamo l'animazione impostando però la proprietà Backward su true, in questo modo l'animazione cresce procederà normalmente, mentre decresce sarà del tutto equivalente eccetto per il fatto che procede a ritroso.
Non resta che creare un nuovo oggetto Character passandogli un riferimento al gioco e un vettore posizione (ottenuto dalle coordinate del mouse correnti) e impostiamo la sua proprietà Animations su un array composto dalle animazioni cresce e decresce. Infine chiamiamo il metodo Components.Add di Game passandogli la stellina appena creata, in questo modo essa verrà aggiunta alla lista dei componenti e saranno chiamati i suoi metodi Draw e Update ogni volta che sarà necessario.

 

<< INDIETRO by VeNoM00