Corona SDK: Construye un defensor de los monos
Spanish (Español) translation by Esther (you can also view the original English article)
En este tutorial, vamos a crear un juego llamado Monkey Defender utilizando el Corona SDK. Este juego servirá como una gran base para un montón de diferentes géneros incluyen juegos de estilo de defensa. Así que, ¡comencemos!
Resumen del proyecto
En esta versión de Monkey Defender, el jugador tendrá que defender al mono disparando granadas de mono a las naves espaciales entrantes. Cada vez que el jugador acierte a una nave espacial enemiga, su puntuación aumentará en uno. Si no consiguen golpear la nave espacial enemiga antes de que llegue al mono, perderán un plátano, o una vida. Cuando el jugador se quede sin plátanos, ¡se acabó el juego!
Este juego fue construido con el Corona SDK y aquí están algunas de las cosas que aprenderás:
- Cómo utilizar el Storyboard
- Cómo utilizar los widgets
- Cómo girar objetos en función del tacto
- Cómo utilizar la colisión de Corona
- Cómo crear un juego completo con el SDK de Corona
Requisitos previos de la tutoría
Para poder utilizar este tutorial, necesitarás tener el Corona SDK instalado en tu ordenador. Si no tienes el SDK, dirígete a http://www.coronalabs.com para crear una cuenta gratuita y descargar el software gratuito.
Para descargar los gráficos que utilicé para el juego, por favor descarga los archivos fuente adjuntos a este post. Los gráficos de este juego provienen de www.opengameart.org y www.vickiwenderlich.com. El gráfico de fondo viene de Sauer2 en Open Game Art y el resto de los gráficos vienen de Vicki Wenderlich. Si decides publicar este juego con estos gráficos, por favor asegúrate de acreditar a ambos artistas.
1. Construir la configuración
El primer paso para construir nuestro juego, Monkey Defender, es crear un nuevo archivo llamado build.settings y colocarlo dentro de la carpeta del proyecto. El archivo build.settings maneja todas las propiedades de tiempo de construcción dentro de nuestra aplicación. Para nuestro juego, solo tenemos que preocuparnos por la orientación del juego, y solo vamos a permitir el modo horizontal.
settings = { orientation = { default = "landscapeRight", supported = { "landscapeRight", "landscapeLeft"} }, }
2. Configuración del tiempo de ejecución
A continuación, crearemos otro archivo llamado config.lua y lo colocaremos dentro de la carpeta del proyecto. El archivo config.lua maneja todas nuestras configuraciones de tiempo de ejecución tales como la altura, el ancho, el tipo de escala y la velocidad de los cuadros. Para nuestro juego, vamos a configurar nuestra aplicación para que sea de 320x480, esté en un buzón y tenga 30 cuadros por segundo.
application = { content = { width = 320, height = 480, scale = "letterBox", fps = 30, }, }
3. Construyendo main.lua
Ahora que tenemos nuestro proyecto configurado, podemos seguir adelante con la creación de main.lua. El archivo main.lua es el punto de partida de cada aplicación construida con el Corona SDK, así que crea un nuevo archivo llamado main.lua y muévelo a tu carpeta de proyecto. Dentro de nuestro archivo main.lua, ocultaremos la barra de estado, añadiremos algunas variables globales y utilizaremos la función Storyboard de Corona para gestionar nuestras escenas.
-- hide the status bar display.setStatusBar( display.HiddenStatusBar ) -- Set up some global variables for our game screenW, screenH, halfW, halfH = display.contentWidth, display.contentHeight, display.contentWidth*0.5, display.contentHeight*0.5 -- include the Corona "storyboard" module local storyboard = require "storyboard" -- load menu screen storyboard.gotoScene( "menu" )
4. Construir el menú
Con nuestro archivo main.lua configurado, vamos a pasar a nuestro menú. El menú para este juego será simple. Mostrará el título del juego y un botón para iniciar el juego.
Paso 1
Para empezar, crea un nuevo archivo llamado menu.lua y colócalo en la carpeta de tu proyecto. La primera adición a este archivo es añadir un guión gráfico y la biblioteca de widgets. Mientras que el storyboard nos permitirá gestionar fácilmente nuestra escena, la biblioteca de widgets nos permitirá añadir fácilmente elementos comunes a nuestra aplicación. En este caso, utilizaremos la biblioteca de widgets para añadir un botón a nuestro menú. Llegaremos a eso más tarde.
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local widget = require "widget"
Paso 2
Después de los requerimientos, crearemos nuestra primera función llamada scene:createScene()
. Esta función será llamada cuando la escena no exista y es un lugar perfecto para nuestro título y botón del juego.
-- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view end
Paso 3
Dentro de nuestra función scene:createScene()
, crearemos un nuevo objeto de pantalla que se utilizará como fondo. Si aún no lo has hecho, asegúrate de descargar los archivos fuente de este tutorial y coloca todos los gráficos dentro de una carpeta llamada images en tu proyecto.
El objeto de visualización de fondo se centrará en la pantalla y se insertará en nuestra variable de grupo. Al insertar el objeto de visualización en nuestra variable de grupo, le estamos diciendo a Corona que este objeto pertenece a esta escena. Cuando cambiemos de escena, Corona sabrá eliminar este objeto u ocultarlo porque ya no estamos viendo la escena del menú.
Una mirada en profundidad a los storyboards está fuera del alcance de este tutorial, pero puedes leer más en la documentación oficial.
-- Insert a background into the game local background = display.newImageRect("images/background.png", 480, 320) background.x = halfW background.y = halfH group:insert(background)
Paso 4
Después de nuestro objeto de fondo, colocaremos un objeto de texto que mostrará el título de nuestro juego. Este objeto de texto estará centrado en la pantalla y se insertará en la variable de grupo.
-- Insert the game title local gameTitle = display.newText("Space Monkey",0,0,native.systemFontBold,32) gameTitle.x = halfW gameTitle.y = halfH - 80 group:insert(gameTitle)
Paso 5
Nuestra última adición a la función scene:createScene()
será un widget de botón. Este widget permitirá a los jugadores iniciar el juego. Antes de que podamos añadir el widget, necesitamos crear una función que maneje el evento táctil.
Cuando se toque el botón, se llamará a la siguiente función. Esta función enviará a los jugadores a la escena del juego, que crearemos más adelante.
-- This function will only be fired when the widget playBtn is touched. local function onPlayBtnRelease() storyboard.gotoScene( "game", "fade", 500 ) return true end
Paso 6
Después de la función onPlayBtnRelease
, añadiremos el botón a la escena del menú. Añadimos el botón utilizando widget.newButton
con un par de parámetros. La propiedad label establecerá el texto de nuestro botón y la propiedad onRelease
le dirá a nuestra aplicación qué función debe disparar cuando se toque. Luego, colocaremos el botón en el centro de la pantalla y lo insertaremos en la variable de grupo. El playBtn
será la última pieza añadida a la función scene:createScene()
.
-- Create a widget button that will let the player start the game local playBtn = widget.newButton { label="Play Now", onRelease = onPlayBtnRelease } playBtn.x = halfW playBtn.y = halfH group:insert( playBtn )
Paso 7
Justo después de la función scene:createScene()
, vamos a añadir la función scene:enterScene()
. Esta función será llamada después de que la escena sea creada y esté en la pantalla.
function scene:enterScene(event) local group = self.view if(storyboard.getPrevious() ~= nil) then storyboard.purgeScene(storyboard.getPrevious()) storyboard.removeScene(storyboard.getPrevious()) end end
Paso 8
Para terminar nuestro archivo menu.lua, añadiremos dos escuchadores de eventos y devolveremos la variable de escena. Este es un paso importante porque los dos escuchadores de eventos dispararán las funciones apropiadas y devolverán la variable de escena para significar el final del archivo.
scene:addEventListener("createScene", scene) scene:addEventListener("enterScene", scene) return scene
5. Lógica del juego
Por ahora, hemos completado la configuración, el archivo main.lua y el archivo menu.lua. A continuación, vamos a crear la lógica de nuestro juego.
Paso 1
La lógica del juego se mantendrá dentro de un archivo llamado game.lua. Para empezar, crea un nuevo archivo llamado game.lua y colócalo dentro de la carpeta de tu proyecto. Nuestra primera adición al nuevo archivo es requerir el Storyboard de Corona.
local storyboard = require( "storyboard" ) local scene = storyboard.newScene()
Paso 2
A continuación, añadiremos la física a nuestro juego. Utilizaremos la física para facilitar la detección de colisiones entre las balas y las naves enemigas. Justo después de añadir la capacidad física, la pondremos en pausa para que no interfiera con la creación de la escena.
local physics = require "physics" physics.start(); physics.pause()
Paso 3
Después de la física, predefiniremos las variables para nuestro juego.
local background, monkey, bullet, txt_score local tmr_createBadGuy local lives = {} local badGuy = {} local badGuyCounter = 1 local score = 0
Paso 4
El siguiente conjunto de variables se utilizará como ajustes para nuestro juego. Siéntete libre de cambiar las velocidades del juego o aumentar el número de vidas para el jugador.
local numberOfLives = 3 local bulletSpeed = 0.35 local badGuyMovementSpeed = 1500 local badGuyCreationSpeed = 1000
Paso 5
Ahora crearemos nuestra función scene:createScene
.
-- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view
Paso 6
A continuación, crearemos una función que se disparará cuando se toque el objeto de la pantalla de fondo, y esta función contendrá la mayor parte de nuestra lógica de juego. Cuando se dispare, giraremos el mono hacia el evento de toque y dispararemos una bala hacia el mismo evento de toque.
En primer lugar, vamos a crear la función tocada y crear una declaración condicional para que nuestra lógica solo se ejecute durante la fase de inicio.
function touched(event) if(event.phase == "began") then
Dentro de la declaración if-then, utilizamos la biblioteca matemática para determinar el ángulo entre el mono y el evento de toque. Esto se consigue utilizando una combinación de math.deg
y math.atan2
para encontrar dónde hay que rotar el mono. Una vez encontrado el grado de rotación, giramos el mono a la posición adecuada.
angle = math.deg(math.atan2((event.y-monkey.y),(event.x-monkey.x))) monkey.rotation = angle + 90
Como esta función va a disparar una bala, tenemos que crear la bala como un objeto de visualización. Una vez creada, haremos el objeto de física para que pueda responder a las colisiones con los enemigos.
bullet = display.newImageRect("images/grenade_red.png",12,16) bullet.x = halfW bullet.y = halfH bullet.name = "bullet" physics.addBody( bullet, "dynamic", { isSensor=true, radius=screenH*.025} ) group:insert(bullet)
Ahora que tenemos nuestra bala, tenemos que averiguar dónde enviarla. Para ello, primero determinamos si tenemos que enviar la bala a la izquierda o a la derecha y, a continuación, utilizamos la fórmula y = mx + b para determinar la ubicación y. Nuestro último ejercicio matemático es encontrar la distancia entre los dos puntos para poder determinar la velocidad de proyección de la bala.
-- Find out if we need to fire the bullet to the left or right local farX = screenW*2 if(event.xStart >= screenW/2)then farX = screenW*2 else farX = screenW-(screenW*2) end -- Use y = mx + b to find the Y position local slope = ((event.yStart-screenH/2)/(event.xStart-screenW/2)) local yInt = event.yStart - (slope*event.xStart) local farY = (slope*farX)+yInt -- Get the distance from the bullet to bullet destination local xfactor = farX-bullet.x local yfactor = farY-bullet.y local distance = math.sqrt((xfactor*xfactor) + (yfactor*yfactor))
Ahora que sabemos la distancia y las coordenadas x/y del destino de la bala, podemos enviar nuestra bala al destino usando transition.to
. También necesitaremos incluir un par de sentencias end para envolver la función tocada.
bullet.trans = transition.to(bullet, { time=distance/bulletSpeed, y=farY, x=farX, onComplete=nil}) end end
Paso 7
Después de todas esas matemáticas, los siguientes pasos son sencillos. Añadiremos un fondo a nuestro juego (adjuntaremos un oyente de eventos al fondo más tarde), el mono, y un objeto de texto para mostrar la puntuación.
-- Create a background for our game background = display.newImageRect("images/background.png", 480, 320) background.x = halfW background.y = halfH background:setFillColor( 128 ) group:insert(background) -- Place our monkey in the center of screen monkey = display.newImageRect("images/spacemonkey-01.png",30,40) monkey.x = halfW monkey.y = halfH group:insert(monkey) -- Create a text object for our score txt_score = display.newText("Score: "..score,0,0,native.systemFont,22) txt_score.x = 430 group:insert(txt_score)
También insertaremos tres plátanos para representar tres vidas del jugador en la parte superior derecha de la pantalla.
-- Insert our lives, but show them as bananas for i=1,numberOfLives do lives[i] = display.newImageRect("images/banana.png",45,34) lives[i].x = i*40-20 lives[i].y = 18 group:insert(lives[i]) end
Paso 8
A continuación, crearemos una función que enviará a los tipos malos hacia nuestro jugador. Vamos a llamar a esta función createBadGuy
y primero determinaremos desde qué dirección enviar al malo.
-- This function will create our bad guy function createBadGuy() -- Determine the enemies starting position local startingPosition = math.random(1,4) if(startingPosition == 1) then -- Send bad guy from left side of the screen startingX = -10 startingY = math.random(0,screenH) elseif(startingPosition == 2) then -- Send bad guy from right side of the screen startingX = screenW + 10 startingY = math.random(0,screenH) elseif(startingPosition == 3) then -- Send bad guy from the top of the screen startingX = math.random(0,screenW) startingY = -10 else -- Send bad guy from the bototm of the screen startingX = math.random(0,screenW) startingY = screenH + 10 end
Después de haber determinado la dirección de la que vendrá nuestro chico malo, crearemos un nuevo objeto de visualización para nuestro chico malo y lo convertiremos en un objeto de física. Usaremos la física para detectar colisiones más adelante.
-- Start the bad guy according to starting position badGuy[badGuyCounter] = display.newImageRect("images/alien_1.png",34,34) badGuy[badGuyCounter].x = startingX badGuy[badGuyCounter].y = startingY physics.addBody( badGuy[badGuyCounter], "dynamic", { isSensor=true, radius=17} ) badGuy[badGuyCounter].name = "badGuy" group:insert(badGuy[badGuyCounter])
Luego, usaremos la Corona transition.to
para mover al malo hacia el centro de la pantalla. Una vez realizada la transición y si el enemigo no ha sido golpeado, eliminaremos al malo y le restaremos una vida al jugador.
Si las vidas del jugador han llegado a 0 o menos, dejaremos de enviar a los malos y eliminaremos la capacidad del jugador de enviar más balas. También mostraremos dos objetos de texto para indicar que el juego ha terminado y dejar que el jugador vuelva al menú.
badGuy[badGuyCounter].trans = transition.to(badGuy[badGuyCounter], { time=badGuyMovementSpeed, x=halfW, y=halfH, onComplete = function (self) self.parent:remove(self); self = nil; -- Since the bad guy has reached the monkey, we will want to remove a banana display.remove(lives[numberOfLives]) numberOfLives = numberOfLives - 1 -- If the numbers of lives reaches 0 or less, it's game over! if(numberOfLives <= 0) then timer.cancel(tmr_createBadGuy) background:removeEventListener("touch", touched) local txt_gameover = display.newText("Game Over!",0,0,native.systemFont,32) txt_gameover.x = halfW txt_gameover.y = screenH * 0.3 group:insert(txt_gameover) local function onGameOverTouch(event) if(event.phase == "began") then storyboard.gotoScene("menu") end end local txt_gameover = display.newText("Return To Menu",0,0,native.systemFont,32) txt_gameover.x = halfW txt_gameover.y = screenH * 0.7 txt_gameover:addEventListener("touch",onGameOverTouch) group:insert(txt_gameover) end end; }) badGuyCounter = badGuyCounter + 1 end
Paso 9
La última función dentro de scene:createScene
es para la detección de colisiones. Cuando una bala y el chico malo colisionen, esta función se activará y ocurrirá lo siguiente:
- Añade 1 a la puntuación del jugador y actualiza el objeto de texto de la puntuación.
- Poner el alfa de ambos objetos a 0.
- Cancela la transición en cada objeto para que no interfiera en el proceso de eliminación.
- Finalmente, crearemos un temporizador que esperará 1 milisegundo antes de eliminar ambos objetos. Siempre es una mala idea eliminar los objetos de la pantalla en medio de la detección de colisiones.
function onCollision( event ) if(event.object1.name == "badGuy" and event.object2.name == "bullet" or event.object1.name == "bullet" and event.object2.name == "badGuy") then -- Update the score score = score + 1 txt_score.text = "Score: "..score -- Make the objects invisible event.object1.alpha = 0 event.object2.alpha = 0 -- Cancel the transitions on the object transition.cancel(event.object1.trans) transition.cancel(event.object2.trans) -- Then remove the object after 1 cycle. Never remove display objects in the middle of collision detection. local function removeObjects() display.remove(event.object1) display.remove(event.object2) end timer.performWithDelay(1, removeObjects, 1) end end end
Paso 10
Después de la escena:createScene
, escribiremos nuestra función de entrada a la escena. Esta función se dispara después de crear la escena y pasarla a la pantalla. Dentro de la función de entrar en escena, iniciaremos todas las funciones que escribimos en la función de crear escena.
function scene:enterScene( event ) local group = self.view -- Actually start the game! physics.start() -- Start sending the bad guys tmr_createBadGuy = timer.performWithDelay(badGuyCreationSpeed, createBadGuy, 0) -- Start listening for the background touch event to fire the bullets background:addEventListener("touch", touched) -- Start the listener to remove bad guys and bullets when they collide Runtime:addEventListener( "collision", onCollision ) End
Paso 11
¡Ya casi hemos terminado! La última pieza de nuestro código es añadir los escuchadores de eventos de la escena y devolver la variable scene. Los escuchadores de eventos activarán nuestras funciones y la declaración de retorno permite a Corona saber que hemos terminado con esta escena.
scene:addEventListener( "createScene", scene ) scene:addEventListener( "enterScene", scene ) return scene
Conclusión
¡Espero que hayas disfrutado de este tutorial sobre la creación de un juego Monkey Defender con el Corona SDK! ¡Hemos cubierto un montón de temas que van desde los storyboards a las fórmulas geométricas! Si tienes preguntas o comentarios, por favor déjalos abajo y gracias por leer.