Un juego debe suponer un reto. Si siempre repetimos el mismo ciclo el jugador se aburrirá pronto y dejará el juego.

Para construir un juego divertido es necesario permitir evolucionar y mejorar al personaje o en este caso nuestra nave y avanzar incrementando la dificultad conforme vamos aprendiendo a jugar. Quizá esto último sea lo más difícil de lograr, mantener un equilibrio entre dificultad y jugabilidad. Si el juego es muy fácil aburre y si es muy difícil desespera 😉

En entradas anteriores hemos visto animación, gráficos, sonidos, colisiones… todos elementos que hacen posible la representación del juego.

Una vez sabemos como pintar y animar objetos es tirar de imaginación.

En esta entrada vamos a implementar un aumento de la dificultad conforme avanza el juego y el control de fin de juego, Game Over, bien por éxito al lograr superar todos los niveles o al perder todas las vidas.

Comprueba juego

Empecemos con el código.

Ya tenemos implementada una función xcompruebajuego() que llamamos en love.update() donde comprobamos si hemos abatido a todos los enemigos, si es así, volvemos a crearlos:

function xcompruebajuego()
	if(#aliens==0) then
		xnewalien(0)
		xnewalien(1)
		xnewalien(3)
	end
end

Recordamos que love.update() es ejecutado automáticamente por el motor de love2d y es donde realizamos los cálculos de procesos y eventos del juego.

Ok, vamos a modificar la función xcompruebajuego() por la siguiente:

function xcompruebajuego()
	if(#aliens==0) then
		xnewlevel()
	end
end

La creación de un nuevo nivel la implementamos en la función xnewlevel():

function xnewlevel()
	player1.level = player1.level + 1
	print("Player1.level: " .. player1.level)
	if player1.level==cmodlastlvl then
		xyouwin()
	else
		xloadbackground(player1.level)
		print("bien hecho!")
		for i = 1,background.aliens,1
		do 
			 xnewalien(i)
		end
	end
end

Nuevo nivel y ganar la partida

Si el jugador llega al nivel definido por cmodlastlvl ha ganado, Llama a xyouwin() que cambia el valor de Player1.estado a 2.

function xyouwin()
	player1.estado = 2 -- 0 inicio, 1 juego, 2 ganado, 3 perdido	
end

Mientras no llegue al nivel final, crea un nuevo nivel con fondo, música y número de enemigos distintos

function xloadbackground(lvl)
	if lvl==1 then
		path="recursos/Backgrounds/blue.png"
		background.aliens = 3
	else		
		if lvl % cmodnewlvl == 0 then
			slvl = 1 + math.floor(lvl/cmodnewlvl)
			if slvl == 2 then
				-- niveles del 4 al 7
				path="recursos/Backgrounds/darkPurple.png"
			elseif slvl == 3 then
				-- niveles del 8 al 11
				path="recursos/Backgrounds/darkPurple2.png"
			elseif slvl == 4 then
				-- niveles del 12 al 15
				path="recursos/Backgrounds/purple.png"
			elseif slvl == 4 then
				-- niveles del 16 al 19
				path="recursos/Backgrounds/purple2.png"							
			end
			-- un enemigo más cada 4 niveles
			background.aliens = background.aliens + 1
		else
			return -- si no es ninguno de los niveles establecidos, sale de la funcion
		end
	end
	background.image = love.graphics.newImage(path)
	-- si sube de nivel cambia el fondo
	background.W = background.image:getWidth()
	background.H = background.image:getHeight()	
	background.sound=love.audio.newSource("recursos/musica/space1.mp3")
end

En esta función implementamos los cambios que queramos para cada nivel.

Perder la partida

Si nuestra nave sufre demasiados impactos habremos perdido la partida.

Para implementarlo en código utilizamos la variable player1.damage que contabiliza el daño sufrido por la nave, cuando el daño llega a player1.maxvida perdemos:

p.damage = p.damage + 1 -- incrementa contador de daño
if p.damage == p.maxvida then
  xyoulose()
end

Vamos a la función xcolisionesred() que controla los impactos de los laser enemigos en nuestra nave:

function xcolisionesred()
	-- comprobamos si alguno de los laserred impacta en player1
	-- para ello, tenemos que recorrer los lasersred
	p=player1
	for ldel, l in ipairs(lasers) do
		d=xdistancia(l, p)
		if(d-60<(p.R+l.R)) then -- debug cuando estan cerca
			if(d<(p.R+l.R)) then -- si la distancia entre objetos es menos que la suma de sus radios... BOOM!
				print("impacto!" .. d .. "; RadioA:" .. p.R .. ", RadioL:" ..l.R)
				p.damage = p.damage + 1 -- incrementa contador de daño
				-- activa explosion
					xnewexplosion(l)
				-- destruye el laser
				table.remove(lasers, ldel)
				if p.damage == p.maxvida then
					-- fin de la partida
					xyoulose()
				end
				break
			end
		end
	end
end

El flag que indica que hemos perdido es player1.estado=3.

function xyoulose()
	player1.estado = 3 -- 0 inicio, 1 juego, 2 ganado, 3 perdido	
end

Mensaje fin de partida

Hasta ahora hemos cambiado estados del Player, falta mostrar el mensaje de ganador o perdedor y detener el juego.

Si vamos a detener el juego no tiene sentido seguir realizando calculos y operaciones, por ello incluimos un if en love.update() para que quede así:

function love.update(dt)
	if player1.estado == 1 then
            ... lógica y operaciones del juego
        end
end
function love.update(dt)
	if player1.estado == 1 then -- solo update de juego si player1.estado == 1
		-- aqui ajustamos el tiempo
		dtR=dt*1
		if pause_down then
			return
		end		
		if left_down then
			xmoveplayer(-1,0,dtR)
		end
		if right_down then
			xmoveplayer(1,0,dtR)
		end
		xmovelasers(dtR)
		xmovealiens(dtR)
		xmovelasersred(dtR)
		xexplosiones(dtR)
		xcolisiones()
		xcolisionesred()
		xcompruebajuego()
	end
end

Modificamos la función xdrawpngs() donde dibujamos objetos. Si el estado es 1 muestra los elementos del juego en ejecución y si estado es 2 o 3, ganador o perdedor, muestra el mensaje correspondiente:

function xdrawpngs()
	if not pngs_visible then
		return
	end	
		-- Draw player
  love.graphics.draw(player1.image, player1.x, player1.y)  
  love.graphics.print(player1.kills .. " kills", 30, 10)
	love.graphics.print("lvl:" .. player1.level, WINDOWW-160, 10)
	love.graphics.print("damage:" .. player1.damage, 30, 30)
	-- 0 inicio, 1 juego, 2 ganado, 3 perdido
	if player1.estado == 1 then
		-- Draw enemies
		for _,a in pairs(aliens) do
			love.graphics.draw(a.image, a.x, a.y)
		end
		-- Draw explosiones
		for _,e in pairs(explos) do
			love.graphics.draw(e.image, e.x, e.y)
		end
		-- Draw lasers
		for _,l in pairs(player1.lasers) do
			love.graphics.draw(laserblue01.image, l.x, l.y)
		end
		for _,l in pairs(lasers) do
			love.graphics.draw(laserred01.image, l.x, l.y)
		end
	elseif player1.estado == 2 then
		love.graphics.print("Enhorabuena! HAS GANADO!", 100, WINDOWH/2-40)	
	elseif player1.estado == 3 then
		love.graphics.print("Lo siento! HAS PERDIDO!", 100, WINDOWH/2-40)	
	end	
end

Conclusiones e ideas

Hemos visto las bases suficientes para conocer love2d y crear juegos, a partir de aquí desarrolla y deja libre tu imaginación para hacer tu propio juego.

Es posible hacer ajustes y modificar parámetros para añadir logros/triunfos que nos motiven a seguir. Por ejemplo quitar un punto de daño cada X niveles o enemigos abatidos, incrementar potencia de fuego, evolucionar la nave, añadir nuevos enemigos, añadir objetos como asteroides, mostrar enemigos finales… todo lo que se os ocurra que de riqueza al juego hará que sea más divertido.

Felíz código!!

Temario completo del curso

function xInit()
	print("altaruru intro a love2d - Asteroids Clone")
	fullscreen=false
	love.window.setFullscreen(fullscreen)
	print("ESCAPE TO QUIT")	
  -- recupera dimensiones de la ventana
	WINDOWH = love.graphics.getHeight()
	WINDOWW = love.graphics.getWidth()		
	-- inicia aleatorios:
	math.randomseed(os.time()) -- random initialize	
	pause_down = false
	pngs_visible = true
	circle_visible = false
	cmodnewlvl = 4
	cmodlastlvl = 18
end

function xloadbackground(lvl)
	if lvl==1 then
		path="recursos/Backgrounds/blue.png"
		background.aliens = 3
	else		
		if lvl % cmodnewlvl == 0 then
			slvl = 1 + math.floor(lvl/cmodnewlvl)
			if slvl == 2 then
				-- niveles del 4 al 7
				path="recursos/Backgrounds/darkPurple.png"
			elseif slvl == 3 then
				-- niveles del 8 al 11
				path="recursos/Backgrounds/darkPurple2.png"
			elseif slvl == 4 then
				-- niveles del 12 al 15
				path="recursos/Backgrounds/purple.png"
			elseif slvl == 4 then
				-- niveles del 16 al 19
				path="recursos/Backgrounds/purple2.png"							
			end
			-- un enemigo más cada 4 niveles
			background.aliens = background.aliens + 1
		else
			return -- si no es ninguno de los niveles establecidos, sale de la funcion
		end
	end
	background.image = love.graphics.newImage(path)
	-- si sube de nivel cambia el fondo
	background.W = background.image:getWidth()
	background.H = background.image:getHeight()	
	background.sound=love.audio.newSource("recursos/musica/space1.mp3")
end

function xloadobjs()	
	font = love.graphics.newFont("recursos/Bonus/kenvector_future2.ttf", 30)

	love.graphics.setFont(font)
	background = {}
	xloadbackground(1)

	laserblue01 = {}
	laserblue01.image = love.graphics.newImage("recursos/PNG/Lasers/laserBlue01.png")
	laserblue01.W = laserblue01.image:getWidth()
	laserblue01.H = laserblue01.image:getHeight()
	laserblue01.sound=love.audio.newSource("recursos/Bonus/sfx_laser1.ogg", "static")

	laserred01 = {}
	laserred01.image = love.graphics.newImage("recursos/PNG/Lasers/laserRed03.png")
	laserred01.W = laserred01.image:getWidth()
	laserred01.H = laserred01.image:getHeight()
	laserred01.sound=love.audio.newSource("recursos/Bonus/sfx_laser2.ogg", "static")

	player1 = {}
	player1.image = love.graphics.newImage("recursos/PNG/playerShip1_blue.png")
	player1.W = player1.image:getWidth() -- ancho de la nave
	player1.H = player1.image:getHeight() -- alto de la nave
	player1.x=(WINDOWW-player1.W)/2 -- posicion inicial x
	player1.y=WINDOWH-player1.H-10 -- posicion inicial y
	xpuntocentral(player1)
	player1.R = player1.W*0.49 -- radio aprox para pruebas
	player1.v = 300 --velocidad de la nave
	player1.ctrshoot = -1 -- control de disparos
	player1.kills = 0
	player1.level = 1
	player1.damage = 0
	player1.maxvida = 5
	player1.estado = 0 -- 0 inicio, 1 juego, 2 ganado, 3 perdido
	-- tabla lasers
	player1.lasers={}
	-- tabla de explosiones
	explos={}	
	--laserimpactred = love.graphics.newImage("recursos/PNG/Lasers/laserRed10.png")
	--laserimpactblue = love.graphics.newImage("recursos/PNG/Lasers/laserBlue10.png")
	exploimg = love.graphics.newImage("recursos/PNG/Lasers/laserRed10.png")
	-- tabla lasers global, los lasers son indpendientes de los aliens, estos solo establecen el punto de partida
	lasers={}
	-- tabla de aliens
	aliens = {}
	alienimg = love.graphics.newImage("recursos/PNG/Enemies/enemyBlack1.png")
	-- crea aliens por defecto
	xnewalien(0)
	xnewalien(1)		
	xnewalien(2)
	-- inicia juego
	player1.estado = 1
end

function xnewexplosion(obj)
	explo1 = {}
	explo1.x=obj.x
	explo1.y=obj.y
	explo1.image = exploimg
	explo1.time=18 -- tiempo de "vida" d la explosion
	-- inserta en la tabla
	table.insert(explos, explo1)
end
function xnewalien(fila)
	alien1 = {}
	--alien1.image = love.graphics.newImage("recursos/PNG/Enemies/enemyBlack1.png")
	alien1.image = alienimg --love.graphics.newImage("recursos/PNG/Enemies/enemyBlack1.png")
	alien1.fila=fila;
	alien1.W = alien1.image:getWidth() -- ancho de la nave
	alien1.H = alien1.image:getHeight() -- alto de la nave
	alien1.x = math.random(1,WINDOWW-alien1.H) -- posicion inicial x
	alien1.y = 36 + (fila*alien1.H) -- posicion inicial y
	xpuntocentral(alien1)
	alien1.R = alien1.W*0.5 -- radio aprox para pruebas	
	alien1.v = 100 + (5*math.random(10,40)) --velocidad de la nave variable, entre 150 y 300 a intervalos de 5
	alien1.xdir = 1 -- afecta al sentido del movimiento en el eje X. 1derecha, -1izquierda
	alien1.ctrshoot=-1 -- control de disparos
	-- inserta en la tabla
	table.insert(aliens, alien1)
end

function xdistancia(obj1, obj2)	
	-- utiliza el punto central de cada objeto
	if obj1==nil or obj2==nil then
		return 1000
	end
	if obj1.cx==nil or obj2.cx==nil then
		return 1000
	end
	d=math.sqrt(math.pow(obj2.cx-obj1.cx,2)+math.pow(obj2.cy-obj1.cy,2))	
	return d
end
function xpuntocentral(obj)
	-- debe actualizar siempre que hay movimiento
	obj.cx = obj.x + obj.W/2 --punto central X para calculo de colisiones
	obj.cy = obj.y + obj.H/2 --punto central Y para calculo de colisiones	
end
function xpuntocentralL(obj)
	-- debe actualizar siempre que hay movimiento
	obj.cx = obj.x + obj.W/2 --punto central X para calculo de colisiones
	obj.cy = obj.y + obj.H*0.2 --punto central Y para calculo de colisiones	
end
function xcolisiones()
	-- comprobamos si alguno de nuestros laser impacta en los aliens
	-- para ello, tenemos que recorrer los lasers y los aliens
	for ldel, l in ipairs(player1.lasers) do
		--love.graphics.draw(laserblue01.image, l.x, l.y)
		for adel,a in ipairs(aliens) do
			--love.graphics.draw(a.image, a.x, a.y)
			d=xdistancia(l, a)
			if(d-60<(a.R+l.R)) then -- debug cuando estan cerca
				--print(a.fila .. ">" .. d .. " * " .. a.R+l.R)
				if(d<(a.R+l.R)) then -- si la distancia entre objetos es menos que la suma de sus radios... BOOM!
					print("impacto!" .. d .. "; RadioA:" .. a.R .. ", RadioL:" ..l.R)
					player1.kills = player1.kills + 1 -- incrementa contador de muertes
					-- explosion
					xnewexplosion(l)
					-- destruye el laser
					table.remove(player1.lasers, ldel)
					-- destruye alien
					table.remove(aliens, adel)
					break
				end
			end
		end
	end
end
function xcolisionesred()
	-- comprobamos si alguno de los laserred impacta en player1
	-- para ello, tenemos que recorrer los lasersred
	p=player1
	for ldel, l in ipairs(lasers) do
		d=xdistancia(l, p)
		if(d-60<(p.R+l.R)) then -- debug cuando estan cerca
			if(d<(p.R+l.R)) then -- si la distancia entre objetos es menos que la suma de sus radios... BOOM!
				print("impacto!" .. d .. "; RadioA:" .. p.R .. ", RadioL:" ..l.R)
				p.damage = p.damage + 1 -- incrementa contador de daño
				if p.damage == p.maxvida then
					-- fin de la partida:
					xyoulose()
				end
				-- activa explosion
					xnewexplosion(l)
				-- destruye el laser
				table.remove(lasers, ldel)
				break
			end
		end
	end
end
function xexplosiones(dt)
	for edel, e in ipairs(explos) do
		-- control del tiempo de vida de la explosión
		e.time=e.time-dt*150
		if e.time<0 then
			-- dstruye explosion
			table.remove(explos, edel)
		end
	end	
end
function love.load()
	xInit()
	xloadobjs()
	background.sound:setVolume(0.3) -- 30% del volumen general
	background.sound:setLooping(true)
	background.sound:play()
end

function xdrawpoints()
	if not circle_visible then
		return
	end
	wpoint=6
	-- Draw player
  love.graphics.setColor(80, 255, 0)
	love.graphics.rectangle("fill", player1.cx, player1.cy, wpoint, wpoint)
	love.graphics.circle("line", player1.cx, player1.cy, player1.R)
	-- Draw enemies
  love.graphics.setColor(200, 0, 200)
	for _,a in pairs(aliens) do
		love.graphics.rectangle("fill", a.cx, a.cy, wpoint, wpoint)
		love.graphics.circle("line", a.cx, a.cy, a.R)
	end
	-- Draw lasers
  love.graphics.setColor(190, 170, 0)
	for _,l in pairs(player1.lasers) do
		love.graphics.circle("line", l.cx, l.cy, l.R)
	end
	-- Draw lasers red
  love.graphics.setColor(240, 20, 20)
	for _,l in pairs(lasers) do		
		love.graphics.circle("line", l.cx, l.cy, l.R)
	end
end
function xdrawpngs()
	if not pngs_visible then
		return
	end	
		-- Draw player
  love.graphics.draw(player1.image, player1.x, player1.y)  
  love.graphics.print(player1.kills .. " kills", 30, 10)
	love.graphics.print("lvl:" .. player1.level, WINDOWW-160, 10)
	love.graphics.print("damage:" .. player1.damage, 30, 30)
	-- 0 inicio, 1 juego, 2 ganado, 3 perdido
	if player1.estado == 1 then
		-- Draw enemies
		for _,a in pairs(aliens) do
			love.graphics.draw(a.image, a.x, a.y)
		end
		-- Draw explosiones
		for _,e in pairs(explos) do
			love.graphics.draw(e.image, e.x, e.y)
		end
		-- Draw lasers
		for _,l in pairs(player1.lasers) do
			love.graphics.draw(laserblue01.image, l.x, l.y)
		end
		for _,l in pairs(lasers) do
			love.graphics.draw(laserred01.image, l.x, l.y)
		end
	elseif player1.estado == 2 then
		love.graphics.print("Enhorabuena! HAS GANADO!", 100, WINDOWH/2-40)	
	elseif player1.estado == 3 then
		love.graphics.print("Lo siento! HAS PERDIDO!", 100, WINDOWH/2-40)	
	end
	
end
function love.draw()
	-- Draw background  
  love.graphics.setColor(255, 255, 255)
  love.graphics.draw(background.image, 0, 0, 0, WINDOWW/background.W, WINDOWH/background.H)
	xdrawpngs()
	xdrawpoints()
end

function xmoveplayer(x, y, dt)
	player1.x = player1.x + (x * player1.v * dt)
	xpuntocentral(player1)
	if player1.ctrshoot>0 then
		player1.ctrshoot = player1.ctrshoot - 1000 * dt
	end
end	

-- Move Aliens
function xmovealiens(dt)
	for _,a in pairs(aliens) do				
		a.x = a.x + (a.xdir * a.v * dt)
		-- si llega a los bordes de la ventana cambia la dirección
		if a.x > WINDOWW-a.W then
			a.x = WINDOWW-a.W
			a.xdir = -1
		elseif a.x<0 then
			a.x = 0
			a.xdir = 1
		end
		if a.x > player1.x-20 and a.x<player1.x+20 then
			xalienshoot(a)		
		end
		if a.ctrshoot>0 then
			a.ctrshoot = a.ctrshoot - 1000 * dt
		end
		xpuntocentral(a)
	end
end	

function xmovelasers(dt)
	for ldel, laser in ipairs(player1.lasers) do
		if laser.y > -5 then
			laser.y= laser.y - 500 * dt
			xpuntocentralL(laser)
		else
			-- si llega a la parte superior de la ventana se elimina
			table.remove(player1.lasers, ldel)
			player1.ctrshoot=-1 -- temporal
		end
	end
end
function xmovelasersred(dt)
	-- estos lasers van d arriba a abajo
	for ldel, laser in ipairs(lasers) do
		if laser.y < WINDOWH then
			laser.y = laser.y + 500 * dt
			xpuntocentralL(laser)
		else
			-- si llega a la parte superior de la ventana se elimina
			table.remove(lasers, ldel)
		end
	end
end

function xplayershoot()
	if player1.ctrshoot < 0 then
		player1.ctrshoot=10
		laser={}
		laser.W = laserblue01.W
		laser.H = laserblue01.H
		laser.x= player1.x + (player1.W/2) - (laserblue01.W/2)
		laser.y= player1.y - laserblue01.H
		laser.R = laserblue01.W/2
		table.insert(player1.lasers, laser)
		-- reproduce sonido laser
		laserblue01.sound:stop()
		laserblue01.sound:play()
	end
end

function xalienshoot(a)
	-- el alien establece el punto de partida del laser, pero a partir de ese momento
	-- el laser es independiente, no se vincula al alien ya que aunque este ultimo sea destruido, el laser seguira existiendo.
	if a.ctrshoot < 0 then
		a.ctrshoot=200
		laser={}
		laser.W = laserred01.W
		laser.H = laserred01.H
		laser.x= a.x + (a.W/2) - (laserred01.W/2)
		laser.y= a.y + a.H
		laser.R = laserred01.W/2
		table.insert(lasers, laser)
		-- reproduce sonido laser
		laserred01.sound:stop()
		laserred01.sound:play()
	end
end

function love.update(dt)
	if player1.estado == 1 then -- solo update de juego si player1.estado == 1
		-- aqui ajustamos el tiempo
		dtR=dt*1
		if pause_down then
			return
		end		
		if left_down then
			xmoveplayer(-1,0,dtR)
		end
		if right_down then
			xmoveplayer(1,0,dtR)
		end
		xmovelasers(dtR)
		xmovealiens(dtR)
		xmovelasersred(dtR)
		xexplosiones(dtR)
		xcolisiones()
		xcolisionesred()
		xcompruebajuego()
	end
end

function xyouwin()
	player1.estado = 2 -- 0 inicio, 1 juego, 2 ganado, 3 perdido	
end
function xyoulose()
	player1.estado = 3 -- 0 inicio, 1 juego, 2 ganado, 3 perdido	
end

function xnewlevel()
	player1.level = player1.level + 1
	print("Player1.level: " .. player1.level)
	if player1.level==cmodlastlvl then
		xyouwin()
	else
		xloadbackground(player1.level)
		print("bien hecho!")
		for i = 1,background.aliens,1
		do 
			 xnewalien(i)
		end
	end
end
function xcompruebajuego()
	if(#aliens==0) then
		xnewlevel()
	end
end

function love.keypressed(key)
	if key == "p" then
		-- cada pulsación de P activa o desactiva la pausa
		pause_down = not pause_down
	end
	if key == "escape" then
		love.event.quit()
	end
	if pause_down then
		return
	end
  if key == 'left' then
		left_down = true
	elseif key == 'right' then
    	right_down = true
 	end
	if key == " " then
		xplayershoot()
	end
	if key == "7" then
		-- muestra oculta imagenes
		pngs_visible = not pngs_visible
	end
	if key == "8" then
		-- muestra oculta circulos detección colisiones
		circle_visible = not circle_visible
	end
	if key == "f" then
		fullscreen=not fullscreen
		love.window.setFullscreen(fullscreen)
	end
end

function love.keyreleased(key, unicode)
	if pause_down then
		return
	end
 	if key == 'left' then
		left_down = false
	elseif key == 'right' then
   	right_down = false
	end
	if key == " " then
		space_down = false
 	end
end

function love.quit()
  print("¡Hasta pronto!")
end

Deja tu comentario