Buenas!

Voy a mostrar hoy cómo hacer para animar un spritesheet en Unity3D, con C#. Antes que nada, quiero aclarar que sé que hay una manera mucho más “fácil”, que es con el Animator. Evito usar este componente por una cuestión de que es tan fácil aprenderlo que se puede ver algún tutorial en YouTube y listo. El tema es que si en algún momento tenemos que animar un spritesheet y no es en Unity3D, sino en cualquier otra plataforma, vamos a estar un poco complicados. Así que doy ahora la alternativa de hacerlo con código, de la manera más genérica que pueda, cosa que si en algún momento alguien usa otro engine solamente tenga que aprender las variaciones como:  “¿Cuál es el Update?”, “¿Cómo se pinta en pantalla?”, etc; en lugar de ¿Cuál es el Animator que hace todo por mí?

 

 

1) ANTES DE EMPEZAR…

Entonces… Sabemos que en un spritesheet vamos a tener una secuencia de “frames” (ver  Frame), la cual tiene que pasar X cantidad de veces por segundo para formar una animación (o dar la sensación de que hay una).

Lo primero que tenemos que hacer es cortar una plantilla. Vamos a usar el editor de Unity para eso (también se pueden usar otros editores, incluso Photoshop). Seleccionamos la imagen a cortar, y nos va a aparecer una ventana como esta. La que dice “Sprite Editor” nos va a aparecer luego de hacer click en el 3º paso.

 

tutorialunityspritesheet

 

Con esto podemos seleccionar los frames dentro de la ventana UNO… POR… UNO…

 

237157251

 

O… También se puede hacer automático de la siguiente manera:

 

tutorialunityspritesheet

 

Una vez hechos los cortes podemos cargar todas las imágenes juntas. Para eso tenemos que tener el sprite en una carpeta llamada “Resources” dentro de “Assets“. También se puede, como hice yo, ponerlo dentro de otra carpeta PERO… Esa carpeta debería estar sí o sí dentro de “Resources“. En mi caso yo cree una subcarpeta llamada “Characters“:

 

tutorialunityspritesheet

 

Como podemos ver, si desplegamos el sprite vamos a ver todos los cortes como archivos diferentes. Podemos agarrar cualquiera de esos archivos y usarlo como archivos individuales. Yo lo que suelo hacer es agarrar uno y tirarlo al escenario, y a ESE le voy a arrastrar todos los scripts como si fuera mi personaje.

El siguiente paso es poner un GameObject en la escena como para empezar a trabajar (como dije, puede ser una de esas imágenes):

 

tutorialunityspritesheet

 

Como se puede ver, si arrastramos uno de esos sprites cortados, también se crea el componente de SpriteRenderer.

 

 

 

2) EMPEZANDO CON LA PARTE DE CÓDIGO…

Para empezar con la parte de código, voy a crear una clase (script), llamada “MovieClip“, cuya finalidad va a ser representar un clip de película. Le pongo ese nombre para que los que programan en Action Script hagan la relación automática. Entre otras cosas, utilizaremos un SpriteRenderer para pintar cada frame. Las variables serían, por empezar:

 

 

[System.Serializable]
public class MovieClip
{
    //Para guardar el frame actual.
    public int currentFrame;

    //Acá vamos a guardar cada uno de los frames.
    public Sprite[] frames;

    //SpriteRenderer que se nos va a pasar como parámetro.
    public SpriteRenderer renderer;

    //FrameRate actual del MovieClip (esto es para controlar la velocidad de reproducción).
    public int frameRate = 15;

    //Tiempo que se tarda en pasar hasta otro frame.
    public float timeToChangeFrame;

    //Tiempo QUE FALTA para que pase al siguiente frame.
    public float currentTimeToChangeFrame;

 

 

Vamos ahora a crear una función que se deberá encargar de cargar cada uno de los frames. Para ello vamos a utilizar una función de Unity llamada a la cual pasandole como parámetro una URL nos devolverá un array de sprites que contiene todas las partes cortadas de nuestro personaje. En cuanto a nuestra función, vamos a pasarle por parámetro dos cosas: una ruta y un SpriteRenderer. La ruta será la del spritesheet y el otro es el componente que vamos a utilizar para cambiar entre frames. Otra cosa que también debería hacer nuestra función, ya que estamos, es setear cuánto sería el tiempo que se va a tardar en cambiar de un frame al otro. Sería de esta manera:

 

 

public void LoadFrames(string url, SpriteRenderer renderer)
{
    //Asigna la variable LOCAL (parámetro), dentro de la variable GLOBAL.
    this.renderer = renderer;

    //Carga un array de sprites que están en Resources.
    frames = Resources.LoadAll<Sprite>(url);

    //Asigna el frame que esté en el índice 0 como Sprite.
    renderer.sprite = frames[0];

    //Setea que cambie cada "frameRate parte" de un segundo. EJ: Si el frameRate es 2, cambiaría cada 0.5 segundos.
    timeToChangeFrame = 1f / frameRate;
}

 

 

Vamos a tener también una función Update que debería ejecutarse por cada frame de Unity, aunque no es el Update que viene con los MonoBehaviours ya que el script que estoy mostrando no lo es. Le saqué la línea que dice  MonoBehaviour arriba por una cuestión de rendimiento. Realmente no la necesita, ya que puede ser actualizada por la clase (script), que la contiene. Volviendo al tema, nuestra función de actualización debería antes de hacer cualquier cosa, preguntar si nuestro MovieClip se está reproduciendo. En tal caso, vamos a sumar más una fracción de segundo (Time.deltaTime), a nuestra variable “timeToChangeFrame“, ya que es la que dice cuándo se debería pintar el siguiente fotograma.

 

 

public void Update()
{
    //¿Se está reproduciendo?
    if (isPlaying)
    {
        //Sumamos la porción de segundo que corresponde a este frame.
        currentTimeToChangeFrame += Time.deltaTime;

        //¿Es momento de cambiar de frame?
        if (currentTimeToChangeFrame >= timeToChangeFrame)
        {
            //Avanzamos un frame.
            currentFrame++;

            //Seteamos el frame que se va a mostrar.
            renderer.sprite = frames[currentFrame];

            //Reiniciamos el contador de "cada cuánto" tiempo cambia un frame.
            currentTimeToChangeFrame = 0;
        }
    }
}

 

 

Con esto ya tenemos lo básico necesario para hacer funcionar nuestra secuencia de sprites. Teniendo esta base, ya podemos trabajar sobre ella y perfeccionarla para el lado que a nosotros más nos guste.

Si agregamos sobre la declaración de la clase  (sobre la parte que dice “public class MovieClip…”), un metadato que diga “[System.Serializable]” podemos hacer que, a pesar de no heredar de MonoBehaviours, Unity muestre sus propiedades y podamos modificarlas, al igual que crear todo desde el editor.

 

 

 

3) PERFECCIONANDO EL SCRIPT…

Vamos a darle ahora los retoques necesarios para redondear un poco más su funcionamiento. Como aclaro también en las clases que doy, esta es sólo una de las tantas formas que hay de realizar esto. Hay muchas más y cada programador le da su “toque” particular. Aclarado esto, sigamos entonces.

Vamos a crear una clase llamada “AnimationData“, que tampoco va a ser un MonoBehaviours, y va a contener los datos propios de cada animación que tengamos (no de cada frame, sino de cada “secuencia” de frames). Básicamente sería así:

 

 

using UnityEngine;
using System.Collections;

//Este script (sin ser un MonoBehaviour), va a guardar los datos de UNA animación.
[System.Serializable]
public class AnimationData
{
    //Nombre de la animación.
    public string name;

    //Fotograma en el cual comienza.
    public int start;

    //Fotograma en el cual termina.
    public int end;

    //FrameRate al cual se anima.
    public int frameRate;

    //¿Loopea?
    public bool isLoop;

    //Siguiente animación a la cual va (va a quedar en blanco si no debe ir a ninguna).
    public string nextAnimation;
}

 

 

Ahora… Cómo la usamos? Fácil… En nuestra clase MovieClip vamos a crear una variable llamada “currentAnimation” del tipo “AnimationData“. Pero también vamos a crear un array de “AnimationData“, ya que debemos guardar CADA UNA de todas las animaciones. Así que agregamos estas dos líneas:

 

 

//Lista de todas las animaciones.
public AnimationData[] animations;

//Animación actual.
public AnimationData currentAnimation;

 

 

Ahora quedaría agregar las animaciones al array. Configurarlas y utilizar cada una de las instancias para que podamos cambiar entra cada una de ellas. Para ello, dentro de nuestra clase “AnimationData” también podemos agregar, arriba de todo, “[System.Serializable]” para que podamos ver sus datos desde el editor de Unity cada uno de sus datos.

Imaginemos que metí dentro de un script “Hero” una variable llamada “mc” del tipo MovieClip. Se vería más o menos así:

 

tutorialunityspritesheet

 

Lo que vamos a hacer ahora para completar un poco más nuestro MovieClip es crearle 3 funciones básicas que no le pueden faltar:

 

 

//Para reproducir la animación.
public void Play()
{
    isPlaying = true;
}

//Para pausar la animación.
public void Pause()
{
    isPlaying = false;
}

//Para detener la animación y dejarla en el comienzo.
public void Stop()
{
    isPlaying = false;
    currentFrame = currentAnimation.start;
}

 

 

Acá usamos la variable “currentAnimation” que hicimos con lo que sería  nuestra animación actual. Lo que hacemos en la función “Stop()” es decirle que no se está reproduciendo y además que vaya al primer frame de la animación actual.

Vamos ahora a agregar también una función para ir a una animación específica, y la llamaremos “GotoAndPlay” (sí, sí… Como en Action Script). Así que nuestro código tendría ahora esto:

 

 

public void GotoAndPlay(string targetAnim)
{
    //¿La animación a la cual quiero ir es distinta a la cual estoy?
    //Esto es para que no vuelva al principio de la animación
    //Cada vez que ejecuto esta función.
    if (currentAnimation.name != targetAnim)
    {
        //Recorro todas las animaciones.
        for (int i = 0; i < animations.Length; i++)
        {
            //¿Esta animación se llama como la animación a la cual quiero ir?
            if (animations[i].name == targetAnim)
            {
                //Seteo la NUEVA animación actual.
                currentAnimation = animations[i];

                //Seteo el frame actual como el primero de la animación.
                currentFrame = currentAnimation.start;

                //PINTO el frame actual.
                renderer.sprite = frames[currentFrame];

                //Seteo el nuevo frameRate.
                frameRate = currentAnimation.frameRate;

                //Hago de nuevo el cálculo para actualizar cada cuánto tiempo se pinta cada frame.
                timeToChangeFrame = 1f / frameRate;

                //Inicio la reproducción.
                //Esto es porque si estaba en pausa, ni siquiera va a avanzar en esta animación.
                Play();

                //Detiene el bucle más próximo. Hacemos esto porque ya encontramos a la animación, llegado este punto.
                break;
            }
        }
    }
}

 

 

Ahora quedaría agregar nuestras funciones y nuestro “AnimationData” al “Update“. Lo que vamos a hacer es chequear si la animación actual llegó al final, en cuyo caso también tendríamos que preguntar si loopea o no. Si loopea pasarlo al principio de la animación actual, sino, preguntar si tiene alguna animación de destino. Si la tiene, ir a dicha animación, sino, detener la reproducción (este último caso es para cuando queremos que al llegar al final se quede ahí).

 

 

 Sí… Un quilombo bárbaro… Pero hey! Si estás en esta sección (Unity3D – Intermedio), te la tenés que bancar. ;).

Vamos a añadir entonces el bloque de código que hace lo que dijimos. Quedaría de la siguiente manera:

 

 

public void Update()
{
    //¿Se está reproduciendo?
    if (isPlaying)
    {
        //Sumamos la porción de segundo que corresponda a este frame.
        currentTimeToChangeFrame += Time.deltaTime;

        //¿Es el momento de cambiar de frame?
        if (currentTimeToChangeFrame >= timeToChangeFrame)
        {
            //Avanzamos un frame.
            currentFrame++;

            //¿Llegó al final de la animación actual?
            if (currentFrame >= currentAnimation.end)
            {
                //OPCIONAL: Con esto básicamente enviamos un MENSAJE para que si existe una función
                //llamada "OnEndAnimation" en el script que contiene a este, se llame.
                renderer.SendMessage("OnEndAnimation", SendMessageOptions.DontRequireReceiver);

                //¿Debe loopear?
                if (currentAnimation.isLoop)
                {
                    //... Lo volvemos al principio.
                    currentFrame = currentAnimation.start;
                }
                else
                {
                    //¿Hay una siguiente animación?
                    if (currentAnimation.nextAnimation != "")
                    {
                        //Cambiamos a dicha animación.
                        GotoAndPlay(currentAnimation.nextAnimation);
                    }
                    else
                    {
                        //Llegado acá significa que no tiene que loopear, ni tampoco hay otra animación a la cual ir.
                        Pause();
                    }
                }
            }

            //Seteo el frame que se va a mostrar.
            renderer.sprite = frames[currentFrame];

            //Lo vuelvo a cero para resetearlo.
            currentTimeToChangeFrame = 0;
        }
    }
}

 

 

Por último y con esto terminamos, hay que pulir un poco la función “LoadFrames“, ya que ahora tenemos más datos que setear. Nada de otro mundo, sólo vamos a asegurarnos de que nuestras variables tengan los valores iniciales que corresponden. Por ejemplo, la variable “currentAnimation“, luego de cargar todos los frames, debería tener como valor inicial la primera animación que encuentre. Y sabemos que vamos a tener al menos una, puesto que si no fuera así, no tendría sentido usar el script que estuvimos haciendo.

Entonces… Respiramos ondo y… Vamos con el último esfuerzo. Entonces… Nuestra función “LoadFrames” quedaría así:

 

 

public void LoadFrames(string url, SpriteRenderer renderer)
{
    //Asigna la variable LOCAL (parámetro), dentro de la variable GLOBAL.
    this.renderer = renderer;

    //Carga un array de sprites que están en Resources.
    frames = Resources.LoadAll<Sprite>(url);

    //Asigna el frame que esté en el índice 0 como Sprite.
    renderer.sprite = frames[0];

    //Setea que cambie cada frameRate parte de un segundo.
    timeToChangeFrame = 1f / frameRate;

    //Seteamos la primera animación como la actual.
    //No chequeamos si existe ya que si vamos a usar esta clase
    //se supone que tenemos al menos UNA animación.
    currentAnimation = animations[0];

    //El frame actual sería el primero de la ANIMACIÓN ACTUAL.
    currentFrame = currentAnimation.start;

    //Con esto le diríamos que empiece a reproducir la animación.
    Play();
}

 

 

Bueno… Y con esto terminamos. A mis alumnos, espero que les sirva como repaso. Al público en general, espero le haya servido para… Algo. Cualquier duda, pueden expresarla en los comentarios.

Adjunto los archivos:

[sdm_download id=”1009″ fancy=”1″ show_size=”1″ show_version=”1″ sdm-download-counter=”1″]

Saludos!