[LÖVE] Movimiento del personaje

En este tutorial nos centraremos en cómo mover y hacer saltar al personaje que hemos creado en el capítulo anterior, así como gestionar la cámara para que esté centrada en todo momento sobre el personaje. Para ello vamos a plantear un escenario de prueba con varios obstáculos y zonas donde saltar, así que vamos a ello.

Antes de nada, decir que voy a usar la imagen del suelo (ground.png) y de bloque (block.png) del paquete de plataformas, por lo que voy a cambiar un poco la distribución de la carpeta de images. He creado la carpeta “player” que contiene front.png y ground.png y block.png los he metido en la carpeta “platforms”, quedando de la siguiente forma:

- resources/
    - images/
        - platforms/
            - block.png
            - ground.png
        - player/
            - front.png

Vamos a empezar creando el suelo y varios bloques flotantes. Como he pensado que van a ser elementos estáticos que yo iré definiendo donde me parezca mejor, he pensado en crear una clase que aglutina todo. Para ello cree el archivo “platform.lua” en la carpeta “main/entities”.

-- Importamos el módulo de clases
local Class = require "libs.hump.class"
-- Importamos el gestor de recursos
local Resources = require "main.systems.resources"
-- Importamos la clase entidad
local Entity = require "main.entities.entity"

-- Definimos la clase Plataforma
local Platform = Class {
  __includes = Entity, -- Definimos la plataforma como subclase de Entidad
  
  -- Función que se llama al crear una plataforma
  init = function (self, x, y, platform_type)
    self.image = Resources:loadResource("image", "platforms/" .. platform_type) -- Cargamos la imagen de la plataforma
    
    -- Llamamos al constructor del padre para iniciar la plataforma
    Entity.init(self, x, y, self.image:getWidth(), self.image:getHeight())
  end,
  
  -- Dibujamos la plataforma
  draw = function (self)
    love.graphics.draw(self.image, self.x, self.y)
  end
}

return Platform

No hay mucho que explicar si hemos seguido el capítulo anterior. Como mucho en el método init, el tipo de la plataforma indicará la imagen que se tiene que cargar, y como está en una subcarpeta no queda más remedio que añadirle eso delante del tipo (en Lua el operador .. es el de concatenar strings, es decir, pegar).

Pasemos ahora al archivo initialstage.lua, ya que vamos a añadirle estas plataformas al cargar el nivel. Para ello vamos a modificar el método enter un poco. Yo lo he puesto así:

enter = function (self)
  -- Iniciamos un nuevo mundo
  self.world = Bump.newWorld(35)
  
  -- Iniciamos el gestor de entidades
  Entities:switch(self.world)

  -- Creamos los cuadrados del suelo
  for i = 0, 19 do
    Entities:add(Platform(i*70, 500, "ground"))
  end
    
  -- Creamos algunas plataformas flotantes
  local platform_1 = Platform(560, 290, "block")
  local platform_2 = Platform(630, 290, "block")
  local platform_3 = Platform(700, 290, "block")
  local platform_4 = Platform(980, 150, "block")
  local platform_5 = Platform(1050, 150, "block")
  local platform_6 = Platform(140, 150, "block")
  Entities:add(platform_1, platform_2, platform_3, platform_4, platform_5, platform_6)
    
  -- Creamos al jugador y lo añadimos al gestor de entidades
  self.player = Player(350, 400)
  Entities:add(self.player)
end

De aquí comentar que como quiero que el suelo cubra toda la escena, he metido un bucle for que me genera todo el suelo, por lo que como las plataformas son de 70 x 70, multiplico el valor de la i del for por 70. Para las plataformas flotantes las he ido añadiendo yo a mano indicando los puntos donde están, por lo que tengo que añadir luego todas las plataformas a mano al gestor de entidades.

Bien, si en este momento ejecutamos, nos deberíamos de encontrar que la pantalla ha cambiado mucho, pero nuestro jugador sigue sin moverse. Para lograr esto, primero vamos a crear lo que se llama gestor de controles, que en nuestro caso será el módulo Baton. Este módulo se encarga de observar las teclas que pulsamos en el teclado o en un mando. El gestor de controles también nos permitirá cambiar las configuraciones en tiempo de ejecución, por lo que no tendremos que reiniciar el juego. Crea el archivo input.lua en la carpeta “main/systems” y pasemos a configurar el gestor de controles.

-- El gestor de controles se encarga de definir las entradas que usaremos en
-- nuestro videojuego, independientemente de si usamos teclado o mando. También
-- hay casos donde nos permite cambiar teclas y reasignarlas en tiempo real.

-- Importamos el módulo de control de entradas
local Baton = require "libs.baton"

-- Devolvemos una instancia inicializada del módulo de control de entradas
return Baton.new {
  controls = {
    left = {'key:a'},
    right = {'key:d'},
    up = {'key:w'}
  }
}

Bien, aquí comentar que el módulo Baton nos permite definir para un control que teclas o botones lo activan. Yo solo estoy usando el teclado, pero se podría añadir también un mando de esta forma:

    left = {'key:a', 'button:dpleft'},
    right = {'key:d', 'button:dpright'},
    up = {'key:w', 'button:dpup'}

En la página del módulo tenéis más información de cómo funciona. Ahora vamos a pasar a integrarlo en el jugador, para ello tendremos que hacer unas modificaciones en el archivo player.lua en la carpeta “main/entities”. Primero de todo importamos el gestor de la siguiente forma:

-- Importamos el gestor de controles
local Input = require "main.systems.input"

Ahora vamos a añadir unas variables en la clase del jugador para controlar la gravedad y la velocidad del mismo:

-- Definimos la clase de Jugador
local Player = Class {
  __includes = Entity, -- Definimos el jugador como subclase de Entidad
 
  gravity = 1030.05, -- píxeles por segundos al cuadrado
  maxSpeed = 350, -- Velocidad máxima que alcanza al jugador (medida en píxeles por segundo)

La gravedad actuará cuando nuestro personaje salte o se caiga de una plataforma, y la he definido como la gravedad terrestre (9,81 m/s2) x 105 píxeles/m, es decir, 1 metro en la realidad serán 105 píxeles del juego. En el caso de la velocidad máxima será la velocidad a la que nuestro jugador se mueva.

init = function (self, x, y)
  self.image = Resources:loadResource("image", "player/front") -- Cargamos la imagen del jugador
  
  -- Llamamos al constructor del padre para iniciar al jugador
  Entity.init(self, x, y, self.image:getWidth(), self.image:getHeight())
  
  -- Iniciamos valores del jugador
  self.v = {x = 0, y = 0} -- Velocidad del jugador
  self.isJumping = false -- Miramos si está saltando o no
end

En el caso del init he cambiado el recurso, ya que como ya os comenté, lo he movido a la carpeta player. También he creado la variable v, la cual guardará las velocidades que toma nuestro jugador en los ejes x e y. Por último he creado una variable isJumping, la cual estará a true si está saltando o false si no.

Vale, llegamos al método fundamental para lo que queremos conseguir. En update aplicaremos todo el tema de gravedad y del movimientos del personaje, así que vayamos por partes:

update = function (self, dt)
  -- Aplicamos la gravedad a la velocidad
  self.v.y = self.v.y + self.gravity * dt
  • En la primera línea lo que estoy haciendo es aplicar la gravedad sobre la velocidad del eje y. Esto lo hago porque el personaje tiene que estar todo el rato “cayendo” hacia al suelo. Puede parecer raro, pero con esto conseguimos que baje si se ha caido de una plataforma. La gravedad está multiplicada por la variable dt, que es el tiempo en segundos desde la última vez que LÖVE llamó a la función update. Con esto conseguimos que nuestro personaje se mueva de la misma forma en todos los ordenadores, independiende de la velocidad a la que se ejecute el programa.
  -- Comprobamos si hay que movernos izquierda o derecha
  if Input:down("left") then
    self.v.x = -self.maxSpeed
  elseif Input:down("right") then
    self.v.x = self.maxSpeed
  -- Nos quedamos quietos
  else
    self.v.x = 0
  end
  • En las líneas siguientes aplico el gestor de controles para saber si el usuario ha pulsado alguna dirección y mover al personaje. Para ello basta con cambiar la velocidad en el eje x.
  -- Comprobamos si hay que saltar
  if Input:pressed("up") and not self.isJumping then
    self.v.y = -self.maxSpeed*2
    self.isJumping = true
  end
  • La siguiente línea lo que hago es cambiar la velocidad en el eje y una sola vez para que así realice un salto. Si no comprobase si está saltando, el personaje flotaría hacia arriba indefinidamente. También, si la velocidad en el eje y fuera negativa, el personaje “saltaría” hacia abajo. Esto es debido a que el sistema de coordenadas en el eje y es inverso en LÖVE, es decir, hacia bajo es positivo y hacia arriba es negativo. Teneis en la wiki de LÖVE un ejemplo gráfico de esto. Luego, para darle una fuerza mayor al salto he multiplicado por dos la velocidad máxima.
  -- Movimiento esperado si no colisionamos con nada
  local goalX = self.x + self.v.x * dt
  local goalY = self.y + self.v.y * dt
  
  -- Movemos el personaje y comprobamos si ha colisionado con algún objeto
  self.x, self.y, collisions = self.world:move(self, goalX, goalY)
  
  -- Recorremos todas las colisiones
  for i, collider in ipairs(collisions) do
    -- Comprobamos que hemos tocado un suelo y dejamos de saltar
    if collider.normal.y < 0 then
      self.isJumping = false
    end
  end
  • A continuación procedo a intentar actualizar la posición x e y del jugador comprobando antes si no hay ningún objeto que colisiones conmigo. Para ello tenemos que llamar al método move del mundo, que pasando la entidad y hacia dónde nos queremos desplazar, nos devuelve la posición donde nos hemos quedado con el choque y la lista de objetos con los que hemos colisionado. Con esta lista miro a ver si la normal en el eje y de la colisión en negativa, es decir, si hemos chocado con el objeto desde arriba y no desde abajo. Esto me sirve para saber cuando ha terminado un salto, es decir, cuando me he apoyado en el suelo de una plataforma.
  -- Si hemos modificado la posición y es porque estamos chocando con el suelo
  if self.y ~= goalY then
    self.v.y = 0
  end
end
  • Por último hago una comprobación de si hemos llegado a la posición y final, ya que si no fuera el caso es que hay un obstáculo que nos impide el camino. Con esto se cuando tengo que dejar la velocidad en el eje y en 0, ya que si hay un obstáculo en ese eje, esta velocidad se reinicia.

El código de la clase jugador debería de quedar de la siguiente manera:

-- Importamos el módulo de clases
local Class = require "libs.hump.class"
-- Importamos el gestor de recursos
local Resources = require "main.systems.resources"
-- Importamos el gestor de controles
local Input = require "main.systems.input"
-- Importamos la clase entidad
local Entity = require "main.entities.entity"

-- Definimos la clase de Jugador
local Player = Class {
  __includes = Entity, -- Definimos el jugador como subclase de Entidad
  
  gravity = 1030.05, -- píxeles por segundos al cuadrado
  maxSpeed = 350, -- Velocidad máxima que alcanza al jugador (medida en píxeles por segundo)
  
  -- Función que se llama al crear un jugador
  init = function (self, x, y)
    self.image = Resources:callLoader("image", "player/front") -- Cargamos la imagen del jugador
    
    -- Llamamos al constructor del padre para iniciar al jugador
    Entity.init(self, x, y, self.image:getWidth(), self.image:getHeight())
    
    -- Iniciamos valores del jugador
    self.v = {x = 0, y = 0} -- Velocidad del jugador
    self.isJumping = true -- Miramos si está saltando o no
  end,
  
  -- Actualizamos al jugador
  update = function (self, dt)
    -- Aplicamos la gravedad a la velocidad
    self.v.y = self.v.y + self.gravity * dt
    
    -- Comprobamos si hay que movernos izquierda o derecha
    if Input:down("left") then
      self.v.x = -self.maxSpeed
    elseif Input:down("right") then
      self.v.x = self.maxSpeed
    -- Nos quedamos quietos
    else
      self.v.x = 0
    end
    
    -- Comprobamos si hay que saltar
    if Input:pressed("up") and not self.isJumping then
      self.v.y = -self.maxSpeed*2
      self.isJumping = true
    end
    
    -- Movimiento esperado si no colisionamos con nada
    local goalX = self.x + self.v.x * dt
    local goalY = self.y + self.v.y * dt
    
    -- Movemos el personaje y comprobamos si ha colisionado con algún objeto
    self.x, self.y, collisions = self.world:move(self, goalX, goalY)
    
    -- Recorremos todas las colisiones
    for i, collider in ipairs(collisions) do
      -- Comprobamos que hemos tocado un suelo y dejamos de saltar
      if collider.normal.y < 0 then
        self.isJumping = false
      end
    end
    
    -- Si hemos modificado la posición y es porque estamos chocando con el suelo
    if self.y ~= goalY then
      self.v.y = 0
    end
  end,

  -- Dibujamos al jugador
  draw = function (self)
    love.graphics.draw(self.image, self.x, self.y)
  end
}

return Player

Bien, con esto ya casi hemos terminado. Si probais a ejecutar el proyecto veréis que nuestro personaje se comporta tal y como queremos: se mueve a los lados, realiza saltos y las plataformas actúan como obstáculos. El principal problema es que si se sale de la pantalla, la pantalla no se mueve para mostrarlo. Para arreglarlo vamos a implementar una cámara que va a seguir al jugador siempre. Abre el archivo initialstage.lua de nuevo y añade unas cuantas líneas más:

-- Importamos el módulo de colisiones
local Bump = require 'libs.bump'
-- Importamos el módulo de cámara
local Camera = require "libs.hump.camera"
...

-- Definimos la escena inicial
local InitialScene = {
  world = nil, -- Guarda el mundo actual
  player = nil, -- Guarda el jugador
  camera = nil, -- Guarda la cámara
  
  -- Carga la escena
  enter = function (self)
    ...

    -- Creamos la cámara y la centramos en el jugador
    self.camera = Camera.new()
  end,
  • Primeramente necesitamos importar el módulo de cámara de HUMP para que todo funcione. Con ello podemos crear una cámara dentro de la clase InitialStage.
  -- Actualizamos la escena
  update = function (self, dt)
    -- Actualizamos todas las entidades
    Entities:update(dt)

    -- Comprobamos que el jugador no se sale de la escena
    local playerWidth = self.player.image:getWidth()
    self.player.x = math.min(math.max(self.player.x, 0), 1400-playerWidth)

    -- Comprobamos que la cámara se mantiene estática en los bordes de la escena
    local playerX = math.min(math.max(self.player.x, 350), 1050)
    local playerY = math.min(self.player.y, 280)
    self.camera:lookAt(playerX, playerY)
  end
  • Luego pasamos al método update, en donde vamos a realizar varias cosas. Primeramente vamos a delimitar la posición del jugador a la escena que hemos creado. En mi caso, mi escena va en el eje x desde la posición 0 a la 1400 (un poco menos ya que la imagen del personaje no tiene que salirse de pantalla). Para indicar entre qué valores puede ir esta posición, he preferido usar las funciones math.max (que devuelve el valor más alto de todos y con el que uso para indicarle la cota más baja) y math.min (que devuelve el valor más bajo de todos y con el que uso para indicarle la cota más alta). Con esto garantizo que si x es menor a 0, math.max me devolverá la cota mínima, si x es mayor a 1400, math.min me devolverá la cota máxima y si está entre estos valores no pasará nada.Para calcular la posición en donde se tiene que centrar la cámara hago un tratamiento parecido, ya que no quiero que se mueva cuando el jugador está cerca de los bordes de la escena o cuando está sobre el suelo. Para ello he aplicado el mismos concepto que en el caso anterior, solo que en el eje x va a estar limitada entre la mitad del tamaño de la ventana y 1400 menos la mitad del tamaño de la ventana. Yo como estoy trabajando con una ventana de 700 de ancho, la cámara se desplazará entre 350 y 1050. En el caso del eje y hago lo mismo, solo que no limito la distancia mínima, ya que quiero que el jugador pueda escalar hacia arriba todo lo que quiera.Cámara.png
    Todos estos cálculos se los pasamos al método lookAt de la cámara para situar en su posición. Existen más métodos con los que realizar más cosas, como el método lockWindow, que permite hacer que la cámara se fije en un elemento definiendo una ventana por la que puede moverse, si el elemento sale de esa ventana, la cámara se mueve (es muy útil si es un juego con perspectiva cenital). También si sabemos cuánto nos hemos movido tenemos el método move, que le pasamos a la cámara un delta. Por último, podemos hacer rotaciones y aumentar el enfoque con los métodos rotate y zoom. Todo esto lo podéis encontrar en la documentación del módulo camera, aunque yo os recomiendo jugar con las funciones para ver cómo funcionan.
  -- Dibujamos la escena
  draw = function (self)
    -- Dibuja el fondo
    love.graphics.clear(0.816, 0.957, 0.969)
    
    -- Activamos la cámara
    self.camera:attach()
    -- Dibujamos las entidades
    Entities:draw()
    -- Desactivamos la cámara
    self.camera:detach()
    
    -- Imprimimos información del jugador
    info = string.format("Posición: %i, %i, Velocidad: %.2f %.2f\nEl jugador" ..
      "está saltando?: %s", self.player.x, self.player.y, self.player.v.x, 
      self.player.v.y, tostring(self.player.isJumping))
    love.graphics.print({{0, 0, 0}, info}, 10, 10)
  end
  • Nos queda solo el método draw, en donde tenemos que usar el método attach y detach de la cámara para aplicar los cambios visuales. Entre medias tenemos que poner todos los métodos draw de los elementos que se ven afectados por la cámara (en este caso las entidades). Como extra he decidido imprimir en la pantalla de juego información sobre el jugador para saber si está realizando las operaciones correctamente, pero esto es opcional.

Y con esto ya tendríamos todo listo. Si ejecutamos podemos ver como nuestro personaje salta y anda por el escenario, y la cámara lo sigue por todas partes. Solo nos falta añadir algo, y es animar a nuestro personaje, pero eso lo veremos en el próximo capítulo.

movimiento

Descargas

Anuncios

Un pensamiento en “[LÖVE] Movimiento del personaje

  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