Seguimos con el tutorial de invaders. A estas alturas tenemos elementos suficientes para echar un rato jugando, sin embargo, hay detalles y elementos que marcan la diferencia entre un juego para un rato y otro que enganche y nos tenga horas jugando.
Algunos detalles son evidentes como las explosiones que veremos en esta entrada y otros que pueden pasar casi desapercibidos pero enriquecen la partida y la sensación de realismo, las sombras y efectos de luz, el polvo o estela de las naves, elementos visuales que completen la escena aun sin intervenir directamente, como asteroides de fondo…
Explosiones
En un juego como este donde el objetivo es acertar a los enemigos, realzar el impacto es fundamental. Cada impacto es un pequeño triunfo que nos acerca más a ganar la batalla, así que hagamos que se vean.

Las explosiones son objetos que se muestran en pantalla durante un breve espacio de tiempo y se desvanecen. Para crearlas usaremos una tabla donde cada elemento tendrá información de una explosión.
Si queréis echar un vistazo antes de continuar, este es el código completo de la entrada:
function xInit()
print("altaruru intro a love2d")
fullscreen=true
-- 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
end
function xloadbackground(lvl)
path="recursos/Backgrounds/blue.png"
background.image = love.graphics.newImage(path)
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
-- 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)
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
-- 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)
-- 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
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)
-- 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
function xcompruebajuego()
if(#aliens==0) then
xnewalien(0)
xnewalien(1)
xnewalien(3)
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
Creando explosiones
Al igual que el resto de elementos del juego, declaramos la tabla inicialmente vacía en la función xloadobjs():
explos={}
La explosión tendrá unas coordenadas donde mostrarse, una imagen, y el tiempo durante el cual se muestra en pantalla:
coordenada x: explo1.x
coordenada y: explo1.y
imagen a mostrar: explo1.image
tiempo de vida: explo1.time=18
Creando la explosión al hacer blanco
Las explosiones no se muestran al azar, debemos crearlas cuando un laser haga blanco. Las coordenadas de la explosión serán las mismas que las del laser en el momento del impacto.
Para iniciar cada nueva explosión vamos a crear una función que establezca los valores de la misma a partir del laser que la provoca, función xnewexplosion(<laser>), pasando como parámetro el laser:
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
Incluimos la llamada para crear la explosión en el momento del impacto, vamos alas funciones xcolisiones() y xcolisionesred() y dentro del if que controla si hay colisión incluimos la linea:
xnewexplosion(l)
Las funciones «colision» quedan así:
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
-- activa explosion
xnewexplosion(l)
-- destruye el laser
table.remove(lasers, ldel)
break
end
end
end
end
Ciclo de vida de la explosión
Perfecto!! ya creamos explosiones con cada impacto, ahora implementamos la lógica de su ciclo de vida, en este caso es tan sencillo como restar el tiempo transcurrido a su tiempo total de vida, si este llega a cero la explosión se elimina:
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
-- destruye explosion
table.remove(explos, edel)
end
end
end
Incluimos la llamada en love.update() para que se actualice en cada ciclo. Como siempre, el tiempo es variable, debemos contar con la variable DeltaTime, dtR.
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)
xmovelasersred(dtR)
xexplosiones(dtR)
xcolisiones()
xcolisionesred()
xcompruebajuego()
end
Mostrando la explosión
Por último, nada es visible hasta que lo mostramos, así que vamos a nuestra función de dibujo xdrawpngs(), recuerda que llamamos a esta última desde love.draw(), y añadimos las lineas siguientes:
-- Draw explosiones
for _,e in pairs(explos) do
love.graphics.draw(e.image, e.x, e.y)
end
Simplemente recorremos la tabla explos() y pintamos la imagen en sus coordenadas.

Conclusiones
Con las explosiones hemos visto elementos muy útiles para generar objetos, criaturas, animaciones y efectos autónomos con un ciclo de vida determinado.
Puedes descargar el ejemplo completo aquí:
Un saludo y feliz código!
Siguiente y última entrada de la serie Añadiendo niveles y dificultad al juego