[LÖVE] Programemos un plataformas en 2D: Creando nuestra primera escena

Aún nos quedan unos conceptos importantes por aprender, pero los iremos viendo mientras programemos nuestro juego. En este capítulo nos vamos a centrar en programar una escena lo más simple posible de forma visual, pero ya algo compleja para tocar todos los puntos que nos interesan.

Antes de nada quiero decir que para este tutorial voy a usar como imágenes un pack de plataformas (https://opengameart.org/content/platformer-tiles) que os podéis descargar desde OpenGameArt.org. En esta página podéis encontraros muchísimos recursos, todos de libre distribución. Si no sabéis mucho de diseño os puede salvar de un apuro, pero eso sí, recordad siempre nombrar al autor.

Empecemos por lo más básico de todo, la piedra sobre la que girará todo. Si nos paramos a pensar un momento, una de las más importantes en un videojuego son las entidades, que son casi todos los elementos del juego que están en pantalla. Con algunos se pueden interactuar (ya sea el personaje principal o un personaje secundario), otros están de decoración (una flor que se mueve en el suelo del nivel) y otros sirven de muro o de suelo y están completamente estáticos.

Si pensamos en cómo funciona una entidad, la mejor forma de representarlos en código es mediante un objeto. En Lua (el lenguaje que usa LÖVE) se puede crear objetos mediante prototipos, pero creo que nos va a ser más cómodo usar clases al principio, por lo que partiremos del módulo Class de la biblioteca HUMP. La idea es crear una clase padre Entity, y luego hijos que hagan cosas más complejas, como Player, Enemy, Ground, Platform, etc, etc. Si representamos la jerarquía, quedaría de la siguiente forma:

Entidades

Para modelar esto vamos a realizar nuestro primer script en Lua. Crearemos un archivo “entities.lua” en la carpeta “main/entities” y lo abriremos con ZeroBrane para añadir el siguiente código:

-- Las entidades representan objetos que se pueden dibujar en pantalla

-- Cargamos el módulo de clases
local Class = require "libs.hump.class"

-- Definimos la clase entidad
local Entity = Class {
  -- Método que se llama al crear una entidad
  init = function (self, x, y, width, height)
    -- Inicializamos las variables internas
    self.world = nil
    self.x = x
    self.y = y
    self.w = width
    self.h = height
  end,
  
  -- Método que se llama cuando se quiera destruir la entidad
  destroy = function (self)
    -- No hacemos nada
    return
  end,  
  
  -- Método que devuelve el rectángulo que ocupa la entidad
  getRect = function (self)
    -- En Lua podemos devolver varios parámetros como una lista
    return self.x, self.y, self.w, self.h
  end,
  
  -- Método que actualiza las variables internas de la entidad
  update = function (self, dt)
    -- No hacemos nada
    return
  end,
  
  -- Método que dibuja la entidad
  draw = function (self)
    -- No hacemos nada
    return
  end
}

-- Esta línea es importante, hay que devolver siempre la clase creada
return Entity

A modo de resumen, lo que he hecho es crear una clase Entidad con el módulo Class de HUMP. Para crear un objeto, Class usa un método llamado init para iniciar los atributos del nuevo objeto. Yo he usado este método para guardar la información de la posición y el tamaño de la entidad.

A continuación he definido el método destroy, que lo que hace es quitar la entidad del mundo en el que se encuentra. El siguiente método, getRect, lo usaremos para las colisiones. Lo que hace es devolver una lista con la posición y el tamaño de la entidad. Por último, los métodos update y draw se llaman en cada frame y permiten mover la entidad o dibujarla. Yo de momento he dejado que no hagan nada para que las subclases los implementen.

Una vez tenemos la plantilla para las entidades, vamos a crear lo que se llama el gestor de entidades, que se encargará de tener todas las entidades juntas para poder actualizarlas o mostrarlas. Esto nos va a ahorrar código en las escenas ya que no tendremos porqué crear bucles para actualizar cada entidad de cada escena. Cómo lo vamos a tratar como un gestor, tendremos una instancia única que todas las escenas compartirán. Para ello usaremos el prototipado de objetos en vez de el módulo class, ya que tampoco supondrá una diferencia. Crea un nuevo archivo en “main/systems” y llámalo “entities.lua” añadiéndole el siguiente código:

-- El gestor de entidades se encarga de actualizar todas las entidades y de
-- dibujarlas en nuestro mundo si hiciera falta. Hay veces en las que incluso
-- se encarga de comprobar colisiones.

-- Definimos la clase del gestor de entidades
local Entities = {
  -- Lista de entidades
  entities = {},
  world = nil,

  -- Inicia el gestor
  switch = function (self, world)
    self:clear()
    self.world = world
  end,
  
  -- Añade una o varias entidades
  add = function (self, ...)
    -- Con este for recorremos la lista de elementos pasados por parámetro
    for i, newEntity in ipairs({...}) do
      table.insert(self.entities, newEntity)
      -- Incluimos la entidad en el mundo
      self.world:add(newEntity, newEntity:getRect())
      newEntity.world = self.world
    end
  end,
  
  -- Elimina una o varias entidades
  remove = function (self, ...)
    -- Realizamos algo parecido a add
    for i, delEntity in ipairs({...}) do
      -- Recorremos la lista para encontrar la entidad a borrar
      for j, entity in ipairs(self.entities) do
        if entity == delEntity then
          -- Eliminamos la entidad del mundo
          self.world:remove(entity)
          -- Destruimos la entidad
          entity:destroy()
          -- Eliminamos la entidad de la lista
          table.remove(self.entities, j)
          break
        end
      end
    end
  end,
  
  -- Limpia la lista de entidades
  clear = function (self)
    for i, entity in ipairs(self.entities) do
      -- Eliminamos la entidad del mundo
      self.world:remove(entity)
      -- Destruimos la entidad
      entity:destroy()
    end
    self.entities = {}
  end,
  
  -- Actualizamos todas las entidades
  update = function (self, dt)
    for i, entity in ipairs(self.entities) do
      entity:update(dt)
    end
  end,
  
  -- Mostramos todas las entidades
  draw = function (self)
    for i, entity in ipairs(self.entities) do
      entity:draw()
    end
  end
}

return Entities

No hay mucho que destacar, el método switch inicia el gestor con un mundo fijo, add permite añadir una o varias entidades al gestor, que se encarga también de añadirlas al mundo, remove permite quitar una o varias entidades del gestor, quitándolas también del mundo y destruyéndolas, clear limpia la lista de entidades, update se encarga de actualizar todas las entidades, y draw de mostrarlas.

Vamos a proceder a hacer la entidad de jugador, pero antes vamos a hacer lo que se llama gestor de recursos, que se encargará de cargar las imágenes, las fuentes o los sonidos de nuestro videojuego. No sólo lo usaremos en las entidades, si no que también en las escenas y en los menús del juego. Vamos a realizar algo parecido al gestor de entidades, por lo que realizaremos un prototipo. Crea el fichero “resources.lua” y guárdalo en la carpeta anterior (“main/systems”).

-- El gestor de recursos se encarga de cargar todos los recursos que le pidamos,
-- gestionando por dentro las carpetas a revisar y todo el tratamiento que
-- necesitemos.

-- Definimos la clase del gestor de recursos
local Resources = {
  -- Lista con los recursos cargados
  loaded = {},
  -- Lista con los métodos añadidos
  loaders = {},
  
  -- Añade un método de carga para un recurso específico
  addLoader = function (self, resource, method)
    -- Usamos una lista asociativa donde la clave es el recurso y el valor la función
    self.loaders[resource] = method
  end,
  
  -- Si el recurso no está cargado, llama a un método de carga concreto y devuelve el recurso
  loadResource = function (self, resource, name)
    -- Usamos una lista asociativa donde la clave es la unión del tipo de
    -- recurso y el nombre del recurso y el valor el recurso en si
    key = resource .. ":" .. name
    
    -- Si no existe lo cargamos
    if not self.loaded[key] then
      self.loaded[key] = self.loaders[resource](name)
    end
    
    -- Devolvemos el recurso cargado
    return self.loaded[key]
  end,
  
  -- Elimina un recurso de memoria
  removeResource = function (self, resource, name)
    key = resource .. ":" .. name
    
    -- Si tiene método release, se llama
    if self.loaded[key].release then
      self.loaded[key]:release()
    end
    
    -- Eliminamos el recurso cargado
    self.loaded[key] = false
  end
}

return Resources

Como podéis ver hay dos listas, que las trataremos como listas asociativas, es decir, a cada elemento le correspondemos una clave. En una de las listas guardaremos los recursos ya cargados (para no volverlos a cargar de nuevo) y otra donde guardaremos las funciones que permiten cargar los recursos. Estas funciones se añaden con el método addLoader, en donde se indica qué recurso carga esa función. Para cargar un recurso usaremos el método loadResource, que le pasamos el recurso y el nombre del recurso, para que nos lo devuelva de la lista de recursos cargados, y si no existe, lo carga antes llamando a la función añadida con el método anterior. Por último está el método remove, que se encarga de eliminar un recurso cargado.

Los métodos para cargar recursos los veremos más adelante, por lo que vamos a pasar a crear ya el jugador. Crea el fichero “player.lua” en la carpeta de entidades (“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 de Jugador
local Player = Class {
  __includes = Entity, -- Definimos el jugador como subclase de Entidad
  
  -- Función que se llama al crear un jugador
  init = function (self, x, y)
    self.image = Resources:loadResource("image", "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())
  end,

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

return Player

Esta vez usamos un clase debido a que tenemos que indicar que es una subclase de entidad. No hay mucho que destacar, ya que en el init lo que hacemos es llamar al gestor de recursos para que cargue la imagen y luego llamar la clase Entidad para que cree el resto de atributos, mientras que en el método draw pintamos en pantalla al personaje principal mediante la función de LÖVE love.graphics.draw.

Vamos a crear ahora la escena principal donde mostremos al jugador. Para ello crea el archivo “initialscene.lua” en la carpeta “main/scenes”. Dentro haremos el prototipo para la escena inicial.

-- Escena inicial

-- Importamos el módulo de colisiones
local Bump = require "libs.bump"
-- Importamos el gestor de entidades
local Entities = require "main.systems.entities"

-- Importamos las entidades a usar
local Player = require "main.entities.player"

-- Definimos la escena inicial
local InitialScene = {
  world = nil, -- Guarda el mundo actual
  player = nil, -- Guarda el jugador
  
  -- Carga la escena
  enter = function (self, previous)
    -- Iniciamos un nuevo mundo
    self.world = Bump.newWorld(35)
  
    -- Iniciamos el gestor de entidades
    Entities:switch(self.world)
    
    -- Creamos al jugador y lo añadimos al gestor de entidades
    self.player = Player(317, 468)
    Entities:add(self.player)
  end,

  -- Actualizamos la escena
  update = function (self, dt)
    -- Actualizamos todas las entidades
    Entities:update(dt)
  end,

  -- Dibujamos la escena
  draw = function (self)
    -- Dibuja el fondo
    love.graphics.clear(0.816, 0.957, 0.969)
    
    -- Dibujamos las entidades
    Entities:draw()
  end
}

return InitialScene

Como vamos a usar el módulo Gamestate de HUMP, nuestras escenas tienen que tener una serie de métodos para que funcione correctamente. El método enter se ejecuta al principio de una escena que se ha iniciado, y recibe como parámetro la escena anterior. Existe el método init, que se ejecuta solo la primera vez que se inicia la escena. También se puede usar el método leave, que se ejecuta cuando se sale de la escena. Los métodos update y draw se ejecutan en cada frame, y los usaremos para actualizar o pintar la escena. El resto de métodos los podéis ver en la documentación del módulo HUMP ya que no lo los usaremos.

Cabe destacar que he usado la función love.graphics.clear para poner un color de fondo en la escena. El color va en formato RGB con porcentajes, siendo 1 el color total y 0 sin ese color. Podéis usar vuestro color mirando en una aplicación gráfica como GIMP o Photoshop o incluso buscando colores en internet (Os recomiendo la entrada en la Wikipedia de colores web).

Ya nos queda poco, solo tendremos que hacer el script de inicio y de configuración para cargar la escena inicial y mostrarla. Crea el archivo “main.lua” en la carpeta raíz del proyecto.

-- Programa inicial

-- Importamos el módulo del gestor de escenas
local Gamestate = require "libs.hump.gamestate"
-- Importamos el gestor de recursos
local Resources = require "main.systems.resources"
-- Importamos nuestra escena inicial
local InitialScene = require "main.scenes.initialscene"

-- Función de Love2D que se llama al principio del juego
function love.load(args)
  -- Añadimos la función para cargar imágenes
  Resources:addLoader("image", function (name)
    image = love.graphics.newImage("resources/images/" .. name .. ".png")
    return image
  end)
  
  -- Preparamos el gestor de escenas
  Gamestate.registerEvents({"update", "draw"})
  -- Cambiamos a la escena principal
  Gamestate.switch(InitialScene)
end

-- Función de Love2D que se llama cuando se pulsa una tecla
function love.keypressed(key, scancode)
  -- Si pulsamos escape salimos
  if scancode == "escape" then
    love.event.quit(0)
  end
end

Vayamos por partes. La función love.load se lanza cada vez que LÖVE se carga, por lo que la usaremos para iniciar todos los módulos que necesitemos. En este caso añadiremos la función para cargar imágenes, la cual recibe el nombre de la imagen y lo concatena a la ruta. Para cargar una imagen necesitaremos la función love.graphics.newImage, que devuelve un objeto imagen, el cual lo devolveremos. Con esto nuestro gestor de recursos ya puede cargar imágenes.

Continuando con la función love.load, lo siguiente a hacer es preparar el gestor de escenas, donde indicamos que sustituya las funciones love.update y love.draw por los métodos de la escena, por último le indicamos a qué escena tenemos que cambiar.

Vamos ahora con el archivo de configuración. En la carpeta raíz crea un archivo llamado “conf.lua”.

-- Fichero de configuración

function love.conf(t)
  -- Título de la ventana
  t.window.title = "Primera escena"
  -- Ancho de la ventana
  t.window.width = 700
  -- Alto de la ventana
  t.window.height = 560
  -- Le indicamos a Love2D que abra una consola
  t.console = true
end

Con esto ya tenemos todo listo, solo falta ejecutar el proyecto. Si presionamos el icono ejecutar_icono y no hay errores, tendría que salirnos una ventana con la escena que hemos realizado.

Primera escena

Como veis aún no se mueve, pero queda poco, ya que en el próximo capítulo le indicaremos cómo moverse e incluso saltar.

Descargas

Anuncios

Un pensamiento en “[LÖVE] Programemos un plataformas en 2D: Creando nuestra primera escena

  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