Animando nuestro sprite

En el tutorial pasado empezamos a ver lo básico de MonoGame y pudimos poner un sprite en pantalla, esta vez vamos a empezar a crear nuestro juego en sí y programar nuestro engine para que sea más sencillo dividir nuestro código.

Refactorizando nuestro código

Primero, en Visual Studio, vamos a crear una nueva carpeta llamada Engine , este será el core de nuestro juego y donde pondremos las clases generales.

Dentro de la carpeta Egine, vamos a crear tres clases RenderContext , GameObject2D  y GameSprite .

Render Context

En RenderContext tendremos nuestro propio graphics device, sprite batch y un game time. Abramos nuestra clase de RenderContext y agreguemos lo siguiente

La clase base

La clase base de nuestro juego es llamada GameObject2D . Lo único que hace es guardar la posición, escala y rotación de nuestro objeto y tiene una propiedad boleana que determina si el objeto debe ser dibujado. Este objeto solo tiene cuatro métodos: Initialize , LoadContent , Draw  y Update . Estos métodos están actualmente vacíos, pero los objetos que hereden de esta clase base van a implementarlas.

Abre la clase GameObject2D y sustituye lo que hay por lo siguiente

Construyendo nuestra clase GameSprite

Ahora es tiempo de construir nuestra clase sprite. Esta clase va a heredar de GameObject2D . En el tutorial pasado usamos el método Draw del spritebatch para dibujar nuestro sprite. Ahora que esta clase va a ser una clase común a nuestros elementos, vamos a usarlo mucho.

Como vamos a usar esta clase para dibujar texturas en la pantalla, necesitamos guardar el nombre de la textura que queremos usar y un objeto para cargarlo.

En nuestra clase vamos a necesitar propiedades que son usadas en el método Draw , por lo cual agregaremos todos estos argumentos.

Para finalizar, nuestra clase tendrá dos métodos LoadContent  y Draw . Usaremos LoadContent para cargar nuestras texturas y en el método Draw verificamos si el objeto se puede dibujar para ponerlo en pantalla.

Esta clase debería quedar así

Como te podrás dar cuenta, esta vez usamos un overload del método Draw que contiene muchos más parámetros que el que usamos anteriormente, esto es porque queremos un poco más de control en nuestro sprite además de solo darle la posición. En esta ocasión vamos a poder usar rotación, efectos, profundidad, etc.

Actualizando nuestro archivo Game1.cs

Ya hemos creado parte de nuestro engine, ahora lo que nos falta es actualizar nuestro archivo del juego principal para poder usar las clases que ya creamos.

Vamos a actualizar nuestro sprite y crear un Render Context

Ahora vamos a inicializar nuestro renderContext , igualmente actualizaremos a nuestro sprite ahora como GameSprite  y también vamos a darle una posición inicial.

También actualizaremos nuestro LoadContent  para usar nuestro renderContext  y llamar el método LoadContent  de nuestro sprite.

En el método Update  necesitamos pasarle el GameTime  a nuestro renderContext . También aquí mandamos a llamar el Update  de nuestro sprite.

Finalmente, en el método Draw  actualizamos nuestro sprite.

Si corres de nuevo el proyecto, deberías de ver lo mismo. Lo que hicimos fue prepararnos para cuando nuestro juego crezca en tamaño y estar mejor organizados.

Agregando movimiento a nuestro sprite

Por ahora tenemos una bonita nave estática, pero lo que quereos es que tenga movimiento así que vamos a ello. Lo mejor que podemos hacer es encapsular a nuestro jugador en su propia clase, vamos a crear una clase llamada Player2D . Esta clase va a ser responsable de cargar la textura, actualizar la posición y dibujar la textura.

Vamos a hacer que esta clase herede de GameObject2D . Podríamos hacer que heredara de GameSprite  pero no lo haremos, en vez de eso vamos a agregar un campo del tipo GameSprite . Esto es llamado composición.

Nuestra clase debe quedar como lo siguiente

El método Update  es donde hacemos la lógica para mover a nuestro sprite, sigue los siguientes pasos:

  1. Como solo calculamos la distancia que se ha movido el jugador desde la última actualización, debemos almacenar la posición actual del sprite en una variable temporal.
  2. Si el campo de dirección es 1 (lo que significa que nos estamos moviendo hacia la derecha) y la posición del héroe es más grande que el ancho de la pantalla menos el ancho del héroe (el héroe está tocando el borde derecho de la pantalla), luego, negaremos la dirección y estableceremos el efecto de sprite de nuestro sprite de héroe en FlipHorizontally .
  3. Si la dirección es menos uno y la posición del sprite es menor que cero, estableceremos la dirección en uno y dejaremos de aplicar un efecto de sprite.
  4. Luego agregaremos un delta a la posición x del héroe. Este delta representará el movimiento y se calcula multiplicando la velocidad, el tiempo transcurrido del juego y la dirección entre sí. Multiplicamos por el tiempo transcurrido del juego para asegurarnos de que nuestro héroe se mueve a la misma velocidad, independientemente de la velocidad de cuadros.
  5. Finalmente, establece la posición del sprite.

Actualizando nuestra clase Game1.cs

Ya que tenemos nuestra nueva clase, vamos a actualizar Game1.cs  para que se adecue a lo que tenemos.

Primero cambiamos el tipo de nuestro sprite

En el método Initialize  creamos nuestra instancia Player2D  y quitamos la posición

Ahora corramos el proyecto y verás cómo se mueve nuestro jugador

Generando animaciones

Ya es tiempo de generar la animación de nuestro sprite. Para eso vamos a generar el archivo xnb  usando el Pipeline  como lo hicimos en el tuturial anterior. Vamos a usar el archivo llamado Ship.png .

Luego de que tengamos nuestro sprite vamos a generar una nueva clase GameAnimatedSprite  que va a ser una extensión de GameSprite . La funcionalidad extra incluirá la animación del sprite.

Vamos a ir paso a paso para explicar un poco mejor lo que haremos

Campos

Por ahora necesitaremos 3 campos privados que nos ayudarán con las animaciones, currentFrame  nos indica la imagen de la animación actual que tenemos, totalFrames  es el número de cuadros que tenemos por spritesheet y timeUntilNextFrame  nos ayuda a calcular la velocidad de la animación

Propiedades

También necesitamos algunas propiedades, valores a los que necesitamos poder acceder desde fuera de la clase. Necesitamos saber cuántos fotogramas hay en la hoja de sprites, qué tamaño tiene cada fotograma, qué fotograma estamos mostrando actualmente y también si estamos reproduciendo una animación o si está en pausa. Estos valores pueden tener un setter privado porque no queremos que se modifiquen fuera de esta clase. También necesitamos el intervalo de frames (a qué velocidad va la animación) y un boleano que determina si debemos iniciar la animación.

Constructor

En nuestro constructor vamos a pasar los parámetros necesarios de nuestro spritesheet tales como el nombre del archivo, el número de filas y columnas, etc.

Play, Pause y Stop

Vamos a necesitar agregar un método para pausar, animar o parar las animaciones. Estos métodos son muy sencillos ya que solamente necesitamos poner algunos valores. Estas propiedades serán usados en el método update.

El método Update

En el método Update  calculamos qué frame es qué vamos a utilizar, igualmente nos ayudamos a la velocidad del frame dado que varias cantidades de cuadros va a dar distintas velocidades. Por eso en el constructor le pasamos el número de cuadros por segundo que va a necesitar para animarse bien.

El método Draw

En el método Draw  es donde realmente mostramos la animación, primero checamos si el sprite está animándose y no está en pausa, si es así entonces procedemos a dibujar el sprite. Dado que ahora es un spritesheet, necesitamos saber qué frame vamos a dibujar, calculamos el frame con la cantidad de fila y columna y posteriormente mandamos este cálculo a DrawRect  para que nuestra clase sepa qué recta dibujar.

Actualizando la clase GameSprite

Como en nuestro GameSpriteAnimation  necesitamos saber sobre la textura y ahora no podemos acceder a ella debido a que es privado, vamos a tener que modificar la clase GameSprite  para agregar una nueva propiedad

Con esto nos dejará de marcar error en el archivo anterior.

Actualizando nuestro Player2D

Finalmente nos falta actualizar nuestra clase Player2D  para que use animaciones.

Primero necesitamos el método Initialize  para que user nuestra nueva clase animada

Y también actualizamos el método Update  para que tome el ancho nuevo del sprite, ya que como cambiamos la imagen que utilizamos, solo queremos calcular el frame actual, este método debe quedar así

Ahora corremos nuestro juego y debemos ver la animación de nuestro sprite

Resumiendo

Esta vez si escribimos mucho código :). Hicimos una refactorización de nuestros archivos para que sea más fácil el mantenimiento y que nos ayude a manejar mejor los objetos. Creamos una animación a partir de un spritesheet y ahora podemos animar cualquier spritesheet que tengamos gracias a las clases que creamos.

En la siguiente lección moveremos a nuestro jugador usando el teclado y no automáticamente a como lo estamos haciendo.

Si quieres ver el código completo hasta esta parte, puedes verlo en mi Github