[Windows Phone] Creando juegos con WaveEngine. 2º Parte.

WaveEngine LogoIntroducción

En el artículo anterior realizamos una introducción a WaveEngine con el objetivo de analizar el arranque y la funcionalidad básica del engine. Para ello, nos basamos en un clásico como “Super Mario Bross”.

NOTA: “Super Mario Bross” es una marca registrada por Nintendo. Este artículo es un simple homenaje a la saga.

Tras conocer el engine, la gestion de recursos y lograr plasmar el fondo, suelo y al personaje principal, el objetivo de esta entrada es continuar desde el punto anterior y evolucionar el ejemplo hasta conseguir poder mover al personaje (mediante un gamepad táctil) junto al sistema de animaciones.

¿Te apuntas?

Añadiendo comportamientos

El aspecto de nuestro videojuego hasta ahora es el siguiente:

Mario en escena!

Mario en escena!

Interesante pero… ¿Mario sin correr?. Tenemos que resolver esto. Veamos como añadir animaciones a Mario.

Una animación consta de estados. Cada estado a su vez consta de varias imágenes. Tendremos dos estados:

  • Idle: Cuando Mario este parado.
  • Walk: Cuando Mario camina o corre.

Ya aprendimos a utilizar la herramienta Wave Exporter para generar los archivos .gmk de los que se nutre el juego. Podríamos generar un gmk para cada una de las imágenes de cada animación aunque a la larga, penalizaría el rendimiento.

¿Qué hacemos?

La solución se llamda SpriteSheet. Un spritesheet no es más que una imagen que contiene muchas imágenes. Se utiliza en el juego dividiendo y obteniendo cada una de las imágenes que contiene pero todas se obtienen del mismo fichero.

Nosotros en nuestro ejemplo vamos a utilizar el siguiente conjunto de imágenes:

Imágenes

Imágenes

Asi pues, primer objetivo, crear el spritesheet. Podríamos hacerlo a mano con los distintos editores fotográficos conocidos aunque la tarea es bastante tediosa.

Vamos a utilizar la herramienta TexturePacker que puedes descargar desde aquí. Abrimos la herramienta y arrastramos las imágenes anteriores:

TexturePacker01

En el lateral izquierdo tenemos el panel de configuración. El primer cambio importante que realizaremos será elegir el formato de salida elegido. Elegiremos “Generic XML”:

TexturePacker02Además del spritesheet en formato PNG obtendremos un archivo en XML que indicará el tamaño y la posición de cada una de las imágenes que componen al spritesheet. Será importante en nuestro juego.

Finalmente, en la parte superior pulsamos el botón “Publish”:

TexturePacker03

Como resultado además del spritesheet obtenemos el XML:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with TexturePacker http://texturepacker.com-->
<!-- $TexturePacker:SmartUpdate:9253cd2deaa49622a1344586a860f75a$ -->
<!--Format:
n  => name of the sprite
x  => sprite x pos in texture
y  => sprite y pos in texture
w  => sprite width (may be trimmed)
h  => sprite height (may be trimmed)
oX => sprite's x-corner offset (only available if trimmed)
oY => sprite's y-corner offset (only available if trimmed)
oW => sprite's original width (only available if trimmed)
oH => sprite's original height (only available if trimmed)
r => 'y' only set if sprite is rotated
-->
<TextureAtlas imagePath="Mario.png" width="64" height="128">
     <sprite n="0.png" x="2" y="2" w="17" h="27"/>
     <sprite n="1.png" x="21" y="2" w="17" h="28"/>
     <sprite n="2.png" x="40" y="2" w="15" h="28"/>
     <sprite n="3.png" x="2" y="32" w="17" h="28"/>
     <sprite n="4.png" x="21" y="32" w="15" h="28"/>
     <sprite n="5.png" x="38" y="32" w="17" h="28"/>
     <sprite n="6.png" x="2" y="62" w="17" h="27"/>
</TextureAtlas>

Como podéis observar en el archivo XML, contiene la posición de cada una de las imágenes junto a su tamaño. Todo listo para comenzar!

Vamos a añadir el spritesheet y el XML en el proyecto pero … espera!. No directamente, recuerda que tenemos que utilizar la herramienta Wave Exporter para generar el archivo .gmk:

Recursos Mario

Nos centramos en la entidad Mario. El principal cambio con respecto a lo que teníamos hasta ahora será la introducción del componente de tipo Animation2D. El componente Animation2D nos permite definir desde una hasta un conjunto de animaciones:

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

Nosotros necesitamos por ahora al menos dos conjuntos de animaciones (Mario en estado Idle y Mario en estado Running). Todo ello recordar utilizando el mismo sprite sheet.

Antes de continuar, analicemos el código anterior. Para crear la animación utilizamos el método estático Animation2D.Create que recibe un parámetro genérico (generalmente una clase, en nuestro ejemplo TexturePackerGenericXml) donde se definirá cada sprite perteneciente al spritesheet utilizado en la animación.  Una vez creado el Animation2D definimos todas las animaciones que contendrá. En nuestro caso definimos las dos necesarias (Idle i Running).

En cada animación definida establecemos un nombre junto a un objeto de tipo SpriteSheetAnimationSecuence que definirá el frame inicial de la animación, el frame final y el número de frames a renderizar por segundo.

Continuamos. Antes de avanzar, es importante que no se nos pase añadir la entidad a escena:

EntityManager.Add(mario);

Bien, si ejecutamos en este momento… ops!, no hay cambios. Efectivamente, hemos añadido a Mario animaciones para cuando este parado y para cuando corra pero no tenemos forma de cambiar entre los estados, es decir, aun no somos capaces de hacer correr a Mario.

Pongamos fin a este problema. Vamos a crear un behavior, en nuestro ejemplo llamado MarioBehavior, que nos permitirá desplazar a Mario horizontalmente. Este Behavior capturará las pulsaciones  en la pantalla táctil para permitir modificar el estado de Mario y asi permitir el desplazamiento:

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;
          }
     }

     // 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;
}

En la parte superior tenéis el código implementado en el método Update. En el método se realizan varias tareas fundamentales:

  • Captura las pulsaciones en la pantalla (izquierda y derecha).
  • Modifica el estado de la animación a la adecuada (dependiendo del valor de currentState).
  • Se mueve el sprite a la posición correspondiente.
  • Se verifica que el sprite este dentro de los márgenes.

Llegados a este punto lo tenemos todo casi preparado para ver correr por fin a Mario pero… espera!. Debemos modificar la instancia de Mario para añadirle el Behavior:

AddComponent(new MarioBehavior())

Ahora si!. Si ejecutamos y pulsamos en el lateral izquierdo Mario correrá hacia la izquierda hasta el márgen izquierdo como máximo y exactamente igual en sentido contrario. Genial, ¿cierto?. Aunque… llevamos toda la vida jugando con Mario con una cruceta, ¿como lo solucionamos?.

Bueno, ningun dispositivo Windows Phone cuenta con botones físicos específicos para juegos. Sin embargo, tenemos una maravillosa pantalla táctil con todas las posibilidades que ofrece.

¿Que tal si implementamos una cruceta virtual?.

Manos a la obra. Lo primero que debemos hacer es… tener unos gráficos de nuestra cruceta para mostrar en pantalla. Asi que tenemos que crear nuestro wpk correspondiente. Una vez añadido al proyecto, tenemos que instanciarlo:

var joystick = new Entity("joystick")
.AddComponent(new Sprite("Content/Joystick.wpk"))
.AddComponent(new SpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new Transform2D()
{
     X = joystickOffset,
     Y = WaveServices.Platform.ScreenHeight - joystickOffset - 300
});

De esta forma lograremosque la cruceta aparezca en la zona inferior izquierda de la pantalla para molestar lo mínimo posible al jugador. La añadimos a escena:

EntityManager.Add(joystick);

De momento, si pulsamos cualquier dirección de la cruceta, no hace nada… ¿no echáis en falta algo?. Tenemos que añadir comportamiento a la cruceta digital asi que debemos crear un nuevo Behavior. Creamos un Behavior llamado JoyStickBehavior:

protected override void Update(TimeSpan gameTime)
{
     this.UpPressed = this.DownPressed = this.LeftPressed = this.RightPressed = false;

     var touchState = WaveServices.Input.TouchPanelState;

     if (touchState.Count > 0)
     {
          var touch = touchState[0];

          // Up/down check
          if (touch.Position.X > (trans2D.X + 100) && touch.Position.X < (trans2D.X + 200))
          {
               // Here we're inside the vertical area defined by up/down buttons, let's check which one of those
               if (touch.Position.Y > trans2D.Y && touch.Position.Y < (trans2D.Y + 100))
               {
                    // Up
                    this.UpPressed = true;
               }
               else if (touch.Position.Y > (trans2D.Y + 200) && touch.Position.Y < (trans2D.Y + 300))
               {
                    // Down
                    this.DownPressed = true;
               }
          }
          // Left/right check
          else if (touch.Position.Y > (trans2D.Y + 100) && touch.Position.Y < (trans2D.Y + 200))
          {
               // Here we're inside the horizontal area defined by left/right buttons, let's check which one of those
               if (touch.Position.X > trans2D.X && touch.Position.X < (trans2D.X + 100))
               {
                    // Left
                    this.LeftPressed = true;
               }
               else if (touch.Position.X > (trans2D.X + 200) && touch.Position.X < (trans2D.X + 300))
               {
                    // Right
                   this.RightPressed = true;
               }
          }
     }
}

Basicamente detectamos si  el usuario toca en la cruceta. Modificamos el Behavior MarioBehavior para añadir una instancia del Behavior Joystick y detectar si tenemos que cambiar a Mario de estado según el estado del Joystick:

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

     // touch panel
     if (joystick.RightPressed)
     {
          currentState = AnimState.Right;
     }

     if(joystick.LeftPressed)
     {
          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;
}

Añadimos a la instancia Mario el Behavior del Joystick:

AddComponent(new MarioBehavior(EntityManager.Find("joystick")))

La instancia Mario por lo tanto quedaría:

var mario = new Entity("Mario")
.AddComponent(new Transform2D()
{
     X = WaveServices.Platform.ScreenWidth / 2,
     Y = WaveServices.Platform.ScreenHeight - 46,
     Origin = new Vector2(0.5f, 1)
})
.AddComponent(new Sprite("Content/Mario.wpk"))
.AddComponent(Animation2D.Create<TexturePackerGenericXml>("Content/Mario.xml")
.Add("Idle", new SpriteSheetAnimationSequence() { First = 3, Length = 1, FramesPerSecond = 11 })
.Add("Running", new SpriteSheetAnimationSequence() { First = 0, Length = 3, FramesPerSecond = 27 }))
.AddComponent(new AnimatedSpriteRenderer(DefaultLayers.Alpha))
.AddComponent(new MarioBehavior(EntityManager.Find("joystick")));

Si ejecutamos ahora nuestro juego podemos probar que efectivamente pulsando en la cruceta el lateral izquierdo Mario corre a la izquierda y exactamente lo mismo en dirección opuesta ocure si pulsamos la derecha.

Convirtiendo el proyecto a Windows Phone

Pero… espera…el proyecto es una aplicación Windows de escritorio, ¿no ibamos a desarrollar el juego para Windows Phone?

Esto… efectivamente. Vamos a descubrir a continuación una de las grandes características que ofrece el engine que no es otra que poder convertir nuestro proyecto a las siguientes plataformas:

  • Windows Phone 7/8. Por ahora es un proyecto Windows Phone 7. Por compatibilidad binaria funciona sin problemas en Windows Phone 8.
  • IOS (iPhone, iPad & iPod Touch),
  • Windows 8 (Aplicación Windows Store).
  • Android.
WAVE ENGINE Converter Tool

WAVE ENGINE Converter Tool

NOTA: La herramienta la podéis encontrar dentro de una carpeta llamada “Tools” en el directorio de instalación.

¿Cómo la utilizamos?

Muy fácil. Pulsamos el botón Open para seleccionar la solución (archivo .sln) a convertir. Marcamos las casillas de las plataformas que deseamos como destino y pulsamos el boton Convert.

Una vez que termina la conversión si nos dirigimos al directorio de la solción tendremos una nueva solución mas su carpeta asociada por cada casilla marcada (plataforma destino).

Listo!.

¿Ya?. Casi. Las soluciones estan practicamente listas a falta de los recursos (nuestro archivos wpk). Podemos reutilizar los ya utilizados o podemos crear nuevos archivos de recursos adaptados a las nuevas plataformas.

NOTA: Recordar establecer el valor Content en la propiedad Build Action.

Tras añadir los recursos, podemos ejecutar el proyecto convertido en el emulador de Windows Phone o en un dispositivo físico donde podremos probar nuestro juego!

Nuestro juego en Windows Phone!

Nuestro juego en Windows Phone!

En nuestro caso hemos convertido la solución a un proyecto Windows Phone. Le hemos añadido y a continuación tenéis disponible el resultado:

Podéis ver a continuación un video del juego en funcionamiento:

Espero que lo visto en la entrada os haya resultado interesante. Recordar que cualquier duda o sugerencia la podéis dejar en los comentarios de la entrada.

Extra

Es sumamente recomendable que utilicéis la herramienta Sample Browser. Es una herramienta extra de los chicos de Wave Engine que nos permite ver y descargar una gran cantidad de ejemplos realizados con el motor. Una fuente sin duda excelente de aprendizaje. Todo lo visto en esta entrada esta perfectamente cubierto en los ejemplos. Muy recomendado!

Sample Browser

Sample Browser

Conclusiones

Estamos ante un engine joven pero lo suficientemente sólido como para desarrollar juegos de peso. Es gratuito, nos permite desarrollar bajo C# y además podemos exportar nuestros juegos a multitud de plataformas. Sin duda, gran trabajo el realizado hasta ahora por el equipo de Wave Engine con actualizaciones periódicas constantes afinando poco a poco el engine.

Keep Pushing!

Más información

5 pensamientos en “[Windows Phone] Creando juegos con WaveEngine. 2º Parte.

  1. Hola, antes de nada muchas gracias por el artículo.
    Estuve echándole un ojo y probando las animaciones pero al crear el SpriteSheet con el TextureParker me rota algunos gráficos, y en el xml lo refleja con el atributo r=”y”, pero
    desde el código no veo como volverlos a su orientación normal.

    ¿Como lo hiciste?
    En tu imagen del SpriteSheet aparencen los marios rotados, pero en el xml que muestras no aparece el atributo ‘r’.

    Un saludo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s