7. Creación y animación del enemigo y objetos del mapa - AdrianN17/Tutorial_Love2d_Top_Down_shooter GitHub Wiki

En la clase anterior, vimos lo que es la creación de nuestro jugador, sus estados y acciones, ademas de la animación con la cual este contaría, ahora para dar mayor interacción a nuestro jugador, nos toca dar vida a nuestro enemigo.

El enemigo

Para no tocar temas muy profundos como IA o pathfinding A*, u otra manera de darle un razonamiento, y para no alargar mucho mas nuestro tutorial, nuestro enemigo solo tendra una funcion:

Puede:

  • Atravesar paredes, pero de manera muy lenta, las va a saltar
  • Destruir barricadas, pero demorara un tiempo en realizarlo Seguir al jugador a donde quiera que vaya

No puede:

  • Disparar

Ahora, nuestro enemigo se regenerara las veces que sea necesario, una cantidad infinita de veces, hasta que nuestro jugador muera.

Lo que primero realizaremos es editar nuestro archivo enemigos.lua

--enemigos.lua
local Class = require "libs.class"
local base = require "gamestate.base"
local entidad = require "entidades.entidad"
local vector = require "libs.vector"

local enemigos = Class{
	__includes = entidad
}

function enemigos:init(x,y,w,h)

	self.body=base.entidades.collider:rectangle(x,y,w,h)
	self.w,self.h=w,h

	self.velocidad=250

	self.spritesheet=spritesheet.img
	self.imgs=spritesheet.enemigo

	self.posicion=1

	self.radio=0

	self.hp=6

	self.ox,self.oy=self.body:center()

	self.visible=false

	self.delta = vector(0,0) 

	self.collision=false
	
	self.daño=2

end

function enemigos:draw()
	love.graphics.draw(self.spritesheet,self.imgs[self.posicion],self.ox,self.oy,self.radio,1,1,self.w/2,self.h/2)

end

function enemigos:update(dt)
	local delta= self.delta * self.velocidad *dt



	self.body:move(delta:unpack())

	self.body:setRotation(self.radio)

	self.ox,self.oy=self.body:center()

	self.collision=false
	
end

function enemigos:start()

	self.visible=true

	base.entidades.timer_enemigo:after(1, function() self.posicion=2 end)
end

return enemigos

Agregamos nuestro objeto en game.lua

function game:object()
	...
	
	elseif object.name == "Enemigo" then
		be:add(Zombie(object.x,object.y,object.width,object.height),"enemigos")
	end
end

Y finalmente en nuestra funcion entidades.lua

--entidades.lua

function entidades:player_draw()
	self.player:draw()

end

function entidades:player_update(dt)
	self.player:update(dt)

	self.timer_player:update(dt)
	
end

function entidades:enemigos_draw()
	for _, e in ipairs(self.enemigos) do
		e:draw()
	end
end

function entidades:enemigos_update(dt)
	self.timer_enemigo:update(dt)

	for _, e in ipairs(self.enemigos) do
		e:update(dt)
	end
end

function entidades:seek_player()
	self.timer_enemigo:every(2, function() 
		for _, zombie in ipairs(self.enemigos) do
			if zombie.visible then
				zombie.radio=math.atan2(self.player.oy-zombie.oy,self.player.ox-zombie.ox)
				zombie.delta.y=math.sin(zombie.radio)
				zombie.delta.x=math.cos(zombie.radio)
			end
		end
	end)

	self.timer_enemigo:every(0.5, function() 
		for _, zombie in ipairs(self.enemigos) do
			if not zombie.collision then
				zombie.velocidad=250
			end
		end
	end)
end

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

		for _,zombie in ipairs(self.enemigos) do
			local dx,dy,collision=0,0,false
			collision,dx,dy= zombie.body:collidesWith(solido.body) 

			if collision then
				zombie.velocidad=50
				zombie.collision=true
			end
		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

	for _,zombie_1 in ipairs(self.enemigos) do
		for _,zombie_2 in ipairs(self.enemigos) do
			local dx,dy,collision=0,0,false
			collision,dx,dy= zombie_1.body:collidesWith(zombie_2.body) 

			if collision then
				zombie_1.body:move(dx,dy)
			end
		end
	end

end
function entidades:script()

	for _, zombie in ipairs(self.enemigos) do
		if self:camera_visible(zombie) and not zombie.visible then
			zombie:start()
		end
	end
end

Lo que hacemos es enviarle la distancia entre el jugador y el zombie para que este lo siga, pero en un intervalo de tiempo predeterminado, ademas el zombie te seguirá cuando lo veas en pantalla, cambiando de inmovil a movil en 1 segundo, ademas de que estan separados unos de otros.

Lo que a continuacion haremos será eliminarlos con nuestras balas, para ello crearemos la siguiente funcion en entidades.lua :

entidades.lua
function entidades:collisions()
	...
	
	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

		for _,bala in ipairs(self.balas) do
			local dx,dy,collision=0,0,false
			collision,dx,dy=destruible.body:collidesWith(bala.body) 

			if collision then
				destruible.hp=destruible.hp-bala.daño
				self:remove(bala,"balas")
				print(destruible.hp)
			end
		end
	end

	for _,zombie_1 in ipairs(self.enemigos) do
		for _,zombie_2 in ipairs(self.enemigos) do
			local dx,dy,collision=0,0,false
			collision,dx,dy= zombie_1.body:collidesWith(zombie_2.body) 

			if collision then
				zombie_1.body:move(dx,dy)
			end
		end

		for _,bala in ipairs(self.balas) do

			local dx,dy,collision=0,0,false
			collision,dx,dy=zombie_1.body:collidesWith(bala.body) 

			if collision then
				zombie_1:damage(bala)
			end
		end
	end
end

Y agregamos adicionalmente la colisión bala-pared, para que nuestras balas se eliminen a tocar algo solido, solo nos faltaría agregar la función damage en nuestro archivo enemigos.lua

--enemigos.lua
function enemigos:damage(agresor)

	self.hp=self.hp-agresor.daño

	base.entidades:remove(agresor,"balas")

	if self.hp<1 then
		base.entidades:remove(self,"enemigos")
	end
end


Lo que tendríamos seria algo así:

alt text

Y podemos hacer que nuestros enemigos desaparezcan, solo con dispararles las veces que sea necesario.

Pero, nos tocan y no nos hacen nada.

Para ello, agregaremos las colisiones player - zombies, añadiendo la funcion damage en player.lua y la colision en entidades.lua

--player.lua
function player:damage(agresor)

	self.hp=self.hp-agresor.daño
	
	if self.hp<1 then

	end

	self.estado.inmunidad=true

	base.entidades.timer_player:after(1, function() self.estado.inmunidad=false end)
end
--entidades.lua
function entidades:collisions()
	for _,zombie_1 in ipairs(self.enemigos) do
		local dx,dy,collision=0,0,false
		collision,dx,dy= self.player.body:collidesWith(zombie_1.body) 

		if collision and not self.player.estado.inmunidad then
			self.player:damage(zombie_1)
		end

		...
	end
end

Nuestro personaje cuenta con 1 segundo de inmunidad al ser golpeado por un zombie, aun nos faltaria agregar que hacer en el caso que llegue a 0 nuestra vida. Podriamos destruir el objeto, tal como hacemos con las balas y zombies, pero nos dara un error, ya que de todas maneras lo utilizamos. Pero dejaremos esto para más adelante ...

Ahora nos toca la creación de objetos en nuestro mapa:

Creación y animación de objetos del mapa.

Para comenzar, crearemos nuestro objeto en el archivo municion.lua

--municion,lua
local Class = require "libs.class"
local base = require "gamestate.base"
local entidad = require "entidades.entidad"

local municion = Class{
	__includes = entidad
}

function municion:init(x,y)
	self.body=base.entidades.collider:circle(x,y,16)

	self.ox,self.oy=self.body:center()

	self.tipo=love.math.random(1,3)

	self.spritesheet=spritesheet.img2
	self.img=spritesheet.balas[self.tipo]
end

function municion:draw()
	love.graphics.draw(self.spritesheet,self.img,self.ox,self.oy,0,2,2,8,8)
end

return municion

Seguidamente, lo invocaremos en nuestra función game.lua, agregaremos en los layers para que pueda ser dibujado. Ademas de una función nueva que se encarga de destruir si una bala toca repetidas veces un objeto destruible, destruyéndolo y cambiándole la imagen

--game.lua

...

local Municion = require "entidades.municion"

...

function game:layers()
	local layer_personajes = self.map.layers["Personajes"]

	be=base.entidades

	function layer_personajes:draw()
		be:objetos_draw()
		be:balas_draw()
		be:enemigos_draw()
		be:player_draw()
	end

	function layer_personajes:update(dt)
		be:player_update(dt)
		be:enemigos_update(dt)
		be:balas_update(dt)
		be:objetos_update(dt)
	end
end

function game:change_items()
	local be=base.entidades

	for _,destruible in ipairs(be.destruible) do
		if destruible.hp<1 and destruible.tipo=="caja" then
			local x,y=destruible.body:center()
			be:add(Municion(x,y),"objetos")
			be:remove(destruible,"destruible")
			be:replace_object(destruible,265)
		elseif destruible.hp< 1 and destruible.tipo=="pared" then
			local random={265,292}
			be:remove(destruible,"destruible")
			be:replace_tile(2,destruible,random[love.math.random(1,2)])
		end
	end
end

Ademas de las colisiones en entidades.lua

--entidades.lua

function entidades:collisions()

	...
	
	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

		for _,bala in ipairs(self.balas) do
			local dx,dy,collision=0,0,false
			collision,dx,dy=destruible.body:collidesWith(bala.body) 

			if collision then
				destruible.hp=destruible.hp-bala.daño
				self:remove(bala,"balas")
			end
		end
	end

	...
end

El resultado:

alt text

Además, nos faltaría la creación de vidas para nuestro jugador, para ello buscaremos las siguientes imagenes

Elegimos las siguientes imágenes de la carpeta PNG, en mi caso elegiré las siguientes, pero es opcional:

alt text

Creamos un spritesheet con nuestra herramienta, tal como el anterior le damos un padding de 5 px y listo, ya tenemos nuestro spritesheet para nuestra ui y vida.

--sprites.lua
	sprites["img3"]= love.graphics.newImage("assets/img/sprites_2.png")

	sprites["vida"] = love.graphics.newQuad(155,153,24,24,sprites["img3"]:getDimensions())

	sprites["hp"]={}
	sprites["hp"][1]= love.graphics.newQuad(196,75,6,26,sprites["img3"]:getDimensions())
	sprites["hp"][2]= love.graphics.newQuad(184,151,16,26,sprites["img3"]:getDimensions())
	sprites["hp"][3]= love.graphics.newQuad(196,106,6,26,sprites["img3"]:getDimensions())

	sprites["ui"]= love.graphics.newQuad(50,75,100,100,sprites["img3"]:getDimensions())

	sprites["mouse"]={}
	sprites["mouse"][1]= love.graphics.newQuad(155,75,36,36,sprites["img3"]:getDimensions())
	sprites["mouse"][2]= love.graphics.newQuad(157,116,30,30,sprites["img3"]:getDimensions())

Lo agregamos a nuestro archivo sprites.lua.

Invocamos el objeto en game.lua

--game.lua
local Vida = require "entidades.vida"

Y en la funcion change_items, agregamos debajo de destruible.tipo=="caja" una probabilidad de que salga vida o municion.

--game.lua
local x,y=destruible.body:center()
local random= love.math.random(1,10)

if random >=5 then
	be:add(Municion(x,y),"objetos")
else
	be:add(Vida(x,y),"objetos")
end

Y ademas creamos una funcion reload, tanto para municion, como para vida.lua

--municion.lua
function municion:init(x,y)	

	...
	
	if self.tipo==2 then
		self.cantidad=love.math.random(15,40)
	elseif self.tipo==3 then
		self.cantidad=love.math.random(10,30)
	end
end

function municion:reload(tabla)
	local be=base.entidades

	if tabla.municion[self.tipo]+self.cantidad< tabla.max_municion[self.tipo] then
		tabla.municion[self.tipo]=tabla.municion[self.tipo]+self.cantidad
		self.cantidad=0
		be:remove(self,"objetos")
	else
		local muni=tabla.max_municion[self.tipo]-tabla.municion[self.tipo]
		tabla.municion[self.tipo]=tabla.municion[self.tipo]+muni
		self.cantidad=self.cantidad-muni
	end
end

--vida.lua
function vida:reload(tabla)

	if tabla.hp < 10 then
		tabla.hp=tabla.hp+2

		if tabla.hp>10 then
			tabla.hp=10
		end

		local be=base.entidades

		be:remove(self,"objetos")
	end

end

La implementamos en entidades.lua

--entidades.lua
function entidades:collisions()
	
	...
	
	for _, objeto in ipairs(self.objetos) do
		local dx,dy,collision=0,0,false
		collision,dx,dy= self.player.body:collidesWith(objeto.body) 

		if collision then
			objeto:reload(self.player)
		end
	end
end

Ahora, agregamos para finalizar el siguiente script, con el cual el enemigo podra romper paredes y cajas.

--entidades.lua
function entidades:collisions()

	...
	
	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

		for _,bala in ipairs(self.balas) do
			local dx,dy,collision=0,0,false
			collision,dx,dy=destruible.body:collidesWith(bala.body) 

			if collision then
				destruible.hp=destruible.hp-bala.daño
				self:remove(bala,"balas")
			end
		end

		for _,zombie in ipairs(self.enemigos) do
			local dx,dy,collision=0,0,false
			collision,dx,dy=zombie.body:collidesWith(destruible.body) 

			if collision then
				zombie.body:move(dx,dy)

				if  not zombie.atacando then
					destruible.hp=destruible.hp-zombie.daño
					zombie.atacando=true

					self.timer_enemigo:after(1, function () zombie.atacando=false end)
				end
			end
		end
	end

	...
	
end

Ademas de agregarle a enemigos.lua un atributo más

--enemigos.lua
function enemigos:init(x,y,w,h)
	
	...
	
	self.atacando=false

end

El resultado final seria:

alt text

Para finalizar, cuando terminamos, los enemigos ya no se regeneran, para ello haremos que regeneren de manera ilimitada.

Creamos un contador en nuestro archivo 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={},
	respawn_enemigos={},
	cantidad_zombies=0
}

end

function entidades:clear()
	self.map=nil
	self.player=nil
	self.enemigos={}
	self.solidos={}
	self.destruible={}
	self.objetos={}
	self.balas={}
	self.limites={}
	self.respawn_enemigos={}
	self.cantidad_zombies=0
end

function entidades:respawn_all(objeto)
	for _, posicion in ipairs(self.respawn_enemigos) do
		self:add(objeto(posicion.x,posicion.y),"enemigos")
	end

	self.cantidad_zombies=self.cantidad_zombies+7
end

function entidades:respawn_random(objeto)
	local seed = love.math.random(1,7)
	local tabla=self.respawn_enemigos[seed]
	self:add(objeto(tabla.x,tabla.y) ,"enemigos")

	self.cantidad_zombies=self.cantidad_zombies+1
end

Ademas modificamos nuestros archivo enemigos.lua

--enemigos.lua
function enemigos:init(x,y)

	self.body=base.entidades.collider:rectangle(x,y,35,43)

	self.velocidad=250

	self.spritesheet=spritesheet.img
	self.imgs=spritesheet.enemigo

	self.posicion=1

	self.radio=0

	self.hp=6

	self.ox,self.oy=self.body:center()

	self.visible=false

	self.delta = vector(0,0) 

	self.collision=false

	self.daño=2

	self.atacando=false

end

function enemigos:draw()
	love.graphics.print(self.velocidad,self.ox,self.oy-100)
	love.graphics.draw(self.spritesheet,self.imgs[self.posicion],self.ox,self.oy,self.radio,1,1,35/2,43/2)

end

Y game.lua:

--game.lua
local intervalo=0

function game:enter()
	intervalo=2.5

	base.entidades:clear()
	base.init(self,"assets/mapas/mapa.lua")

	self:layers()


	self:tiles(1)
	self:tiles(2)

	self:object()

	self.map:removeLayer("Borrador")

	base.entidades:respawn_all(Zombie)


	base.entidades:seek_player()

	base.entidades.timer_player:every(intervalo, function() 
		local be=base.entidades

		if be.cantidad_zombies<1 then
			be:respawn_all(Zombie)
		else
			be:respawn_random(Zombie)
		end
	end)

	base.entidades.timer_player:every(25, function() 
		if intervalo>0.75 then
			intervalo=intervalo-0.25
		end

	end)
end

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,tipo="caja",gid=object.gid, x=object.x, y=object.y-object.height},"destruible")
		elseif object.name == "Enemigo" then
			table.insert(be.respawn_enemigos,{x = object.x , y = object.y})
		end
	end
end

Lo que haremos es coger las posiciones y cada cierto tiempo se crearan nuevos zombies, ademas de que se regulara la cantidad de apariciones o el intervalo cada cierto tiempo.

Y nuestro resultado debe ser algo así:

alt text

Y con esto finalizamos lo que sería nuestro videojuego.