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

Introducció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:

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:

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:

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”:

Ademá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”:

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:

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.

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!

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 en el siguiente enlace.

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!

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

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *