6. Creación y animación de personaje - AdrianN17/Tutorial_Love2d_Top_Down_shooter GitHub Wiki
Anteriormente habíamos creados las bases de nuestro juego, ahora nos toca crear a nuestro personaje y sus funcionalidades, para ello debemos definir como va a interactuar en nuestro juego.
Nuestro personaje puede:
- Caminar.
- Correr.
- Disparar.
- Recargar Balas.
- Curarse.
- Destruir paredes.
- Recibir daño de los enemigos.
Nuestro personaje no puede:
- Sobrevivir mas de 5 golpes de enemigos.
- Atravesar muro sólidos.
- Disparar continuamente.
Ya definiendo como se va a comportar nuestro jugador, lo primero es tener sus animaciones, por lo cual nos vamos a ayudar de una herramienta para dividir nuestro spritesheet en partes mas pequeñas.
Elegimos nuestro spritesheet y damos click a nuestro skin a elegir, en mi caso elegiré al robot. Seleccionamos y guardamos las posiciones de las 5 imágenes, por ejemplo:
.sprite { background: url('imgs/spritesheet_characters.png') no-repeat -458px -88px; width: 33px; height: 43px; }
cogemos solo -458px -88px; width: 33px; height: 43px;
Los negativos lo pasamos a positivo y nos quedaría así:
458,88,33,43 x,y,w,h
Luego de haber apuntado las demás imágenes, ademas de nuestro zombie o enemigo a elegir, crearemos un archivo adicional en la carpeta assets, llamado sprites.lua
Nota : En nuestro mapa, las dimensiones de los objetos que era de 100 x 100, se han modificado a 35 x 43
--sprites.lua
local sprites={}
sprites["img"]= love.graphics.newImage("assets/img/spritesheet_characters.png")
sprites["player"]={}
sprites["player"][1]= love.graphics.newQuad(458,88,33,43, sprites["img"]:getDimensions())
sprites["player"][2]= love.graphics.newQuad(423,44,35,43, sprites["img"]:getDimensions())
sprites["player"][3]= love.graphics.newQuad(306,88,39,43, sprites["img"]:getDimensions())
sprites["player"][4]= love.graphics.newQuad(164,44,49,43, sprites["img"]:getDimensions())
sprites["player"][5]= love.graphics.newQuad(55,176,54,43, sprites["img"]:getDimensions())
sprites["player"][6]= love.graphics.newQuad(166,0,49,43, sprites["img"]:getDimensions())
sprites["enemigo"]={}
sprites["enemigo"][1]= love.graphics.newQuad(460,0,33,43, sprites["img"]:getDimensions())
sprites["enemigo"][2]= love.graphics.newQuad(424,0,35,43, sprites["img"]:getDimensions())
sprites["enemigo"][3]= love.graphics.newQuad(346,88,39,43, sprites["img"]:getDimensions())
sprites["enemigo"][4]= love.graphics.newQuad(163,132,49,43, sprites["img"]:getDimensions())
sprites["enemigo"][5]= love.graphics.newQuad(57,88,54,43, sprites["img"]:getDimensions())
sprites["enemigo"][6]= love.graphics.newQuad(162,176,49,43, sprites["img"]:getDimensions())
return sprites
Ahora, lo llamamos en nuestro archivo game.lua, y lo mandaremos como variable global para los demás objetos.
--game.lua
--las librerias llamadas
local sprites = require "assets.img.sprites"
spritesheet=nil
local game = Class{
__includes = base
}
function game:init()
spritesheet = sprites
end
...
Ahora creamos 5 archivos, player.lua, enemigos.lua, bala.lua, municion.lua y vida.lua, todo en nuestra carpeta entidades.
En nuestro archivo player.lua, debemos implementar el siguiente código.
local Class = require "libs.class"
local base = require "gamestate.base"
local entidad = require "entidades.entidad"
local HC = require "libs.HC"
local Timer = require "libs.timer"
local vector = require "libs.vector"
local player = Class{
__includes = entidad
}
function player:init(x,y,w,h)
end
function player:draw()
end
function player:update(dt)
end
function player:mousepressed(x,y,button)
end
function player:mousereleased(x,y,button)
end
function player:keypressed(key)
end
function player:keyreleased(key)
end
return player
Lo que hacemos es crear las bases de nuestro objeto, llamando como molde nuestro archivo entidad.lua.
Ahora, lo que hacemos es recibir los datos de inicializacion para nuestro objeto player.
--player.lua
function player:init(x,y,w,h)
self.body=HC.rectangle(x,y,w,h)
self.w,self.h=w,h
self.ox,self.oy=self.body:center()
self.radio=0
self.velocidad=500
self.hp=10
self.estado={ correr = false, inmunidad = false, vida = true}
self.direccion={a = false, s = false, d = false, w = false}
self.spritesheet=spritesheet
self.posicion=1
end
function player:draw()
love.graphics.draw(self.spritesheet.img,self.spritesheet.player[self.posicion],self.ox,self.oy,self.radio,1,1,self.w/2,self.h/2)
end
Editamos nuestra función de dibujado y actualizacion en entidades.lua
--entidades.lua
function entidades:player_draw()
self.player:draw()
end
function entidades:player_update(dt)
self.player:update(dt)
end
Implementamos finalmente en el archivo game.lua nuestro objeto
--game.lua
local Player = require "entidades.player"
....
function game:object()
local be=base.entidades
for i, object in pairs(self.map.objects) do
if object.name == "Player" then
be:actor(Player(object.x,object.y,object.width,object.height))
elseif object.name == "Caja" then
elseif object.name == "Enemigo" then
end
end
end
Implementamos un vector de movimiento para que nuestro jugador se mueva.
--player.lua
function player:update(dt)
local delta = vector(0,0)
if self.direccion.a then
delta.x=-1
elseif self.direccion.d then
delta.x=1
end
if self.direccion.w then
delta.y=-1
elseif self.direccion.s then
delta.y=1
end
delta:normalizeInplace()
delta=delta+ delta * self.velocidad *dt
self.body:move(delta:unpack())
self.body:setRotation(self.radio)
self.ox,self.oy=self.body:center()
end
function player:mousepressed(x,y,button)
end
function player:keypressed(key)
if key=="a" then
self.direccion.a=true
elseif key=="d" then
self.direccion.d=true
end
if key=="w" then
self.direccion.w=true
elseif key=="s" then
self.direccion.s=true
end
end
function player:keyreleased(key)
if key=="a" then
self.direccion.a=false
elseif key=="d" then
self.direccion.d=false
end
if key=="w" then
self.direccion.w=false
elseif key=="s" then
self.direccion.s=false
end
end
Lo que resultaría, algo asi:
Para ocultar ese rectángulo blanco, eliminaremos la capa borrador de nuestro mapa. Solo bastaría ingresar el siguiente código en nuestro archivo game.lua:
--game.lua
function game:enter()
...
self.map:removeLayer("Borrador")
end
Ahora, nos faltaría darle movimiento con el mouse y hacer que nuestro jugador sea seguido por la cámara, para ello debemos integrar la camara con nuestro player.
--entidades.lua
...
function entidades:position()
return self.player.ox,self.player.oy
end
...
Y en nuestro archivo game.lua
spritesheet= sprites
local camview={x=0,y=0,w=0,h=0}
...
local Player = require "entidades.player"
function game:draw()
camview.x,camview.y,camview.w,camview.h=self.cam:getVisible()
self.map:draw(-camview.x,-camview.y,self.scale,self.scale)
self.cam:setPosition(base.entidades:position())
end
...
function game:mousepressed(x,y,button)
local cx,cy=self.cam:toWorld(x,y)
base.entidades:mousepressed(cx,cy,button)
end
function game:mousereleased(x,y,button)
local cx,cy=self.cam:toWorld(x,y)
base.entidades:mousereleased(cx,cy,button)
end
Ahora nuestro personaje se puede mover a libertad, pero el problema es que no gira a donde nuestro mouse apunta. Para ello recogeremos los valores de nuestro mouse y le daremos un angulo.
Lo que haremos es aumentar una funcion en entidades.lua y en player.lua, para generar nuestro angulo.
--entidades.lua
function entidades:getmouseposition()
return self.cam:toWorld(love.mouse.getX( ),love.mouse.getY( ))
end
--player.lua
function player:update(dt)
...
self.ox,self.oy=self.body:center()
self.radio=self:angle()
end
function player:angle()
local angulo=0
local mx,my=base.entidades:getmouseposition()
angulo=math.atan2(my-self.oy,mx-self.ox)
return angulo
end
Y ya tenemos el giro de nuestro jugador:
Esto nos ayudará mas adelante con la dirección de las balas y entre otras cosas más.
Pero, nos damos cuenta que nuestro jugador no esta detectando correctamente las colisiones, es capaz de atravesar las paredes como si nada, en este caso, nos toca realizar las colisiones, y para ello nos dirigimos a nuestra funcion collisions de nuestro archivo entidades.lua.
Agregamos el siguiente codigo:
--entidades.lua
function entidades:collisions()
for _,solido in ipairs(self.solidos) do
local dx,dy,collision=0,0,false
collision,dx,dy= self.player.body:collidesWith(solido.body)
if collision then
self.player.body:move(dx,dy)
end
end
for _,destruible in ipairs(self.destruible) do
local dx,dy,collision=0,0,false
collision,dx,dy= self.player.body:collidesWith(destruible.body)
if collision then
self.player.body:move(dx,dy)
end
end
end
Ahora al intentar atravesar una pared, nos es imposible realizar dicha acción, y nos regresa a donde estábamos antes.
Lo único que nos faltaría, seria que nuestro personaje cambie de arma, dispare y corra al presionar un botón.
Pero antes... recordamos que teníamos unas cajas en nuestro mapa, pero no están en ningún lado. Para poder visualizarlos, debemos editar nuestro game.lua
En la función layer, editamos nuestro código.
--game.lua
function game:layers()
local layer_personajes = self.map.layers["Personajes"]
be=base.entidades
function layer_personajes:draw()
be:balas_draw()
be:enemigos_draw()
be:player_draw()
be:objetos_draw()
end
function layer_personajes:update(dt)
be:balas_update(dt)
be:enemigos_update(dt)
be:player_update(dt)
be:objetos_update(dt)
end
end
Ademas de agregar la siguiente función en nuestro entidades.lua :
--entidades.lua
function entidades:replace_tile(layer, tile, newTileGid)
local x=(tile.x/self.map.tilewidth)+1
local y=(tile.y/self.map.tileheight)+1
layer = self.map.layers[layer]
for i, instance in ipairs(self.map.tileInstances[layer.data[y][x].gid]) do
if instance.layer == layer and instance.x == tile.x and instance.y == tile.y then
instance.batch:set(instance.id, self.map.tiles[newTileGid].quad, instance.x, instance.y)
break
end
end
end
function entidades:replace_object(object,gid_tile)
for i, instance in ipairs(self.map.tileInstances[object.gid]) do
if object.x == instance.x and object.y == instance.y then
instance.batch:set(instance.id, self.map.tiles[gid_tile].quad, object.x,object.y)
break
end
end
end
Nos servirá luego, para modificar nuestro mapa.
Agregamos las colisiones de nuestras cajas en game.lua
--game.lua
function game:object()
local be=base.entidades
for i, object in pairs(self.map.objects) do
if object.name == "Player" then
be:actor(Player(object.x,object.y,object.width,object.height))
elseif object.name == "Caja" then
be:add({body=self.collider:rectangle(object.x,object.y,object.width,-object.height),hp=5},"destruible")
elseif object.name == "Enemigo" then
end
end
end
El resultado sera el siguiente:
Volviendo a nuestro jugador, lo que necesitamos ahora es darle animación, por lo tanto, en nuestro file jugador.lua agregaremos las siguientes variables y tablas:
--player.lua
function player:init(x,y,w,h)
self.body=base.entidades.collider:rectangle(x,y,w,h)
self.w,self.h=w,h
self.ox,self.oy=self.body:center()
self.radio=0
self.velocidad=500
self.hp=10
self.estado={ correr = false, inmunidad = false, vida = true, disparo=false}
self.direccion={a = false, s = false, d = false, w = false}
self.spritesheet=spritesheet
self.posicion=1
self.arma=1
self.municion={"infinito",0,0}
self.stock={7,0,0}
self.max_municion={"infinito",100,60}
self.max_stock={7,25,20}
base.entidades.timer_player:every(0.15, function()
if not self.estado.disparo then
if self.direccion.a or self.direccion.d or self.direccion.s or self.direccion.w then
if self.arma == 1 then
self.posicion=2
else
self.posicion=3
end
else
self.posicion=1
end
elseif self.estado.disparo then
if self.arma==1 then
self.posicion=4
elseif self.arma==2 then
self.posicion=5
elseif self.arma==3 then
self.posicion=6
end
end
end)
end
function player:update(dt)
...
if self.estado.correr then
if self.estado.disparo then
self.estado.correr=false
else
self.velocidad=650
end
else
self.velocidad=500
end
end
function player:mousepressed(x,y,button)
if button==1 then
self.estado.disparo=true
end
end
function player:mousereleased(x,y,button)
if button==1 then
self.estado.disparo=false
end
end
function player:keypressed(key)
if key=="a" then
self.direccion.a=true
elseif key=="d" then
self.direccion.d=true
end
if key=="w" then
self.direccion.w=true
elseif key=="s" then
self.direccion.s=true
end
if key=="space" then
self.estado.correr=true
end
if key=="1" then
self.arma=1
elseif key=="2" then
self.arma=2
elseif key=="3" then
self.arma=3
end
end
function player:keyreleased(key)
if key=="a" then
self.direccion.a=false
elseif key=="d" then
self.direccion.d=false
end
if key=="w" then
self.direccion.w=false
elseif key=="s" then
self.direccion.s=false
end
if key=="space" then
self.estado.correr=false
end
end
Estamos implementando la libreria Timer, por lo tanto debemos agregarla en nuestro codigo de base.lua.
--base.lua
local base = Class{
__includes = Gamestate,
init = function(self, mapfile)
self.map=sti(mapfile)
self.scale=0.7
self.cam=gamera.new(0,0,self.map.width*self.map.tilewidth, self.map.height*self.map.tileheight)
self.cam:setScale(self.scale)
self.map:resize(love.graphics.getWidth()*2,love.graphics.getHeight()*2)
self.collider = HC.new()
self.timer_player = Timer.new()
self.timer_enemigo= Timer.new()
entidades:enter(self.map,self.cam,self.collider,self.timer_player,self.timer_enemigo)
end;
entidades = entidades;
}
Y actualizarlo constantemente en nuestro archivo entidades.
--entidades.lua
local entidades = {
map=nil,
cam=nil,
collider=nil,
player=nil,
enemigos={},
timer_player=nil,
timer_enemigo=nil,
solidos={},
destruible={},
objetos={},
balas={}
}
function entidades:enter(map,cam,collider,timer_player,timer_enemigo)
self.map=map
self.cam=cam
self.collider=collider
self.timer_player=timer_player
self.timer_enemigo=timer_enemigo
end
...
function entidades:player_update(dt)
self.player:update(dt)
self.timer_player:update(dt)
end
Esta libreria actualiza cada momento nuestro jugador, sin la necesidad de utilizar muchos contadores, esto puede hacer nuestro código mas legible para nosotros.
Nuestro personaje ya puede movilizarse y cambia de animación de manera dinámica.
Lo que faltaría seria la creación de nuestras balas, nuestro jugador puede disparar, pero la creación del objeto bala no esta definida aun.
Lo que haríamos es crear tal objeto, pero antes necesitamos las imágenes de nuestras balas. Para ello descargamos el siguiente spritesheet, en las imagenes de la carpeta PNG buscamos las siguientes imagenes, tanto con este spritesheet, como el que ya usamos.
Utilizamos la siguiente herramienta para crear spritesheets de manera sencilla.
Le damos padding de 5, y utilizamos la herramienta para medir posiciones de nuestras imagenes, y la agregamos en nuestro archivo sprites.lua.
Debería quedar de la siguiente manera:
--sprites.lua
local sprites={}
...
sprites["img2"]= love.graphics.newImage("assets/img/sprites.png")
sprites["armas"]={}
sprites["armas"][1]= love.graphics.newQuad(45,207,19,10,sprites["img2"]:getDimensions())
sprites["armas"][2]= love.graphics.newQuad(0,222,25,10,sprites["img2"]:getDimensions())
sprites["armas"][3]= love.graphics.newQuad(0,207,33,10,sprites["img2"]:getDimensions())
sprites["balas"]={}
sprites["balas"][1]= love.graphics.newQuad(24,93,16,16,sprites["img2"]:getDimensions())
sprites["balas"][2]= love.graphics.newQuad(24,162,16,16,sprites["img2"]:getDimensions())
sprites["balas"][3]= love.graphics.newQuad(24,24,16,16,sprites["img2"]:getDimensions())
return sprites
Ahora en nuestro archivo bala.lua
--bala.lua
local Class = require "libs.class"
local base = require "gamestate.base"
local entidad = require "entidades.entidad"
local vector = require "libs.vector"
local balas = Class{
__includes = entidad
}
function balas:init(x,y,tipo,velocidad,direccion)
self.body=base.entidades.collider:circle(x,y,8)
self.spritesheet=spritesheet.img2
self.imgbala=spritesheet.balas[tipo]
self.ox,self.oy=self.body:center()
self.delta=vector(1,0)
self.delta:rotateInplace(direccion)
self.delta=self.delta*velocidad
end
function balas:draw()
love.graphics.draw(self.spritesheet,self.imgbala,self.ox,self.oy,0,0.5,0.5,8,8)
end
function balas:update(dt)
local delta=self.delta
delta=delta*dt
self.body:move(delta:unpack())
self.ox,self.oy=self.body:center()
end
return balas
Modificamos de igual manera nuestro archivo entidades.lua para visualizar nuestras balas, con el siguiente código:
--entidades.lua
function entidades:player_draw()
self.player:draw()
for _, e in ipairs(self.balas) do
e:draw()
end
end
function entidades:player_update(dt)
self.player:update(dt)
self.timer_player:update(dt)
for _, e in ipairs(self.balas) do
e:update(dt)
end
end
Y en nuestro archivo player.lua, invocamos nuestras balas
--player.lua
local Bala= require "entidades.balas"
function player:init(x,y,w,h)
...
self.arma=1
self.municion={"infinito",0,0}
self.stock={7,0,0}
self.max_municion={"infinito",100,60}
self.max_stock={7,25,20}
self.vel_bala={700,1000,1200}
base.entidades.timer_player:every(0.15, function()
if not self.estado.disparo then
if self.direccion.a or self.direccion.d or self.direccion.s or self.direccion.w then
if self.arma == 1 then
self.posicion=2
else
self.posicion=3
end
else
self.posicion=1
end
elseif self.estado.disparo then
if self.arma==1 then
self.posicion=4
self.estado.disparo=false
elseif self.arma==2 then
self.posicion=5
elseif self.arma==3 then
self.posicion=6
end
end
if self.estado.disparo and self.arma==2 then
self:create_bullet()
end
end)
base.entidades.timer_player:every(0.1, function()
if self.estado.disparo and self.arma==3 then
self:create_bullet()
end
end)
end
function player:mousepressed(x,y,button)
if button==1 and self.arma>1 then
self.estado.disparo=true
elseif button==1 and self.arma==1 then
self.estado.disparo=true
self:create_bullet()
elseif button==2 then
end
end
function player:mousereleased(x,y,button)
if button==1 and self.arma>1 then
self.estado.disparo=false
elseif button==2 then
end
end
function player:create_bullet()
base.entidades:add(Bala(self.ox,self.oy,self.arma,self.vel_bala[self.arma],self.radio),"balas")
end
El resultado seria el siguiente:
Pero, nuestro personaje dispara balas infinitas en los 3 casos, lo que debemos hacer es limitar su capacidad a solo una cantidad de balas, para ello agregaremos el siguiente código en la funcion create_bullet():
--player.lua
function player:create_bullet()
if self.stock[self.arma] > 0 then
base.entidades:add(Bala(self.ox,self.oy,self.arma,self.vel_bala[self.arma],self.radio),"balas")
self.stock[self.arma]= self.stock[self.arma] -1
end
end
Lo que hacemos es limitar a que la cantidad de stock sea mayor a 0, y si lo cumple disminuimos en 1 la cantidad (disparamos 1 bala), lo que faltaría seria la recarga de nuestras balas.
Para ello agregaremos un estado adicional en la funcion player:init()
--player.lua
self.estado={ correr = false, inmunidad = false, vida = true, disparo=false, recarga=false}
self.arma=1
self.municion={7,0,0}
self.stock={7,0,0}
self.max_municion={"infinito",100,60}
self.max_stock={7,25,20}
self.vel_bala={700,1000,1200}
self.recarga_vel={0.5,0.8,1}
Y en la funcion mousepressed y keypressed se agregan el siguiente script:
function player:mousepressed(x,y,button)
if button==1 and self.arma>1 then
self.estado.disparo=true
self.estado.recarga=false
elseif button==1 and self.arma==1 then
self.estado.disparo=true
self:create_bullet()
self.estado.recarga=false
elseif button==2 and self.stock[self.arma] < self.max_stock[self.arma] and not self.estado.recarga then
self.estado.recarga=true
base.entidades.timer_player:after(self.recarga_vel[self.arma] , function()
if self.estado.recarga then
if self.max_municion[self.arma] == "infinito" then
self.stock[self.arma]=self.max_stock[self.arma]
else
if self.municion[self.arma] + self.stock[self.arma] < self.max_stock[self.arma] then
self.stock[self.arma]=self.municion[self.arma]+self.stock[self.arma]
self.municion[self.arma]=0
else
local carga=self.max_stock[self.arma]-self.stock[self.arma]
self.stock[self.arma]=self.stock[self.arma]+carga
self.municion[self.arma]=self.municion[self.arma]-carga
end
end
self.estado.recarga=false
end
end)
end
end
function player:keypressed(key)
...
if key=="1" then
self.arma=1
self.estado.recarga=false
elseif key=="2" then
self.arma=2
self.estado.recarga=false
elseif key=="3" then
self.arma=3
self.estado.recarga=false
end
end
En este momento, nuestro usuario manera una cantidad limitada de balas, excepto en las pistolas que son ilimitadas.
Pero, nuestro personaje, si nos fijamos bien y con algo de curiosidad, puede salirse de la camara, y nuestras balas continúan de manera infinita su recorrido, lo que seria un gasto de memoria. Para solucionarlo, vamos a delimitar el alcance de nuestro jugador y balas.
--player.lua
function player:update(dt)
local delta = vector(0,0)
local be=base.entidades
if self.direccion.a and self.ox > be.limites.x then
delta.x=-1
elseif self.direccion.d and self.ox < be.limites.x + be.limites.w then
delta.x=1
end
if self.direccion.w and self.oy > be.limites.y then
delta.y=-1
elseif self.direccion.s and self.oy < be.limites.y + be.limites.h then
delta.y=1
end
end
Creamos una function en entidades.lua
--entidades.lua
local entidades = {
map=nil,
cam=nil,
collider=nil,
player=nil,
enemigos={},
timer_player=nil,
timer_enemigo=nil,
solidos={},
destruible={},
objetos={},
balas={},
limites={}
}
function entidades:clear()
...
self.limites={}
end
function entidades:limit(table)
self.limites = table
end
y la implementamos en game.lua
--game.lua
function game:update(dt)
self.map:update(dt)
base.entidades:collisions()
base.entidades:limit(camview)
end
Lo mismo para las balas, si la bala pasa la cámara entonces sera eliminada para ahorrar memoria:
--balas.lua
function balas:update(dt)
local be=base.entidades
...
if self.ox < be.limites.x or self.ox > be.limites.x + be.limites.w or self.oy < be.limites.y or self.oy > be.limites.y + be.limites.h then
be:remove(self,"balas")
end
end
Por ultimo, agregamos en daño que haremos al objeto/enemigo que impacte nuestras balas.
--player.lua
function player:init(x,y,w,h)
...
self.daño={1,2.5,5}
...
end
function player:create_bullet()
if self.stock[self.arma] > 0 then
base.entidades:add(Bala(self.ox,self.oy,self.arma,self.vel_bala[self.arma],self.radio,self.daño[self.arma]),"balas")
self.stock[self.arma]= self.stock[self.arma] -1
end
end
--balas.lua
function balas:init(x,y,tipo,velocidad,direccion,daño)
...
self.daño=daño
...
end