[Wave Engine] Animando a Mai

 ·  ☕ 6 min  ·  ✍️ eiximenis

    Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

    Buenas! Este post es el segundo sobre WaveEngine, esta maravilla que han parido los chicos de Plain Concepts 🙂

    En el post anterior, vimos los fundamentos de Wave Engine y terminamos con un programa que mostraba a Mai Shiranui en la esquina inferior izquierda de la pantalla. Pero Mai Shiranui gana mucho cuando se mueve (¿por qué será?) así que vamos a ver como podemos hacerlo para que nuestra bella protagoniste se anime.

    Aunque a tenor del post anterior pueda parecer que tan solo hemos mostrado una imagen por pantalla, desde el punto de vista de Wave hicimos mucho más: definimos una entidad y le asociamos entre otros el componente de sprite.

    Hoy vamos a añadir un componente nuevo, el de animación. Y es que, obviamente, en Wave los sprites pueden estar animados.

    Sprites sheets

    En Wave una animación consta de varios “estados” (varias animaciones realmente) y cada estado consta de varias imágenes. Así podemos tener una animación (realmente un componente de tipo Animation2D) que defina los estados “Idle” (cuando el personaje no hace nada) y  “walk” (cuando el personaje camina). Cada uno de esos estados consta de varios gráficos que conformarán cada de esos estados.

    En este post la animación tendrá un solo estado (que llamaremos “idle”) y que constará de 12 imágnes (12 pngs que se llaman mai_idle (0).png hasta mai_idle (11).png).

    Por supuesto podríamos usar la herramienta Wave Exporter para generar 12 .wpks (es decir uno por cada png tal y como hicimos en el post anterior) pero es mucho más eficiente usar un sprite sheet. Los que vengáis del mundo web probablemente conocereis este concepto ya que se usa mucho en CSS. La razón es que es mucho más eficiente bajarse una sola imagen grande y mostrar (por CSS) solo la parte que nos interesa que descargarse varias imágenes pequeñas. Pues en videojuegos ocurre lo mismo: es mucho mejor tener una sola textura (recordad que las imágenes son realmente texturas) grande y mostrar tan solo una parte que tener varias texturas pequeñas.

    El primer paso consiste en generar una sola imagen (un png) que combine todas las imágenes que formaran parte de mi sprite sheet. Ni se te ocurra hacerlo a mano (cortando y pegando), ya que por suerte hay herramientas para ello. En los tutoriales de Wave hay una explicación muy buena de como hacerlo paso a paso utilizando Texture Packer. Para no repetirme yo haré un ejemplo utilizando otra herramienta, llamada Shoebox. La verdad es que Texture Packer es más completa (pero hay funcionalidades de pago), pero usar Shoebox me va a permitir contaros una cosilla más sobre Wave 🙂

    Lo primero es generar el .png que contenga los 12 pngs, esto con Shoebox es muy sencillo. Arranco Shoebox, selecciono los 12 pngs desde el explorador de archivos y los arrastro sobre el icono “Sprite Sheet” de la ventana de Shoebox. Hecho esto me aparece una ventana con el sprite sheet:

    image

    Con el botón se settings puedo modificar varias propiedades, como p. ej. si la textura debe ser cuadrada, debe tener un tamaño que sea potencia de dos (eso es deseable en según que motores gráficos por temas de rendimiento) y también el formato del fichero XML que nos generará:

    image

    Sí, he dicho formato XML, porque tener un png grande con todos los pngs pequeños incrustados no sirve de nada si no tenemos manera de saber donde empieza y termina cada “sub-imagen”. ShoeBox nos genera un fichero XML con toda esa información (Texture Packer también, por supuesto).

    En mi caso el contenido del fichero XML es el siguiente (en el formato por defecto que genera Shoebox):

    <TextureAtlas imagePath="sheet.png">

        <SubTexture name="mai_idle (0).png" x="" y="189" width="77" height="93"/>

        <SubTexture name="mai_idle (1).png" x="" y="" width="77" height="92"/>

        <SubTexture name="mai_idle (2).png" x="" y="94" width="77" height="93"/>

        <SubTexture name="mai_idle (3).png" x="157" y="98" width="76" height="95"/>

        <SubTexture name="mai_idle (4).png" x="157" y="" width="76" height="96"/>

        <SubTexture name="mai_idle (5).png" x="235" y="192" width="76" height="95"/>

        <SubTexture name="mai_idle (6).png" x="235" y="" width="76" height="93"/>

        <SubTexture name="mai_idle (7).png" x="79" y="" width="76" height="92"/>

        <SubTexture name="mai_idle (8).png" x="79" y="192" width="76" height="93"/>

        <SubTexture name="mai_idle (9).png" x="157" y="195" width="76" height="95"/>

        <SubTexture name="mai_idle (10).png" x="79" y="94" width="76" height="96"/>

        <SubTexture name="mai_idle (11).png" x="235" y="95" width="76" height="95"/>

    TextureAtlas>

    Básicamente se incluye el nombre del png original y su posición y tamaño dentro del png “global”.

    Ahora usamos Assets Exporter para convertir el png grande a un asset en formato .wpk e incluímos dicho asset y el .xml en el proyecto de VS2012:

    image

    Y con eso ya tendremos suficiente. Los 12 pngs originales no los necesitamos para nada.

    Animaciones 2D en WaveEngine

    Ahora el siguiente paso es modificar la definición de entidad para añadirle un componente nuevo de tipo Animation2D. Este componente, a pesar de su nombre, puede contener un conjunto de animaciones. De momento nosotros tendremos tan solo una (Mai en estado idle), pero podríamos tener varias animaciones distintas (andar, saltar, etc) usando un solo sprite sheet.

    El código de definición de la entidad es como sigue:

    var mai = new Entity("Mai").

        AddComponent(new Transform2D()

        {

            X = 50,

            Y = WaveServices.Platform.ScreenHeight 46,

            Origin = new Vector2(0.5f, 1)

        }).

        AddComponent(new Sprite("Content/mai.wpk")).

        AddComponent(Animation2D.Create<ShoeBoxXmlSpriteSheetLoader>("Content/mai.xml").

            Add("Idle", new SpriteSheetAnimationSequence() {

                First = 1,

                Length = 12,

                FramesPerSecond = 9

            })).

        AddComponent(new AnimatedSpriteRenderer(DefaultLayers.Alpha));

    Las dos diferencias respecto el código del post anterior (además del uso de la fluent interface) son:

    1. Que se añade el componente Animation2D
    2. El componente SpriteRenderer es modificado por el AnimatedSpriteRenderer

    Centrémonos en el primer punto: llamamos al método estático Animation2D.Create para crear una animación. A dicho método le tenemos que pasar un parámetro genérico que es una clase que va a ser la encargada de indicar donde, dentro del asset gráfico, está cada frame de la animación. En mi caso uso la clase ShoeBoxXmlSpriteSheetLoader. Esta clase me la he creado yo y lo que básicamente hace es leer el fichero XML generado por Shoebox y devolver un array de objetos Rectangle que indican la posición y tamaño de cada frame de la animación. El código es como sigue:

    class ShoeBoxXmlSpriteSheetLoader : ISpriteSheetLoader

    {

        public Rectangle[] Parse(string path)

        {

            var doc = XDocument.Load(path);

            var descs = doc.Descendants(XName.Get("SubTex
    ture"
    )).

                Select(node => new Rectangle

                {

                    X = int.Parse(node.Attribute("x").Value),

                    Y = int.Parse(node.Attribute("y").Value),

                    Width = int.Parse(node.Attribute("width").Value),

                    Height = int.Parse(node.Attribute("height").Value)

                }).ToArray();

     

            return descs;

        }

    }

    Una vez tenemos el Animation2D creado debemos añadirle todas las animaciones que realmente contiene (recuerda que puede contener varias). En este caso tan solo contiene una, y por ello tenemos tan solo una llamada al método Add. A cada animación le damos un nombre, y luego un objeto SpriteSheetAnimationSecuence que determina cual es el frame inicial (dentro del Animation2D), cual es el final y cuantos frames deben renderizarse por segundo. En mi caso le coloco 9, y así toda la animación (que consta de 12 frames) tardará 1,3 segundos en realizarse lo que me pareció un valor razonable.

    Finalmente el resto del código de la escena es parecido al del post anterior:

    EntityManager.Add(mai);

    mai.FindComponentOfType<Animation2D>().Play(true);

    Añadimos la entidad a la escena y empezamos a reproducir la animación. El método Play reproduce la animación actual dentro del componente Animation2D (en nuestro caso solo hay una, si hay más de una se puede usar la propiedad CurrentAnimation del propio componente). El parámetro booleano que vale true es para indicar que la animación se vaya repitiendo en un bucle.

    ¡Y listos! Con esto ya tenemos a Mai en todo su esplendor: http://screencast.com/t/eUwEVcSVn6xg

    Let’s fight!

    Espero que os haya resultado interesante…

    Un saludo!!!

    Si quieres, puedes invitarme a un café xD

    eiximenis
    ESCRITO POR
    eiximenis
    Compulsive Developer