Crea un juego Space Invaders en Corona: Implementando Gameplay
Spanish (Español) translation by CYC (you can also view the original English article)



En la primera parte de esta serie, configuramos algunos valores predeterminados para el juego y sentamos las bases para la transición entre escenas. En esta parte, comenzaremos a implementar la jugabilidad del juego.
1. Una palabra sobre Metatables
El lenguaje de programación Lua no tiene un sistema de clase incorporado. Sin embargo, al usar la construcción metatable de Lua podemos emular un sistema de clase. Hay un buen ejemplo en el sitio web de Corona que muestra cómo implementar esto.
Una cosa importante a tener en cuenta es que los objetos Display
de Corona no se pueden establecer como metatabla. Esto tiene que ver con la forma en que el lenguaje C subyacente interactúa con ellos. Una forma sencilla de evitar esto es establecer el objeto Display
como una clave en una nueva tabla y luego poner esa tabla como la metatabla. Este es el enfoque que tomaremos en este tutorial.
Si lees el artículo anterior en el sitio web de Corona, habrás notado que el metamétodo __Index
se estaba utilizando en el metatabla. La forma en que funciona el metamétodo __Index
, es que cuando intentas acceder a un campo ausente en una tabla, desencadena que el intérprete busque un __Index
de metadato. Si el __Index
está allí, buscará el campo y proporcionará el resultado; de lo contrario, dará como resultado un nil
.
2. Implementando la clase PulsatingText
El juego tiene texto que crece y se contrae continuamente, creando un efecto de texto pulsante. Crearemos esta funcionalidad como un módulo para que podamos usarla a lo largo del proyecto. Además, al tenerlo como módulo, podemos usarlo en cualquier proyecto que requiera este tipo de funcionalidad.
Agrega lo siguiente al archivo pulsatingtext.lua que creaste en la primera parte de este tutorial. Asegúrate de que este código y todo el código de aquí en adelante se coloque arriba de donde estás devolviendo el objeto scene
.
1 |
local pulsatingText = {} |
2 |
local pulsatingText_mt = {__index = pulsatingText} |
3 |
|
4 |
function pulsatingText.new(theText,positionX,positionY,theFont,theFontSize,theGroup) |
5 |
local theTextField = display.newText(theText,positionX,positionY,theFont,theFontSize) |
6 |
|
7 |
local newPulsatingText = { |
8 |
theTextField = theTextField} |
9 |
theGroup:insert(theTextField) |
10 |
return setmetatable(newPulsatingText,pulsatingText_mt) |
11 |
end
|
12 |
|
13 |
function pulsatingText:setColor(r,b,g) |
14 |
self.theTextField:setFillColor(r,g,b) |
15 |
end
|
16 |
|
17 |
function pulsatingText:pulsate() |
18 |
transition.to( self.theTextField, { xScale=4.0, yScale=4.0, time=1500, iterations = -1} ) |
19 |
end
|
20 |
|
21 |
return pulsatingText |
Creamos la tabla principal pulsatingText
y la tabla que se usará como el metatable pulsatingText_mt
. En el nuevo método, creamos el objeto TextField
y lo agregamos a la tabla newPulsatingText
que se establecerá como la metatabla. A continuación, agregamos el objeto TextField
a group
que se pasó a través del parámetro, que será el grupo de la escena en el que crearemos una instancia de PulsatingText
.
Es importante asegurarse de que lo agreguemos al grupo de la escena para que se elimine cuando se elimine la escena. Finalmente, establecemos el metatable.
Tenemos dos métodos que acceden al objeto TextField
y realizan operaciones en sus propiedades. Uno establece el color utilizando el método setFillColor
y toma como parámetros los colores R, G y B como un número de 0 a 1. El otro usa la biblioteca Transition para hacer que el texto crezca y se reduzca. Esto aumenta el texto utilizando las propiedades xScale
y yScale
. Establece la propiedad de iteraciones en -1 hace que la acción se repita para siempre.
3. Usando la clase PulsatingText
Abre start.lua y agrega el siguiente código al método: scene:create
.
1 |
function scene:create(event) |
2 |
--SNIP-- |
3 |
local invadersText = pulsatingText.new("INVADERZ",display.contentCenterX,display.contentCenterY-200,"Conquest", 20,group) |
4 |
invadersText:setColor( 1, 1, 1 ) |
5 |
invadersText:pulsate() |
6 |
end
|
Creamos una nueva instancia de TextField
con la palabra "INVADERZ", establecemos su color y llamamos al método de pulsar. Observa cómo pasamos la variable group
como un parámetro para asegurar que el objeto TextField
se agregue a la jerarquía de vista de esta escena.
He incluido una fuente en las descargas llamada "Conquest" que tiene un aspecto futurista. Asegúrate de agregarlo a tu carpeta de proyecto si deseas usarlo. Descargué la fuente de dafont.com, que es un gran sitio web para encontrar fuentes personalizadas. Sin embargo, asegúrate de cumplir con la licencia que el autor de la fuente ha implementado.
Para usar la fuente, también debemos actualizar el archivo build.settings del proyecto. Echa un vistazo al archivo build.settings actualizado.
1 |
settings = { |
2 |
orientation = |
3 |
{
|
4 |
default ="portrait", |
5 |
supported = |
6 |
{
|
7 |
"portrait" |
8 |
},
|
9 |
},
|
10 |
iphone = |
11 |
{
|
12 |
plist= |
13 |
{
|
14 |
UIAppFonts = { |
15 |
"Conquest.ttf" |
16 |
}
|
17 |
},
|
18 |
},
|
19 |
}
|
Si pruebas el proyecto ahora, deberías ver que el texto se agregó a la escena y vibra como se esperaba.

4. Campo generador de estrella
Para hacer el juego un poco más interesante, se crea un campo de estrella móvil en el fondo. Para lograr esto, hacemos lo mismo que hicimos con la clase PulsatingText
y creamos un módulo. Crea un archivo llamado starfieldgenerator.lua y agrega lo siguiente a él:
1 |
local starFieldGenerator= {} |
2 |
local starFieldGenerator_mt = {__index = starFieldGenerator} |
3 |
|
4 |
function starFieldGenerator.new(numberOfStars,theView,starSpeed) |
5 |
local starGroup = display.newGroup() |
6 |
local allStars ={} -- Table that holds all the stars |
7 |
|
8 |
for i=0, numberOfStars do |
9 |
local star = display.newCircle(math.random(display.contentWidth), math.random(display.contentHeight), math.random(2,8)) |
10 |
star:setFillColor(1 ,1,1) |
11 |
starGroup:insert(star) |
12 |
table.insert(allStars,star) |
13 |
end
|
14 |
|
15 |
theView:insert(starGroup) |
16 |
|
17 |
local newStarFieldGenerator = { |
18 |
allStars = allStars, |
19 |
starSpeed = starSpeed |
20 |
}
|
21 |
return setmetatable(newStarFieldGenerator,starFieldGenerator_mt) |
22 |
end
|
23 |
|
24 |
|
25 |
function starFieldGenerator:enterFrame() |
26 |
self:moveStars() |
27 |
self:checkStarsOutOfBounds() |
28 |
end
|
29 |
|
30 |
|
31 |
function starFieldGenerator:moveStars() |
32 |
for i=1, #self.allStars do |
33 |
self.allStars[i].y = self.allStars[i].y+self.starSpeed |
34 |
end
|
35 |
|
36 |
end
|
37 |
function starFieldGenerator:checkStarsOutOfBounds() |
38 |
for i=1, #self.allStars do |
39 |
if(self.allStars[i].y > display.contentHeight) then |
40 |
self.allStars[i].x = math.random(display.contentWidth) |
41 |
self.allStars[i].y = 0 |
42 |
end
|
43 |
end
|
44 |
end
|
45 |
|
46 |
return starFieldGenerator |
Primero creamos la tabla principal starFieldGenerator
y la metatabla starFieldGenerator_mt
. En el nuevo método, tenemos una tabla allStars
que se usará para mantener una referencia a las estrellas que se crean en el ciclo for
. El número de iteraciones del ciclo for es igual a numberOfStars
y utilizamos el método newCircle
del objeto Display
para crear un círculo blanco.
Colocamos el círculo al azar dentro de los límites de la pantalla del juego y también le damos un tamaño aleatorio entre 2 y 8. Insertamos cada estrella en la tabla allStars
y las colocamos en la vista que se pasó como parámetro, que es la vista de la escena.
Establecemos allStars
y starSpeed
como claves en la tabla temporal y luego la asignamos como la metatabla. Necesitamos acceso a la tabla allStars
y propiedades starSpeed
cuando movemos las estrellas.
Utilizaremos dos métodos para mover las estrellas. El método starFieldGenerator:moveStars
hace el movimiento de las estrellas mientras que el método starFieldGenerator:checkStarsOutOfBounds
comprueba si las estrellas están fuera de los límites de la pantalla.
Si las estrellas están fuera del área de la pantalla de reproducción, genera una posición x
aleatoria para esa estrella en particular y establece la posición y
justo arriba de la parte superior de la pantalla. Al hacerlo, podemos reutilizar las estrellas y da la ilusión de una corriente interminable de estrellas.
Llamamos a estas funciones en el método starFieldGenerator:enterFrame
. Al establecer el método enterFrame
directamente en este objeto, podemos establecer este objeto como el contexto cuando agregamos el detector de eventos.
Agrega el siguiente bloque de código al método scene:create
en start.lua:
1 |
function scene:create(event) |
2 |
local group = self.view |
3 |
starGenerator = starFieldGenerator.new(200,group,5) |
4 |
startButton = display.newImage("new_game_btn.png",display.contentCenterX,display.contentCenterY+100) |
5 |
group:insert(startButton) |
6 |
end
|
Observa que hemos invocado el método starGenerator.new
cuando estamos agregando el startButton
. El orden en el que agrega cosas a la escena sí importa. Si tuviéramos que agregarlo después del botón de inicio, algunas de las estrellas habrían estado en la parte superior del botón.
El orden en que agrega cosas a la escena es el orden en que se mostrarán. Hay dos métodos de la clase Display
, toFront
y toBack
, que pueden cambiar este orden.
Si pruebas el juego ahora, deberías ver la escena plagada de estrellas aleatorias. Sin embargo, no se están moviendo. Necesitamos moverlos al método scene:show
. Agrega lo siguiente al método scene:show
de start.lua.
1 |
function scene:show(event) |
2 |
--SNIP-- |
3 |
if ( phase == "did" ) then |
4 |
startButton:addEventListener("tap",startGame) |
5 |
Runtime:addEventListener("enterFrame", starGenerator) |
6 |
end
|
7 |
end
|
Aquí agregamos el detector de eventos enterFrame
, que, si lo recuerdas, hace que las estrellas se muevan y comprueba si están fuera de los límites.
Cada vez que agregues un detector de eventos, debes asegurarte de que también lo estés eliminando en algún momento posterior del programa. El lugar para hacerlo en este ejemplo es cuando se elimina la escena. Agrega lo siguiente al evento scene:hide
.
1 |
unction scene:hide(event) |
2 |
local phase = event.phase |
3 |
if ( phase == "will" ) then |
4 |
startButton:removeEventListener("tap",startGame) |
5 |
Runtime:removeEventListener("enterFrame", starGenerator) |
6 |
end
|
7 |
end
|
Si pruebas el juego ahora, deberías ver las estrellas en movimiento y parecerá una interminable secuencia de estrellas. Una vez que agreguemos el jugador, también dará la ilusión de que el jugador se mueve a través del espacio.

5. Nivel de juego
Cuando presionas el botón startButton
, te lleva a la escena de nivel de juego, que es una pantalla en blanco en este momento. Arreglemos eso.
Paso 1: Variables locales
Agrega el siguiente fragmento de código a gamelevel.lua. Debes asegurarte de que este código y todo el código de este punto esté arriba de donde estás devolviendo el objeto scene
. Estas son las variables locales que necesitamos para el nivel del juego, la mayoría de las cuales son autoexplicativas.
1 |
local starFieldGenerator = require("starfieldgenerator") |
2 |
local pulsatingText = require("pulsatingtext") |
3 |
local physics = require("physics") |
4 |
local gameData = require( "gamedata" ) |
5 |
physics.start() |
6 |
local starGenerator -- an instance of the starFieldGenerator |
7 |
local player |
8 |
local playerHeight = 125 |
9 |
local playerWidth = 94 |
10 |
local invaderSize = 32 -- The width and height of the invader image |
11 |
local leftBounds = 30 -- the left margin |
12 |
local rightBounds = display.contentWidth - 30 - the right margin |
13 |
local invaderHalfWidth = 16 |
14 |
local invaders = {} -- Table that holds all the invaders |
15 |
local invaderSpeed = 5 |
16 |
local playerBullets = {} -- Table that holds the players Bullets |
17 |
local canFireBullet = true |
18 |
local invadersWhoCanFire = {} -- Table that holds the invaders that are able to fire bullets |
19 |
local invaderBullets = {} |
20 |
local numberOfLives = 3 |
21 |
local playerIsInvincible = false |
22 |
local rowOfInvadersWhoCanFire = 5 |
23 |
local invaderFireTimer -- timer used to fire invader bullets |
24 |
local gameIsOver = false; |
25 |
local drawDebugButtons = {} --Temporary buttons to move player in simulator |
26 |
local enableBulletFireTimer -- timer that enables player to fire |
Paso 2: Agregar un campo de estrella
Al igual que la escena anterior, esta escena también tiene un campo de estrellas en movimiento. Agrega lo siguiente a gamelevel.lua.
1 |
function scene:create(event) |
2 |
local group = self.view |
3 |
starGenerator = starFieldGenerator.new(200,group,5) |
4 |
end
|
Estamos agregando un campo de estrellas a la escena. Como antes, necesitamos hacer que las estrellas se muevan, lo que hacemos en el método scene:show
.
1 |
function scene:show(event) |
2 |
local phase = event.phase |
3 |
local previousScene = composer.getSceneName( "previous" ) |
4 |
composer.removeScene(previousScene) |
5 |
local group = self.view |
6 |
if ( phase == "did" ) then |
7 |
Runtime:addEventListener("enterFrame", starGenerator) |
8 |
end
|
9 |
end
|
Estamos eliminando la escena anterior y agregando el detector de eventos enterFrame
. Como mencioné anteriormente, cada vez que agregues un detector de eventos, debes asegurarte de eliminarlo eventualmente. Hacemos esto en el método scene:hide
.
1 |
function scene:hide(event) |
2 |
local phase = event.phase |
3 |
local group = self.view |
4 |
if ( phase == "will" ) then |
5 |
Runtime:removeEventListener("enterFrame", starGenerator) |
6 |
end
|
7 |
end
|
Por último, debemos agregar los oyentes para los métodos create
, show
y hide
. Si ejecutas la aplicación ahora, debes tener un campo de estrella móvil.
1 |
scene:addEventListener( "create", scene ) |
2 |
scene:addEventListener( "show", scene ) |
3 |
scene:addEventListener( "hide", scene ) |
Paso 3: Agregar el jugador
En este paso, agregaremos al jugador a la escena y lo moveremos. Este juego usa el acelerómetro para mover al jugador. También utilizaremos una forma alternativa de mover el reproductor en el simulador agregando botones a la escena. Agrega el siguiente fragmento de código a gamelevel.lua.
1 |
function setupPlayer() |
2 |
local options = { width = playerWidth,height = playerHeight,numFrames = 2} |
3 |
local playerSheet = graphics.newImageSheet( "player.png", options ) |
4 |
local sequenceData = { |
5 |
{ start=1, count=2, time=300, loopCount=0 } |
6 |
}
|
7 |
player = display.newSprite( playerSheet, sequenceData ) |
8 |
player.name = "player" |
9 |
player.x=display.contentCenterX- playerWidth /2 |
10 |
player.y = display.contentHeight - playerHeight - 10 |
11 |
player:play() |
12 |
scene.view:insert(player) |
13 |
local physicsData = (require "shapedefs").physicsData(1.0) |
14 |
physics.addBody( player, physicsData:get("ship")) |
15 |
player.gravityScale = 0 |
16 |
end
|
El jugador es una instancia de SpriteObject
. Al hacer que el jugador sea un sprite en lugar de una imagen regular, podemos animarlo. El jugador tiene dos imágenes separadas, una con el propulsor activado y otra con el propulsor apagado.
Al cambiar entre las dos imágenes, creamos la ilusión de un empuje interminable. Lo lograremos con una hoja de imagen, que es una imagen grande compuesta por varias imágenes más pequeñas. Al recorrer las distintas imágenes, puedes crear una animación.
La tabla de opciones contiene el ancho, alto y NumFrames
de las imágenes individuales en la imagen más grande. La variable numFrames
contiene el valor de la cantidad de imágenes más pequeñas. PlayerSheet
es una instancia del objeto ImageSheet
, que toma como parámetros la imagen y la tabla de opciones.
La variable sequenceData
es utilizada por la instancia de SpriteObject
, la clavestart
es la imagen en la que desea iniciar la secuencia o animación, mientras que la clave count
es la cantidad total de imágenes que hay en la animación. La clave time
es el tiempo que tardará la animación en reproducirse y la claveloopCount
es la cantidad de veces que deseas que la animación se reproduzca o repita. Al establecer loopCount
en 0
, se repetirá para siempre.
Por último, crea la instancia de SpriteObject
al pasar en la instancia de ImageSheet
y sequenceData
.
Le damos al jugador una clave name
, que se usará para identificarlo más tarde. También establecemos sus coordenadas x
e y
, invocamos su método play
e insertamos en la vista de la escena.
Utilizaremos el motor de física incorporado de Corona, que usa el popular motor Box2d debajo del capó, para detectar colisiones entre objetos. La detección de colisión predeterminada usa un método de caja de delimitación para detectar colisiones, lo que significa que coloca un cuadro alrededor del objeto y lo usa para detectar colisiones. Esto funciona bastante bien para objetos rectangulares o círculos mediante el uso de una propiedad radio
, pero para objetos con formas extrañas no funciona tan bien. Echa un vistazo a la imagen de abajo para ver a qué me refiero.



Notarás que aunque el láser no esté tocando la nave, aún se registra como una colisión. Esto se debe a que está colisionando con el cuadro delimitador alrededor de la imagen.
Para superar esta limitación, puedes pasar un parámetro shape
. El parámetro de forma es una tabla de pares de coordenadas x
e y
, donde cada par define un punto de vértice para la forma. Estas coordenadas de parámetros de forma pueden ser bastante difíciles de entender a mano, dependiendo de la complejidad de la imagen. Para superar esto, utilizo un programa llamado PhysicsEditor.
La variable physicsData
es el archivo que se exportó desde PhysicsEditor
. Llamamos al método addBody
del motor de física, pasando el jugador y la variable physicsData
. El resultado es que la detección de colisión utilizará los límites reales de la nave espacial en lugar de utilizar la detección de colisiones de cuadro delimitador. La imagen de abajo aclara esto.



Puedes ver que, aunque el láser está dentro del cuadro delimitador, no se activa ninguna colisión. Solo cuando toque el borde del objeto se registrará una colisión.
Por último, establecemos gravityScale
en 0
en el jugador ya que no queremos que se vea afectada por la gravedad.
Ahora, invoca setupPlayer
en el método scene:create
.
1 |
function scene:create(event) |
2 |
local group = self.view |
3 |
starGenerator = starFieldGenerator.new(100,group,5) |
4 |
setupPlayer() |
5 |
end
|
Si ejecutas el juego ahora, deberías ver al jugador agregado a la escena con su propulsor enganchado y activado.

Paso 4: Mover el jugador
Como se mencionó anteriormente, moveremos al jugador usando el acelerómetro. Agrega el siguiente código a gamelevel.lua.
1 |
local function onAccelerate(event) |
2 |
player.x = display.contentCenterX + (display.contentCenterX * (event.xGravity*2)) |
3 |
end
|
4 |
system.setAccelerometerInterval( 60 ) |
5 |
Runtime:addEventListener ("accelerometer", onAccelerate) |
Se llamará a la función onAccelerate
cada vez que se active el intervalo del acelerómetro. Está configurado para disparar 60 veces por segundo. Es importante saber que el acelerómetro puede ser un gran gasto en la batería del dispositivo. En otras palabras, si no lo estás usando durante un período de tiempo prolongado, sería conveniente eliminar el detector de eventos de él.
Si prueba en un dispositivo, deberías poder mover el jugador inclinando el dispositivo. Sin embargo, esto no funciona cuando se prueba en el simulador. Para remediar esto, crearemos algunos botones temporales.
Paso 5: Botones de depuración
Agrega el siguiente código para dibujar los botones de depuración en la pantalla.
1 |
function drawDebugButtons() |
2 |
local function movePlayer(event) |
3 |
if(event.target.name == "left") then |
4 |
player.x = player.x - 5 |
5 |
elseif(event.target.name == "right") then |
6 |
player.x = player.x + 5 |
7 |
end
|
8 |
end
|
9 |
local left = display.newRect(60,700,50,50) |
10 |
left.name = "left" |
11 |
scene.view:insert(left) |
12 |
local right = display.newRect(display.contentWidth-60,700,50,50) |
13 |
right.name = "right" |
14 |
scene.view:insert(right) |
15 |
left:addEventListener("tap", movePlayer) |
16 |
right:addEventListener("tap", movePlayer) |
17 |
end
|
Este código utiliza el método newRect
de Display
para dibujar dos rectángulos en la pantalla. A continuación, les agregamos un oyente de "tocar y pegar" que llama a la función local movePlayer
.
6. Disparar balas
Paso 1: Agregar y mover viñetas
Cuando el usuario toca la pantalla, la nave del jugador disparará una bala. Limitaremos la frecuencia con la que el usuario puede disparar una bala usando un temporizador simple. Echa un vistazo a la implementación de la función firePlayerBullet
.
1 |
function firePlayerBullet() |
2 |
if(canFireBullet == true)then |
3 |
local tempBullet = display.newImage("laser.png", player.x, player.y - playerHeight/ 2) |
4 |
tempBullet.name = "playerBullet" |
5 |
scene.view:insert(tempBullet) |
6 |
physics.addBody(tempBullet, "dynamic" ) |
7 |
tempBullet.gravityScale = 0 |
8 |
tempBullet.isBullet = true |
9 |
tempBullet.isSensor = true |
10 |
tempBullet:setLinearVelocity( 0,-400) |
11 |
table.insert(playerBullets,tempBullet) |
12 |
local laserSound = audio.loadSound( "laser.mp3" ) |
13 |
local laserChannel = audio.play( laserSound ) |
14 |
audio.dispose(laserChannel) |
15 |
canFireBullet = false |
16 |
|
17 |
else
|
18 |
return
|
19 |
end
|
20 |
local function enableBulletFire() |
21 |
canFireBullet = true |
22 |
end
|
23 |
timer.performWithDelay(750,enableBulletFire,1) |
24 |
end
|
Primero verificamos si el usuario puede disparar una bala. Luego creamos un punto y le damos una propiedad name
para que podamos identificarlo más tarde. Lo agregamos como un cuerpo de física y le damos el tipo dinámico, ya que se moverá con una cierta velocidad.
Establecemos el valor de gravityScale
en 0, porque no queremos que se vea afectado por la gravedad, establecemos la propiedad isBullet
en verdadero y lo configuramos como sensor para la detección de colisiones. Por último, llamamos a setLinearVelocity
para que la viñeta se mueva verticalmente. Puedes encontrar más información sobre estas propiedades en la documentación de los cuerpos de física.
Cargamos y reproducimos un sonido, y luego liberamos inmediatamente la memoria asociada con ese sonido. Es importante liberar la memoria de los objetos de sonido cuando ya no están en uso. Configuramos canFireBullet
en false
e iniciamos un temporizador que lo restablece a verdadero después de un corto tiempo.
Ahora necesitamos agregar el escucha de tap
al Runtime
. Esto es diferente de agregar un escucha de tap a un objeto individual. No importa dónde toques en la pantalla, el oyente Runtime
se dispara. Esto se debe a que Runtime
es el objeto global para los oyentes.
1 |
function scene:show(event) |
2 |
--SNIP-- |
3 |
if ( phase == "did" ) then |
4 |
Runtime:addEventListener("enterFrame", starGenerator) |
5 |
Runtime:addEventListener("tap", firePlayerBullet) |
6 |
end
|
7 |
end
|
También debemos asegurarnos de eliminar este detector de eventos cuando ya no lo necesitemos.
1 |
function scene:hide(event) |
2 |
if ( phase == "will" ) then |
3 |
Runtime:removeEventListener("enterFrame", starGenerator) |
4 |
Runtime:removeEventListener("tap", firePlayerBullet) |
5 |
end
|
6 |
end
|
Si pruebas el juego y tocas la pantalla, se debe agregar una viñeta a la pantalla y moverla a la parte superior del dispositivo. Sin embargo hay un problema. Una vez que la bala se mueve fuera de la pantalla, se mantienen en movimiento para siempre. Esto no es muy útil para la memoria del juego. Imagina tener cientos de balas fuera de la pantalla, moviéndose al infinito. Tomaría recursos innecesarios. Arreglaremos este problema en el próximo paso.
Paso 2: Quitar viñetas
Cada vez que se crea una viñeta, se almacena en la tabla playerBullets
. Esto hace que sea fácil hacer referencia a cada viñeta y verificar sus propiedades. Lo que haremos es recorrer la tabla playerBullets
, verificar su propiedad y, si está fuera de la pantalla, eliminarla de la pantalla y de la tabla playerBullet
.
1 |
function checkPlayerBulletsOutOfBounds() |
2 |
if(#playerBullets > 0)then |
3 |
for i=#playerBullets,1,-1 do |
4 |
if(playerBullets[i].y < 0) then |
5 |
playerBullets[i]:removeSelf() |
6 |
playerBullets[i] = nil |
7 |
table.remove(playerBullets,i) |
8 |
end
|
9 |
end
|
10 |
end
|
11 |
end
|
Un punto importante a tener en cuenta es que estamos recorriendo la tabla playersBullet
en orden inverso. Si tuviéramos que recorrer la tabla de forma normal, cuando elimináramos un objeto, arrojaría el índice y provocaría un error de procesamiento. Al recorrer la tabla en orden inverso, el objeto ya se ha procesado. También es importante tener en cuenta que cuando eliminas un objeto de Display
, debe establecerse en nil
.
Ahora necesitamos un lugar para llamar a esta función. La forma más común de hacer esto es crear un bucle de juego. Si no estás familiarizado con el concepto de bucle del juego, debes leer este breve artículo de Michael James Williams. Implementaremos el bucle del juego en el siguiente paso.
Paso 3: Crea el bucle del juego
Agrega el siguiente código a gamelevel.lua para comenzar.
1 |
function gameLoop() |
2 |
checkPlayerBulletsOutOfBounds() |
3 |
end
|
Necesitamos llamar a esta función repetidamente durante el tiempo que el juego se está ejecutando. Haremos esto usando el evento enterFrame
del Runtime
. Agrega lo siguiente en la función scene:show
.
1 |
function scene:show(event) |
2 |
--SNIP-- |
3 |
if ( phase == "did" ) then |
4 |
Runtime:addEventListener("enterFrame", gameLoop) |
5 |
Runtime:addEventListener("enterFrame", starGenerator) |
6 |
Runtime:addEventListener("tap", firePlayerBullet) |
7 |
end
|
8 |
end
|
Necesitamos asegurarnos de eliminar este detector de eventos cuando dejamos esta escena. Hacemos esto en la función scene:hide
.
1 |
function scene:hide(event) |
2 |
if ( phase == "will" ) then |
3 |
Runtime:removeEventListener("enterFrame", gameLoop) |
4 |
Runtime:removeEventListener("enterFrame", starGenerator) |
5 |
Runtime:removeEventListener("tap", firePlayerBullet) |
6 |
end
|
7 |
end
|
7. Invasores
Paso 1: Agregar invasores
En este paso, agregaremos los invasores. Comienza agregando el siguiente bloque de código.
1 |
function setupInvaders() |
2 |
local xPositionStart =display.contentCenterX - invaderHalfWidth - (gameData.invaderNum *(invaderSize + 10)) |
3 |
local numberOfInvaders = gameData.invaderNum *2+1 |
4 |
for i = 1, gameData.rowsOfInvaders do |
5 |
for j = 1, numberOfInvaders do |
6 |
local tempInvader = display.newImage("invader1.png",xPositionStart + ((invaderSize+10)*(j-1)), i * 46 ) |
7 |
tempInvader.name = "invader" |
8 |
if(i== gameData.rowsOfInvaders)then |
9 |
table.insert(invadersWhoCanFire,tempInvader) |
10 |
end
|
11 |
physics.addBody(tempInvader, "dynamic" ) |
12 |
tempInvader.gravityScale = 0 |
13 |
tempInvader.isSensor = true |
14 |
scene.view:insert(tempInvader) |
15 |
table.insert(invaders,tempInvader) |
16 |
end
|
17 |
end
|
18 |
end
|
Dependiendo de en qué nivel esté el jugador, las filas contendrán un número diferente de invasores. Establecemos cuántas filas crear cuando agregamos la clave rowsOfInvaders
a la tabla gameData
(3
). El invaderNum
se usa para realizar un seguimiento del nivel en el que estamos, pero también se usa en algunos cálculos.
Para obtener la posición x
inicial para el invasor, restamos la mitad del ancho del invasor desde el centro de la pantalla. Luego restamos lo que sea igual a (invaderNum * invaderSize + 10
). Hay un desplazamiento de diez píxeles entre cada invasor, por lo que estamos agregando al invaderSize
. Eso podría parecer un poco confuso, así que tómate tu tiempo para entenderlo.
Determinamos cuántos invasores hay por fila tomando invaderNum * 2
y agregando 1 a él. Por ejemplo, en el primer nivel, invaderNum
es 1
, por lo que tendremos tres invasores por fila (1 * 2 + 1
). En el segundo nivel, habrá cinco invasores por fila, (2 * 2 + 1
), etc.
Usamos bucles anidados para configurar las filas y columnas, respectivamente. En el segundo bucle for, creamos el invasor. Le damos una propiedad name
para que podamos consultarlo más tarde. Si i
es igual a gameData.rowsOfInvaders
, agregamos el invasor a la tabla invadersWhoCanFire
. Esto asegura que todos los invasores en la fila inferior comiencen como capaces de disparar balas. Pusimos la física de la misma manera que hicimos con el jugador antes, e insertamos el invasor en la escena y en la mesa de los invasores para que podamos consultarlo más tarde.
Paso 2: Mover invasores
En este paso, moveremos a los invasores. Usaremos gameLoop
para verificar la posición de los invasores e invertir su dirección si es necesario. Agrega el siguiente bloque de código para comenzar.
1 |
function moveInvaders() |
2 |
local changeDirection = false |
3 |
for i=1, #invaders do |
4 |
invaders[i].x = invaders[i].x + invaderSpeed |
5 |
if(invaders[i].x > rightBounds - invaderHalfWidth or invaders[i].x < leftBounds + invaderHalfWidth) then |
6 |
changeDirection = true; |
7 |
end
|
8 |
end
|
9 |
if(changeDirection == true)then |
10 |
invaderSpeed = invaderSpeed*-1 |
11 |
for j = 1, #invaders do |
12 |
invaders[j].y = invaders[j].y+ 46 |
13 |
end
|
14 |
changeDirection = false; |
15 |
end
|
16 |
end
|
Recorrimos los invasores y cambiamos su posición x
por el valor almacenado en la variable invaderSpeed
. Vemos si el invasor está fuera de límites al verificar leftBounds
y rightBounds
, que configuramos anteriormente.
Si un invasor está fuera de límites, establecemos changeDirection
en true
. Si changeDirection
se establece en true
, negamos la variable invaderSpeed
, movemos los invasores hacia abajo en el eje y en 16 píxeles y restablecemos la variable changeDirection
a false
.
Invocamos la función moveInvaders
en la función gameLoop
.
1 |
function gameLoop() |
2 |
checkPlayerBulletsOutOfBounds() |
3 |
moveInvaders() |
4 |
end
|
8. Detección de colisiones
Ahora que tenemos algunos invasores en pantalla y en movimiento, podemos verificar las colisiones entre cualquiera de las balas del jugador y los invasores. Realizamos esta comprobación en la función onCollision
.
1 |
function onCollision(event) |
2 |
local function removeInvaderAndPlayerBullet(event) |
3 |
local params = event.source.params |
4 |
local invaderIndex = table.indexOf(invaders,params.theInvader) |
5 |
local invadersPerRow = gameData.invaderNum *2+1 |
6 |
if(invaderIndex > invadersPerRow) then |
7 |
table.insert(invadersWhoCanFire, invaders[invaderIndex - invadersPerRow]) |
8 |
end
|
9 |
params.theInvader.isVisible = false |
10 |
physics.removeBody( params.theInvader ) |
11 |
table.remove(invadersWhoCanFire,table.indexOf(invadersWhoCanFire,params.theInvader)) |
12 |
|
13 |
if(table.indexOf(playerBullets,params.thePlayerBullet)~=nil)then |
14 |
physics.removeBody(params.thePlayerBullet) |
15 |
table.remove(playerBullets,table.indexOf(playerBullets,params.thePlayerBullet)) |
16 |
display.remove(params.thePlayerBullet) |
17 |
params.thePlayerBullet = nil |
18 |
end
|
19 |
end
|
20 |
|
21 |
if ( event.phase == "began" ) then |
22 |
if(event.object1.name == "invader" and event.object2.name == "playerBullet")then |
23 |
local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1) |
24 |
tm.params = {theInvader = event.object1 , thePlayerBullet = event.object2} |
25 |
end
|
26 |
if(event.object1.name == "playerBullet" and event.object2.name == "invader") then |
27 |
local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1) |
28 |
tm.params = {theInvader = event.object2 , thePlayerBullet = event.object1} |
29 |
end
|
30 |
end
|
31 |
end
|
Hay dos maneras de hacer la detección de colisión usando el motor de física incorporado de Corona. Una forma es registrarse para la colisión en los objetos mismos. La otra forma es escuchar globalmente. Usamos el enfoque global en este tutorial.
En el método onCollision
, verificamos las propiedades de los nombres de los objetos, establecemos un pequeño retraso e invocamos la función removeInvaderAndPlayerBullet
. Debido a que no sabemos a qué eventos event.object1
y event.object2
apuntarán, debemos verificar ambas situaciones, por lo tanto, las dos sentencias if
opuestas.
Enviamos algunos parámetros con el temporizador para que podamos identificar playerBullet
y al invasor dentro de la función removePlayerAndBullet
. Siempre que modifiques las propiedades de un objeto en una prueba de colisión, debes aplicar un pequeño retraso antes de hacerlo. Esta es la razón del corto temporizador.
Dentro de la función removeInvaderAndPlayerBullet
, obtenemos una referencia a la clave params
. Luego obtenemos el índice del invasor dentro de la tabla de invasores. A continuación, determinamos cuántos invasores hay por fila. Si este número es mayor que invadersPerRow
, determinamos qué invasor agregar a la tabla invadersWhoCanFire
. La idea es que cualquier invasor que sea golpeado, el invasor en la misma columna una fila puede disparar ahora.
Luego configuramos el invasor para que no sea visible, eliminemos su cuerpo del motor de física y lo eliminamos de la tabla invadersWhoCanFire
.
Quitamos la viñeta del motor de física, la retiramos de la mesa playerBullets
, la retiramos de la pantalla y la configuramos en cero para asegurarnos de que esté marcada para la recolección de basura.
Para que todo esto funcione, debemos escuchar los eventos de colisión. Agrega el siguiente código al método scene:show
.
1 |
function scene:show(event) |
2 |
local phase = event.phase |
3 |
local previousScene = composer.getSceneName( "previous" ) |
4 |
composer.removeScene(previousScene) |
5 |
local group = self.view |
6 |
if ( phase == "did" ) then |
7 |
Runtime:addEventListener("enterFrame", gameLoop) |
8 |
Runtime:addEventListener("enterFrame", starGenerator) |
9 |
Runtime:addEventListener("tap", firePlayerBullet) |
10 |
Runtime:addEventListener( "collision", onCollision ) |
11 |
end
|
12 |
end
|
Necesitamos asegurarnos de eliminar este detector de eventos cuando dejamos la escena. Hacemos esto en el método scene:hide
.
1 |
function scene:hide(event) |
2 |
if ( phase == "will" ) then |
3 |
Runtime:removeEventListener("enterFrame", starGenerator) |
4 |
Runtime:removeEventListener("tap", firePlayerBullet) |
5 |
Runtime:removeEventListener("enterFrame", gameLoop) |
6 |
Runtime:removeEventListener( "collision", onCollision ) |
7 |
end
|
8 |
end
|
Si pruebas el juego ahora, deberías ser capaz de disparar una bala, golpear a un invasor, y tener tanto la bala como el invasor eliminados de la escena.
Conclusión
Esto cierra esta parte de la serie. En la siguiente y última parte de esta serie, haremos que los invasores disparen balas, asegurarnos de que el jugador pueda morir y maneje el juego tanto como en niveles nuevos. Espero verte allí.