Comunidad orientada al desarrollo de videojuegos

Juego de Plataformas (Animaciones con hojas de sprites)

Preparados, Listos…

  • El conjunto de assets los proporcionamos nosotros. Te los puedes descargar aquí.
  • Para seguir los pasos de este ejemplo, deberemos tener conocimientos básicos de programación en C#. Se puede encontrar mucha documentación sobre C# por internet.
  • El copyright del arte de Braid pertenece a sus dueños originales. Sprites cogidos de Cyrus Annihilator y fondo cogido de la web de David Hellman´s. Nos encanta este juego y queríamos hacer un pequeño homenaje con este ejemplo.

Objetivos

Al final de esta guía tendremos un pequeño juego en el que podremos mover un personaje por un suelo, ejecutándose diferentes animaciones mientras estas parado y corriendo. Aprenderemos como generar hojas de sprites con TexturePacker, y ejecutar esto dentro de Wave Engine.

Generando la hoja de sprites

Si habéis jugado previamente a Braid, sabréis que su personaje Tim tiene unas magníficas animaciones. Esto es posible por la cantidad de frames que tiene la hoja de sprites y por los detalles de cada uno.

Para este ejemplo usaremos las animaciones de correr y de estar parado. Estas dos animaciones están compuestas por múltiples frames: 27 y 22 respectivamente. Por ejemplo, el primer frame que se ejecuta de la animación de correr es el siguiente:

first-frame-tim-running

mientras que para el de parado es este:

first-frame-tim-idle

Como podemos apreciar, dentro del paquete de texturas, tenemos 27+22=49 imágenes diferentes. Cada una de estas imágenes se consume directamente desde el juego, con el objetivo de mejorar el rendimiento, todas estas imágenes son “empaquetadas” dentro de una sola imagen. la hoja de sprites, junto a un archivo XML que indica básicamente donde se encuentra el frame dentro de la hoja.

La hoja de sprites puede ser generada por vuestro programa favorito de edicición de imagen, pero este proceso puede ser tedioso, pero afortunadamente existen programas que hacen esta tarea automáticamente. Aquí usaremos TexturePacker, el cual podemos descargar aquí.

Lo primero será abrir TexturePacker y arrastrar todo el conjunto de frames a la pestaña de la parte derecha del programa, la cual dibujará una hoja de sprites inicial por defecto en el centro de la pantalla.

texturepacker-screen-shot

En el panel de TextureSettings, en la parte izquierda, lo primero es cambiar, dentro de la sección Output, la pestaña Data Format a Generic XML.

texturepacker-texturesettings-screen-shot

Como podemos apreciar, los frames de ambas animaciones están mezclados dentro del resultado, y algunos frames están rotados 90º. Nosotros no queremos eso, Entonces le diremos al programa que no rote las imágenes, desmarcamos la casilla Allow rotation de la sección Layout, y cambiamos la pestaña Algorithm a Basic.

texturepacker-layout-screen-shot

En el resultado podremos ver algo similar a esto:

texturepacker-screen-shot-2-unmodified

Finalmente, solo falta especificar el formato de datos y de textura del archivo dentro de Output y pulsamos el botón Publish.

texturepacker-publish-screen-shot

La salida de la hoja de sprites quedaría algo parecido a esto:

timspritesheet

mientras que el XML puede empezar de una manera similar a esta:

<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="TimSpriteSheet.png" width="1024" height="1024">
    <sprite n="slice01_01.png" x="2" y="2" w="81" h="137"/>
    <sprite n="slice02_02.png" x="85" y="2" w="81" h="137"/>
    <sprite n="slice03_03.png" x="168" y="2" w="81" h="137"/>
    […]

Exportando los assets

A parte de la hoja de sprites de Tim, haremos uso de dos imágenes más como fondo. en verdad, la razón de usar dos imágenes como fondo es porque queremos dibujar el suelo por encima de todo, para que Tim este detrás de algunas rocas que aparecen en la imagen del suelo.

background

Teniendo los tres assets en mente(TimSpriteSheet.png, Sky.png y Floot.png), simplemente los exportamos al formato de Wave Engine siguiendo, por ejemplo, el ejemplo del dinosaurio en la parte de exportación de modelos y texturas.

Creando la lógica del juego. Ejecutando animaciones

Una vez que tengamos un nuevo proyecto creado en Visual Studio con todos los assets exportados previamente añadidos a la carpeta Content. (Puedes seguir el proceso de crear un proyecto e incluir assets en el ejemplo del dinosaurio).

Empezaremos añadiendo el cielo y el suelo a la escena. Todo dentro del método MyScene.CreateScene(). Como ambos son simple imágenes estáticas, el mismo código se aplica para ambas, excepto la referencia que tiene cada una de ellas.

var sky = new Entity("Sky")
    .AddComponent(new Sprite("Content/Sky.wpk"))
    .AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
    .AddComponent(new Transform2D()
    {
        Origin = new Vector2(0.5f, 1),
        X = WaveServices.Platform.ScreenWidth / 2,
        Y = WaveServices.Platform.ScreenHeight
    });
var floor = new Entity("Floor")
    .AddComponent(new Sprite("Content/Floor.wpk"))
    .AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
    .AddComponent(new Transform2D()
    {
        Origin = new Vector2(0.5f, 1),
        X = WaveServices.Platform.ScreenWidth / 2,
        Y = WaveServices.Platform.ScreenHeight
    });

Tenemos en cuenta que ambas entidades están situadas en la parte inferior central, con sus orígenes ahí, esto permite que siempre estarán centrados los fondos independientemente del tamaño de la pantalla. Con otras palabras, siempre veremos el fondo centrado. De momento no añadimos estas entidades a la escena para después explicar como queremos que se dibujen las tres imágenes, para dar el efecto del personaje detrás de las piedras.

La siguiente y última entidad que añadiremos es Tim. La diferencia con las otras entidades es que introduciremos el componente Animation2D, el cual se encarga de todo lo relacionado con la animación de la hoja de sprites. Además, Animation2D tiene su propio AnimatedSpriteRenderer. El código sería el siguiente:

var tim = new Entity("Tim")
    .AddComponent(new Transform2D()
    {
        X = WaveServices.Platform.ScreenWidth / 2,
        Y = WaveServices.Platform.ScreenHeight - 46,
        Origin = new Vector2(0.5f, 1)
    })
    .AddComponent(new Sprite("Content/TimSpriteSheet.wpk"))
    .AddComponent(Animation2D.Create<TexturePackerGenericXml>("Content/TimSpriteSheet.xml")
        .Add("Idle", new SpriteSheetAnimationSequence() { First = 1, Length = 22, FramesPerSecond = 11 })
        .Add("Running", new SpriteSheetAnimationSequence() { First = 23, Length = 27, FramesPerSecond = 27 }))
    .AddComponent(new AnimatedSpriteRenderer(DefaultLayers.Alpha)));

Aquí el origen también es el mismo que el del fondo, pero la posición de la se desplaza un poco al revés  para los pies de Tim encajen perfectamente con el final del suelo.

A parte de eso,  el resto se aplica a la animación. Lo primero es tener en cuenta que, debido a que vamos a cargar un XML con cada ubicación de frame, Animation2D nos proporciona un método estático el cual crea una instancia de si mismo basada en la estrategia especifica a través de lo genérico. Dicha estrategia es básicamente la manera en la que se interpreta el XML. En verdad, esto puede diferir de una estructura XML: puede ser un TXT por ejemplo, un archivo CSV, o incluso un formato binario. No importa, siempre que se proporcione una estrategia ISpriteSheetLoader, y se pueden crear otros nuevos por la demanda!

Como Animation2D.Create() devuelve una nueva instancia, podemos coger esta ventaja para directamente llamar a Add() para suministrar las diferentes secuencias de animaciones que la hoja de sprites contiene. Esto es fácil: Add() recibe el nombre el cual usaremos más tarde para referirnos a la animación, y un SpriteSheetAnimationSequence el cual, a través de propiedades publicas, nos permite configurar el primer frame de la animación dentro del XML (1-index based), el tamaño de esta animación y los frames por segundo.

Hemos terminado con respecto a la creación de entidades de nuestro juego. Ahora toca dibujarlos. Solo tenemos que añadir las líneas del EntityManager, con este orden:

EntityManager.Add(floor);
EntityManager.Add(tim);
EntityManager.Add(sky);

Si ejecutamos nuestro juego, ahora podemos ver todo posicionado correctamente, pero algo estático.

game-screen-shot-unmodified

Ahora añadiremos alguna animaciones. Lo primero será hacer que Tim respire, con la animación de “idle” o parado, con la que nada más empezar empezará a tener vida.Esto no requiere ningún input, ni ningún estado especifico o similar, se puede hacer directamente con el componente Animation2D añadido a la entidad de Tim, y llamando al método Play(). Tened en cuenta que no estamos especificando la animación que se va a ejecutar, por lo tanto se ejecutara por defecto la primera que añadimos.

var anim2D = tim.FindComponent<Animation2D>();
anim2D.Play(true);

El método Play() tiene varias sobrecargas. La que hemos usado simplemente dice que la animación se ejecutara de forma cíclica o en loop. Si ejecutamos veremos a Tim respirar una y otra vez.

El último paso es añadir un movimiento horizontal, para poder ejecutar la animación de correr. Este behavior se encargará de controlar los inputs de teclado y en consecuencia moverá a Tim con sus correspondientes animaciones. Probablemente os preguntareis como Tim corre a derecha y a izquierda si solo existe los frames mirando hacia un lado. Actualmente no hace falta tener duplicados estos frames a derecha y a izquierda, con Transform2D.Effect podemos cambiar el sentido de la imagen. Simplemente tenemos que cambiar el valor de SpriteEffects.FlipHorizontally y Tim se pondrá mirando al lado contrario.

Vamos a crear una nueva clase y la llamamos TimBehavior. Ponemos el código siguiente sobrescribiendo el método Update():

protected override void Update(TimeSpan gameTime)
{
    currentState = AnimState.Idle;

    // touch panel
    var touches = WaveServices.Input.TouchPanelState;
    if (touches.Count > 0)
    {
        var firstTouch = touches[0];
        if (firstTouch.Position.X > WaveServices.Platform.ScreenWidth / 2)
        {
            currentState = AnimState.Right;
        }
        else
        {
            currentState = AnimState.Left;
        }
    }

    // Keyboard
    var keyboard = WaveServices.Input.KeyboardState;
    if (keyboard.Right == ButtonState.Pressed)
    {
        currentState = AnimState.Right;
    }
    else if (keyboard.Left == ButtonState.Pressed)
    {
        currentState = AnimState.Left;
    }

    // Set current animation if that one is diferent
    if (currentState != lastState)
    {
        switch (currentState)
        {
            case AnimState.Idle:
                anim2D.CurrentAnimation = "Idle";
                anim2D.Play(true);
                direction = NONE;
                break;
            case AnimState.Right:
                anim2D.CurrentAnimation = "Running";
                trans2D.Effect = SpriteEffects.None;
                anim2D.Play(true);
                direction = RIGHT;
                break;
            case AnimState.Left:
                anim2D.CurrentAnimation = "Running";
                trans2D.Effect = SpriteEffects.FlipHorizontally;
                anim2D.Play(true);
                direction = LEFT;
                break;
        }
    }

    lastState = currentState;

    // Move sprite
    trans2D.X += direction * SPEED * (gameTime.Milliseconds / 10);

    // Check borders
    if (trans2D.X < BORDER_OFFSET)
    {
        trans2D.X = BORDER_OFFSET;
    }
    else if (trans2D.X > WaveServices.Platform.ScreenWidth - BORDER_OFFSET)
    {
        trans2D.X = WaveServices.Platform.ScreenWidth - BORDER_OFFSET;
    }
}

Esta parte puede ser separada en cuatro piezas lógicas: Los inputs de teclado, los inputs táctiles, la maquina de estados de las animaciones y el movimiento del sprite.

La pieza de inputs táctiles simplemente escucha el primer input, comprobando si ha sido en la parte derecha o izquierda de la pantalla, y actualiza la maquina de estado como corresponde. Esto particularmente se usa para los inputs de ratón, y si utilizas un dispositivo móvil para los inputs táctiles:

// touch panel
var touches = WaveServices.Input.TouchPanelState;
if (touches.Count > 0)
{
    var firstTouch = touches[0];
    if (firstTouch.Position.X > WaveServices.Platform.ScreenWidth / 2)
    {
        currentState = AnimState.Right;
    }
    else
    {
        currentState = AnimState.Left;
    }
}

De la misma forma actúa la parte del teclado, lo único que entra en juego son las teclas de dirección derecha e izquierda, nada más:

// Keyboard
var keyboard = WaveServices.Input.KeyboardState;
if (keyboard.Right == ButtonState.Pressed)
{
    currentState = AnimState.Right;
}
else if (keyboard.Left == ButtonState.Pressed)
{
    currentState = AnimState.Left;
}

La máquina de estados de las animaciones es cuando se lleva a cabo los cambios según la variable currenState. Dependiendo de este valor la maquina de estados cambia y por lo tanto se ejecuta una animación u otra.

/ Set current animation if that one is diferent
if (currentState != lastState)
{
    switch (currentState)
    {
        case AnimState.Idle:
            anim2D.CurrentAnimation = "Idle";
            anim2D.Play(true);
            direction = NONE;
            break;
        case AnimState.Right:
            anim2D.CurrentAnimation = "Running";
            trans2D.Effect = SpriteEffects.None;
            anim2D.Play(true);
            direction = RIGHT;
            break;
        case AnimState.Left:
            anim2D.CurrentAnimation = "Running";
            trans2D.Effect = SpriteEffects.FlipHorizontally;
            anim2D.Play(true);
            direction = LEFT;
            break;
    }
}

lastState = currentState;

Y finalmente, el movimiento del sprite solo actúa en el eje de las X, con Transform2D.X:

// Move sprite
trans2D.X += direction * SPEED * (gameTime.Milliseconds / 10);

// Check borders
if (trans2D.X < BORDER_OFFSET)
{
    trans2D.X = BORDER_OFFSET;
}
else if (trans2D.X > WaveServices.Platform.ScreenWidth - BORDER_OFFSET)
{
    trans2D.X = WaveServices.Platform.ScreenWidth - BORDER_OFFSET;
}

Aunque con esto el comportamiento está hecho, Tim no se moverá si no le añadimos como componente en su entidad este comportamiento. Lo añadimos en el cuerpo de CreateScene() donde declaramos la entidad de Tim:

.AddComponent(new TimBehavior())

Ahora tenemos un mini juego de plataformas con un personaje que se mueve en horizontal, con sus animaciones correspondientes basadas en una hoja de sprites.

game-screen-shot-2-unmodified

DESCARGAR RECURSOS

Puedes descargar el proyecto completo aquí.

FUENTES

Tutorial original en inglés: http://blog.waveengine.net

Para descargar Wave Engine: http://www.waveengine.net

Traducido por Carlos Sánchez López

, , , , , , ,

2 thoughts on “Juego de Plataformas (Animaciones con hojas de sprites)

Leave a Reply