[LÖVE] Diseño gráfico y animaciones

Este capítulo lo vamos a centrar en dos partes, primeramente vamos a animar el personaje un poco para que parezca algo más orgánico y no solo un personaje estático, y luego aplicaremos una interfaz sencilla para nuestro juego. Para ello vamos a requerir ayuda de la biblioteca Anim8.

Para empezar con la animación he usado varios archivos del pack de plataformas, en concreto front.png y jump.png y walk_sheet.png. Las dos primeras imágenes está claro cómo usarlas, pero en el caso de la última, si os fijais, hay una sucesión de pequeñas imágenes que se corresponden a la animación final. Esto es lo que se llama hoja de sprites, y para poder usar esta imagen tenemos que dividirla en pequeñas imágenes, que cambiaremos rápidamente entre ellas para que a nuestros ojos parezca un movimiento real.

Resultado de imagen de sprite pacman

Ejemplo de hoja de sprites de Pack-Man

Podemos hacerlo a mano, ya que LÖVE soporta funciones para trocear sprites y cargarlos, pero en este caso vamos a facilitarnos las cosas mediante la biblioteca Anim8, que se encargará de todo esto de forma transparente. Para ello tenemos que importar la biblioteca en el archivo player.lua de la misma forma que en los capítulos anteriores. Luego de esto nos centraremos en el método init para proceder a cargar los archivos de imágenes con ayuda del módulo Resources.

-- Cargamos las imagenes del jugador
self.idle_img = Resources:loadResource("image", "player/front")
self.jump_img = Resources:loadResource("image", "player/jump")
self.walk_img = Resources:loadResource("image", "player/walk_sheet")

Una vez hecho esto tenemos que definir las divisiones con las que podremos usar la hoja de sprites. Para ello, Anim8 tiene una función llamada newGrid, que crea una rejilla que usará para trocear la hoja de sprites y nos permite crear una animación, y en donde los dos primeros parámetros son el tamaño de una celda y los dos siguientes el tamaño de la imagen.

-- Creamos una rejilla para la animación
local grid = Anim8.newGrid(72, 97, self.walk_img:getWidth(), self.walk_img:getHeight())

Con esto vamos a pasar a crear la animación. Para ello tenemos que indicarle que celdas forman la animación mediante la rejilla que hemos creado. Si miramos en la documentación, esto se puede hacer pasándole como texto las columnas que queremos usar y como número las filas al objeto creado. En mi caso solo me interesan las filas 1-3 y las columnas 1-3. Luego, se indica al método lo que va a tardar en cambiar de frame (en segundos). El resultado es el siguiente:

-- Creamos la animación
self.anim = Anim8.newAnimation(grid:getFrames('1-3', 1, '1-3', 2, '1-3', 3), 0.03)
-- Dejamos la animación pausada
self.anim:pause()

Ya tenemos creada una animación. Aún así, para que no se ejecute mientras no se muestre, os recomiendo pausarla con el método pause después de crear la animación.

Vale, ahora vamos a pasar al método update para hacer que la animación se actualice, pero antes de eso quiero que os fijeis en una cosa: si mirais la hoja de sprites veréis que el personaje camina solo hacia la derecha. Esto se hace para reducir el tamaño del archivo, ya que si os dais cuenta, caminar hacia la izquierda es la misma animación pero volteada. La biblioteca de anim8 nos permite realizar esto fácilmente con el método flipH.

Para ello vamos a mirar cuando nos estamos moviendo, que va a ser cuando actualicemos la animación. Si recordáis del capítulo pasado, esto lo podemos ver cuando la velocidad en el eje x es distinta a 0 o cuando el jugador no está saltando.

Luego, tenemos que comprobar cuando tenemos que voltear la animación, es decir, si estamos moviéndonos a la derecha (la velocidad en x es positiva) y la animación está volteada (para ver esto está la variable flippedH en el objeto animación), o nos estamos moviendo a la izquierda (la velocidad en x es negativa) y la animación no está volteada. Con esto volteamos la animación solo cuando necesitamos. Id al final del método update y copiar lo siguiente:

-- Actualizamos la animación si nos movemos
if self.v.x ~= 0 and not self.isJumping then
  -- Volteamos la animación en la dirección en la que andemos
  if (self.v.x < 0 and not self.anim.flippedH) or
     (self.v.x > 0 and self.anim.flippedH) then
    self.anim:flipH()
  end

Ahora tenemos que reproducir la animación si estaba pausada (se puede observar en la variable state) y actualizarla mediante el método update.

  -- Si estaba pausada la reanudamos
  if self.anim.status == "paused" then
    self.anim:resume()
  end

  self.anim:update(dt)

Solo nos queda reiniciar la animación si no nos estamos moviendo, para ello tenemos que saltar a la primera imagen de la animación con el método gotoFrame.

else
  -- Pausamos la animación si estamos quietos
  if self.anim.status == "playing" then
    self.anim:pause()
    self.anim:gotoFrame(1)
  end
end

Si hacemos un resumen, el código nos quedaría de la siguiente forma:

-- Actualizamos la animación si nos movemos
if self.v.x ~= 0 and not self.isJumping then
  -- Volteamos la animación en la dirección en la que andemos
  if (self.v.x < 0 and not self.anim.flippedH) or
     (self.v.x > 0 and self.anim.flippedH) then
    self.anim:flipH()
  end
  
  -- Si estaba pausada la reanudamos
  if self.anim.status == "paused" then
    self.anim:resume()
  end

  self.anim:update(dt)
else
  -- Pausamos la animación si estamos quietos
  if self.anim.status == "playing" then
    self.anim:pause()
    self.anim:gotoFrame(1)
  end
end

Ahora vamos a pasar al método draw. Este es el que más tenemos que cambiar, ya que ya no hay solo una imagen, si no que hay tres: la de salto, la animación y la estática. Vamos a empezar por la de salto. Para ello tenemos que comprobar que la velocidad en el eje y es distinta a 0 para poder dibujarla.

La cosa es que esta imagen, como pasa con la animación, solo está a la derecha, por lo que tenemos que tenemos que voltearla. Este volteo se puede hacer en tiempo de dibujado, ya que la función de dibujado de LÖVE permite añadir aparte de la imagen y la posición, la rotación, la escala en el eje x e y, y el punto 0, 0 de la imagen. Con esto, y sabiendo cuando nos movemos a la izquierda (la velocidad en x es negativa), tendremos que poner la escala en negativa en el eje x, que actuará como un flip, y, para que se muestre en la misma posición, desplazar el centro de la imagen la misma distancia que el tamaño de la imagen. El código quedaría así:

-- Comprobamos si estamos saltando
if self.v.y ~= 0 then
  local sx = 1 -- Tamaño en el eje X
  local ox = 0 -- Offset del eje X
  
  -- Comprobamos si hay que voltear la imagen
  if self.v.x < 0 then
    sx = -1
    ox = self.jump_img:getWidth()
  end
  
  love.graphics.draw(self.jump_img, self.x, self.y, 0, sx, 1, ox, 0)

Ahora vamos a imprimir la animación cuando nos estamos moviendo solo, es decir, cuando no estamos saltando y la velocidad en x es distinta a 0. Para ello tenemos que usar el método draw de la animación creada antes, el cual usa los mismos parámetros que la función love.graphics.draw.

-- Comprobamos si estamos andando
elseif self.v.x ~= 0 then
  self.anim:draw(self.walk_img, self.x, self.y)

Con esto solo falta mostrar la imagen de estar quieto en el momento en el que no saltamos ni nos movemos.

-- Momento en el que estamos quietos
else
  love.graphics.draw(self.idle_img, self.x, self.y)
end

Con esto ya estaría animado. Podéis probarlo para ver cómo se anima al ir a la derecha o a la izquierda y como se voltea al saltar.

Pero esto no queda aquí, vamos a pasar a crear una interfaz gráfica sencilla que se encargará de mostrar los puntos recogidos y las vidas que nos quedan. Yo escogí la fuente Handy Andy y el corazón de LÖVE para mostrar estos elementos, por lo que os dejaré un enlace abajo para que lo podais descargar si no quereis buscar algo parecido.

Aprovechando que estamos en el jugador, vamos a añadirle dos variables, por lo que id al método init y añadid al final lo siguiente:

self.life = 3 -- Vida actual del jugador
self.points = 0 -- Puntos actuales del jugador

Con esto tendremos registrados la vida y los puntos del jugador. Ahora vamos a pasar al initialscene.lua para cargar/crear los elementos de la interfaz, que en nuestro caso será la imagen del corazón y un recuadro de texto donde añadamos los puntos actuales. Para ello tenemos que añadir el método init encima de enter con el siguiente texto:

-- Carga la escena al empezar
init = function (self)
  -- Cargamos la imagen de corazón
  self.heart = Resources:loadResource("image", "heart")
  -- Cargamos la fuente
  local font = Resources:loadResource("font", "handy-andy.otf", 32)
  -- Creamos un recuadro de texto con la fuente cargada
  self.points = love.graphics.newText(font, "")
end

Ahora tenemos que poner los puntos a 0 cada vez que entremos en esta escena. Para ello el recuadro de texto tiene el método set, el cual recibe como parámetro el nuevo texto:

-- Ponemos los puntos a 0
self.points:set("0")

Con esto, para que los puntos se actualicen tenemos que usar este método en la función update, solo que le pasaremos los puntos del jugador pero transformados en una variable de texto. Podeis añadir esto al final del método:

-- Actualizamos los puntos
self.points:set(tostring(self.player.points))

Ahora nos falta dibujar en pantalla la interfaz, que se hace después de dibujar la cámara en el método draw. Yo lo que voy a hacer es dibujar cuatro corazones en la esquina izquierda, que serán los corazones máximos del jugador, en donde estarán rojos los corazones que si tiene el jugador y negro transparente los que le falta. Para ello tenemos que “tintar” la imagen, y esto se hace mediante la función love.graphics.setColor, que recibe un color en formato RGBA con valores entre 0 y 1. Yo para ello he usado un bucle for de la siguiente forma:

-- Dibujamos cuatro corazones
for i = 1, 4 do
  -- Ponemos a negro los corazones que no tenemos
  if self.player.life < i then
    love.graphics.setColor(0, 0, 0, .25)
  -- Ponemos en rojo los corazones que tenemos
  else
    love.graphics.setColor(1, 0, 0, 1)
  end

  -- Dibujamos un corazón
  love.graphics.draw(self.heart, 20+(i-1)*36, 20)
end

Ahora solo me falta dibujar los puntos, los cuales estarán en negro pero en la otra esquina. Para que estén alineados a la derecha lo que voy a hacer es quitarle el ancho del recuadro de texto a la posición x al dibujarlo, quedando así:

-- Imprimimos los puntos
love.graphics.setColor(0, 0, 0, 1)
love.graphics.draw(self.points, 680-self.points:getWidth(), 20)

-- Reiniciamos los colores
love.graphics.setColor(1, 1, 1, 1)

Con esto nuestro juego ha ganado puntos en cuanto al diseño del mismo con una interfaz básica y unas animaciones sencillas.

animaciones

Descargas

 

Anuncios

Un pensamiento en “[LÖVE] Diseño gráfico y animaciones

  1. Pingback: Aprende a programar un videojuego con LÖVE: desde lo básico a lo avanzado | El blog de NEKERAFA

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 )

Google+ photo

Estás comentando usando tu cuenta de Google+. 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 )

Conectando a %s