Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Corona SDK
Code

Crea un juego Space Invaders en Corona: Implementando Gameplay

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Create a Space Invaders Game in Corona.
Create a Space Invaders Game in Corona: Project Setup
Create a Space Invaders Game in Corona: Finishing Gameplay

Spanish (Español) translation by CYC (you can also view the original English article)

Final product image
What You'll Be Creating

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.

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.

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.

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:

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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

También debemos asegurarnos de eliminar este detector de eventos cuando ya no lo necesitemos.

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.

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.

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.

Necesitamos asegurarnos de eliminar este detector de eventos cuando dejamos esta escena. Hacemos esto en la función scene:hide.

7. Invasores

Paso 1: Agregar invasores

En este paso, agregaremos los invasores. Comienza agregando el siguiente bloque de código.

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.

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.

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.

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.

Necesitamos asegurarnos de eliminar este detector de eventos cuando dejamos la escena. Hacemos esto en el método scene:hide.

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

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.