Nuestra nave ya se enfrenta a un escuadrón enemigo y tiene buena potencia de fuego, sin embargo, los lasers atraviesan las naves enemigas como si fueran fantasmas! Para solucionarlo tenemos que implementar la detección de colisiones, o lo que es lo mismo, controlar cuando dos o más elementos del juego chocan entre si, por ejemplo, un láser al impactar contra una nave.

Antes de continuar debes saber que hay muchas formas de implementarla, más o menos rápidas, más o menos eficientes, todo dependerá del juego que estés haciendo, si es 2D o 3D, velocidad del juego, si es arcade, estrategia, shooter…

En la última entrada vimos como animar las naves enemigas, ¿te lo perdiste?

Para implementar la detección hace falta tirar un poco de mates, con lo divertidas que son las mates!!! xD (va en serio eh! ;P)

Dejo el código completo para que le eches un vistazo y ahora lo comentamos paso a paso:

function xInit()
	print("altaruru intro a love2d")
	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 = true
end

function xloadobjs()
	background = {}
	background.image = love.graphics.newImage('recursos/Backgrounds/blue.png')
	background.W = background.image:getWidth()
	background.H = background.image:getHeight()
	background.sound=love.audio.newSource("recursos/musica/space1.mp3")

	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")

	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/2 -- radio aprox para pruebas
	player1.v = 300 --velocidad de la nave
	player1.ctrshoot = -1 -- control de disparos
	-- tabla lasers
	player1.lasers={}

	-- tabla de aliens
	aliens = {}
	-- crea aliens por defecto
	xnewalien(0)
	xnewalien(1)		
	xnewalien(2)
end

function xnewalien(fila)
	alien1 = {}
	alien1.image = 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
	-- tabla lasers
	alien1.lasers={}
	-- inserta en la tabla
	table.insert(aliens, alien1)
end

function xdistancia(obj1, obj2)	
	-- utiliza el punto central de cada objeto
	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)
					-- destruye el laser
					table.remove(player1.lasers, ldel)
					-- destruye alien
					table.remove(aliens, adel)
					break
				end
			end
		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.rectangle("fill", l.cx, l.cy, wpoint, wpoint)
		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)
	-- Draw enemies
	for _,a in pairs(aliens) do
		love.graphics.draw(a.image, a.x, a.y)
	end
	-- Draw lasers
	for _,l in pairs(player1.lasers) do
		love.graphics.draw(laserblue01.image, l.x, l.y)
	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)
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
		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
	if player1.ctrshoot>0 then
		player1.ctrshoot = player1.ctrshoot - 1000 * dt
	end
end

function xplayershoot()
	if player1.ctrshoot < 0 then
		player1.ctrshoot=200
		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 love.update(dt)
	-- 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)
	xcolisiones()
	xcompruebajuego()
end

function xcompruebajuego()
	if(#aliens==0) then
		print("bien hecho!")
		xnewalien(0)
		xnewalien(1)
		xnewalien(3)
	end
end

function love.keypressed(key)
  if key == 'left' then
		left_down = true
	elseif key == 'right' then
    	right_down = true
 	end
	if key == " " then
		xplayershoot()
	end
	if key == "p" then
		-- cada pulsación de P activa o desactiva la pausa
		pause_down = not pause_down
	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 == "escape" then
		love.event.quit()
	end
end

function love.keyreleased(key, unicode)
 	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

Cómo implementar la detección de colisiones

La implementación que vamos a hacer aquí es la más simple, utilizaremos círculos, cuando dos círculos se toquen o se solapen… Colisión!

¿Significa entonces que ya no tengo naves ni lasers? pues si y no… nuestro algoritmo de detección ignora la imagen que representa cada objeto, solo sabrá de círculos.

Las siguientes imágenes representan el mismo instante de juego:

Lo que ve el jugador
Lo que «ve» el detector de colisiones.

Así es como nuestro algoritmo verá los objetos.

Superposición de vista del juego y detector de colisiones

Hacerlo con círculos es por lo simple del cálculo, no tanto para nosotros al desarrollar, sino como el número de operaciones a realizar por el programa. Cuantas menos operaciones más rápido y fluido.

A efectos de juego, hay colisión cuando se tocan dos círculos.

¿Como se que se están tocando dos círculos?

Cómo detectar colisión entre dos círculos

Aquí viene la magia de las mates, si la distancia entre el centro de cada circulo es menor que la suma de sus radios… chocan!

En código lo escribimos así:

if(d<(a.R+l.R)) then -- ... BOOM!
	print("impacto!" .. d .. "; RadioA:" .. a.R .. ", RadioL:" ..l.R)

Pero aun nos falta algo, hasta ahora tenemos coordenadas x,y del centro del circulo, pero necesitamos la distancia entre los centros!!!

Teorema de Pitágoras!

En un plano 2d, como es nuestro caso, la distancia entre dos puntos se calcula gracias al teorema de Pitágoras.

«El cuadrado de la hipotenusa es igual a la suma de los cuadrados de los catetos»: hipotenusa²=cateto²+cateto²

No vamos a entrar aquí en explicar el teorema pero si es bueno que sepáis que las mates tienen su aplicación en todo, sobre todo los videojuegos.

Vamos a implementar el teorema, creamos una función xdistancia() que nos va a devolver la distancia entre los puntos centrales de los objetos que les pasemos.

function xdistancia(obj1, obj2)	
	-- utiliza el punto central de cada objeto
	d=math.sqrt(math.pow(obj2.cx-obj1.cx,2)+math.pow(obj2.cy-obj1.cy,2))	
	return d
end

Y la distancia resultante es la que comparamos en if(d<(a.R+l.R))…

Ahora, solo queda, implementar las variables que almacenen los datos de coordenadas, distancia y colisión. Vamos allá!

Estructuras de datos en la detección de colisiones.

Todos los objetos en pantalla que puedan impactar guardar la siguiente info relativa a sí mismos:

  • Radio de la esfera que los contiene
  • Coordenadas x,y del centro de dicha esfera

Las coordenadas deben actualizarse continuamente durante el juego.

El Radio de los objetos se calcula una sola vez al inicio porque no cambia. su valor, ancho entre 2:

player1.R = player1.W/2 -- radio aprox para pruebas

Las coordenadas centrales en cambio están en continuo movimiento, crearemos dos funciones que se invocan cada vez que el objeto se mueve.

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

xpuntocentral() se utiliza en naves y xpuntocentralL para los lasers, la diferencia entre ellas es únicamente la posición de Y.

love.update(), ¿en que momento detectamos la colisión?

Como el resto de operaciones y procesos de calculo en el juego, la detección debe hacerse en el update().

Creamos la función xcolisiones() que llamaremos al final de update(). El motivo de la función en lugar de incrustar el código directamente en update() es tener el código ordenado y cada tarea fácilmente localizable.

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 el centro de los circulos es menos que la suma de sus radios... BOOM!
					print("impacto!" .. d .. "; RadioA:" .. a.R .. ", RadioL:" ..l.R)
					-- destruye el laser
					table.remove(player1.lasers, ldel)
					-- destruye alien
					table.remove(aliens, adel)
					break
				end
			end
		end
	end
end
function love.update(dt)
	-- 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)
	xcolisiones()
	xcompruebajuego()
end

Por último verás que hay una función xcompruebajuego(), en ella comprobamos si no hay más aliens y creamos un nuevo escuadrón para seguir luchando.

function xcompruebajuego()
	if(#aliens==0) then
		print("bien hecho!")
		xnewalien(0)
		xnewalien(1)
		xnewalien(3)
	end
end

Conclusiones…

Esto ya empieza a parecer un juego de verdad!

La entrada ha sido algo más técnica que las anteriores, espero haber sabido explicar los pasos, muchas veces es más sencillo hacerlo que expresarlo.

Cualquier duda y comentario ya sabéis.

Aquí os dejo los fuentes de esta entrada.

Un saludo y feliz código!

En la próxima entrada, como utilizar distintos fuentes de letra durante el juego.


Deja tu comentario