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

Crear invasores del espacio con Swift y Sprite Kit: implementando el juego

by
Difficulty:BeginnerLength:LongLanguages:
This post is part of a series called Create Space Invaders with Swift and Sprite Kit.
Create Space Invaders with Swift and Sprite Kit: Implementing Classes
Create Space Invaders with Swift and Sprite Kit: Finishing Gameplay

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

Final product image
What You'll Be Creating

En la parte previa de ésta serie, implementamos las bases para las principales clases del juego. En éste tutorial, pondremos a mover a los invasores, vamos a disparar las balas desde los invasores y desde el jugador, e implementaremos la detección de colisión. Comencemos.

1. Moviendo los Invasores

Usaremos el método update de la escena para mover los invasores. Cuando quieras mover algo manualmente, el método update es generalmente donde querrías hacer esto.

Aunque antes de hacer esto, necesitamos actualizar la propiedad rightBounds. Inicialmente se estableció en 0, porque necesitamos usar el size (tamaño) de la escena para definir la variable. No pudimos hacer eso afuera de cualesquiera de los métodos de la clase, así que actualizaremos ésta propiedad en el método didMoveToView(_:)

Luego, implementa el método moveInvaders debajo del método setupPlayer que creaste en el anterior tutorial.

Declaramos una variable, changeDirection, para mantener un seguimiento cuando los invasores necesitan cambiar de dirección, moverse a la derecha o moverse a la izquierda. Posteriormente usamos el método enumerateChildNodesWithName(usingBlock:), que busca los hijos de un nodo y llama al closure (función de cierre) una vez para cada nodo coincidente que encuentra con el nombre coincidente "invader". El closure o función de cierre acepta dos parámetros, node es el node que coincide con el name (nombre) y stop (alto) es un indicador a una variable booleana para terminar la enumeración. No usaremos stop aquí, pero es bueno saber para lo que se usa.

Lanzamos node a una instancia de SKSpriteNode de la cual invader es una subclase, obtenemos la mitad de su anchura invaderHalfWidth, y actualizamos su posición. Entonces checamos si su position (posición) está dentro de los límites, leftBounds y rightBoundes, y, si no, establecemos changeDirection en true.

Si changeDirection es true, negamos invaderSpeed, que cambiará la dirección en la que se mueve el invasor. Posteriormente enumeramos los invasores y actualizamos su posición y. Finalmente, establecemos changeDirection de nuevo a false (falso).

El método moveInvaders es llamado en el método update(_:).

Si pruebas la aplicación ahora, deberías ver a los invasores moviéndose a la izquierda, derecha, y luego hacia abajo si llegan a los límites que hemos establecido en cada lado.

2. Disparando las Balas del Invasor

Paso 1: fireBullet

Frecuentemente queremos que uno de los invasores dispare una bala. Por ahora, los invasores en la fila de abajo están preparados para disparar una bala, porque están en el arreglo o vector invadersWhoCanFire.

Cuando un invasor es alcanzado por una bala de un jugador, entonces el invasor de una fila arriba y en la misma columna será agregado al arreglo invadersWhoCanFire, mientras el invasor que sea impactado será removido. De ésta forma sólamente el invasor que está más abajo de cada columna puede disparar balas.

Agrega el método fireBullet a la clase InvaderBullet en InvaderBullet.swift.

En el método fireBullet, creamos una instancia InvaderBullet, pasando "laser" para imageName, y debido a que no queremos que se reproduzca un sonido pasamos nil para bulletSound. Establecemos su position (posición) para que sea la misma que la del invasor, con una ligera desviación en la posición y, y la agregamos a la escena.

Creamos dos instancias SKAction, moveBulletAction y removeBulletAction. La acción moveBulletAction mueve la bala a un cierto punto sobre una cierta duración mientras la acción removeBulletAction la remueve de la escena. Al invocar el método sequence(_:) en éstas acciones, correrán secuencialmente. Por eso mencioné el método waitForDuration cuando se reproduce un sonido en la parte previa de ésta serie. Si creas un objeto SKAction al invocar playSoundFileNamed(_:waitForCompletion:) y estableces waitForCompletion a true, entonces la duración de esa acción se prolongaría mientras se reproduce el sonido, de otra forma pasaría inmediatamente a la próxima acción en la secuencia.

Paso 2: invokeInvaderFire

Agrega el método invokeInvaderFire debajo de los otros métodos que has creado en GameScene.swift.

El método runBlock(_:) de la clase SKAction crea una instancia SKAction e inmediatamente invoca el cierre pasado al método runBlock(_:). En el cierre, invocamos el método fireInvaderBullet. Debido a que invocamos éste método en un cierre, tenemos que usar self para llamarlo.

Después creamos una instancia SKAction llamada waitToFireInvaderBullet al invocar waitForDuration(_:), pasando el número de segundo a esperar antes de moverse. Luego, creamos una instancia SKAction, invaderFire, al invocar el método sequence(_:). Éste método acepta una colección de acciones que son invocadas por la acción invaderFire. Queremos que ésta secuencia se repita para siempre así que creamos una acción llamada repeatForeverAction, pasamos los objetos SKAction para repetir, e invocamos runAction, pasando la acción repeatForeverAction. El método runAction es declarado en la clase SKNode.

Paso 3: fireInvaderBullet

Agrega el método fireInvaderBullet debajo del método invokeInvaderFire que ingresaste en el paso anterior.

En éste método, llamamos lo que parece ser un método llamado randomElement que retornaría un elemento al azar del arreglo invadersWhoCanFire, y luego llama a su método fireBullet. No hay, afortunadamente, ningún método randomElement en la estructura Array. Sin embargo, podemos crear una extensión Array para proporcionar ésta funcionalidad.

Paso 4: Implementar randomElement

Ve a File > New > File... y elige Swift File. Estamos haciendo algo diferente de lo que hicimos antes así que asegúrate que estás eligiendo Swift File y no Cocoa Touch Class. Presiona Next y nombra el archivo Utilities. Agrega lo siguiente a Utilites.swift.

Extendemos la estructura Array para tener un método llamado randomElement. La función arc4random_uniform regresa un número entre 0 y cualquiera que pases. Debido a que Swift no convierte implícitamente tipos numéricos, debemos hacer nosotros mismos la conversión. Finalmente, regresamos el elemento del array en el indíce index.

Éste ejemplo ilustra que fácil es añadir funcionalidad a la estructura y clases. Puedes leer más sobre crear extensiones en el Lenguaje de Programación Swift.

Paso 5: Disparando la Bala

Con todo ésto fuera del camino, podemos ahora disparar las balas. Agrega lo siguiente al método didMoveToView(_:).

Si pruebas tu aplicación ahora, cada segundo aproximadamente deberías ver a uno de los invasores de la fila de abajo disparar una bala.

3. Disparando las Balas del Jugador

Paso 1: fireBullet(scene:)

Agrega la siguiente propiedad a la clase Player en Player.swift.

Queremos limitar que tan frecuentemente el jugador puede disparar una bala. La propiedad canFire será usada para regular eso. Posteriormente, agrega lo siguiente al método fireBullet(scene:) en la clase Player.

Primero nos aseguramos que el jugador pueda disparar al revisar si canFire esta establecido en true. Si no, inmediatamente regresamos del método.

Si el jugador puede disparar, establecemos canFire a false para que no pueden disparar inmediatamente otra bala. Luego creamos una instancia PlayerBullet, pasando "laser" para el parámetro imageNamed. Ya que queremos que se reproduzca un sonido cuando el jugador dispara una bala, pasamos "laser.mp3" para el parámetro bulletSound.

Entonces establecemos la posición de la bala y la agregamos a la pantalla. Las próximas líneas son las mismas que las del método fireBullet de Invader en que movemos la bala y la removemos de la escena. Luego, creamos una instancia SKAction, waitToEnableFire, al invocar el método de la clase waitForDuration(_:) Finalmente, invocamos runAction, pasando waitToEnableFire, y para completar establecemos canFire otra vez en true.

Paso 2: Disparando la Bala del Jugador

Cuando el usuario toca la pantalla, queremos disparar una bala. Ésto es tan sencillo como llamar fireBullet en el objeto player en el método touchesBegan(_:withEvent:) de la clase GameScene.

Si pruebas la aplicación ahora, deberías poder disparar una bala cuando presionas la pantalla. También, deberías escuchar el sonido laser cada vez que es disparada una bala.

4. Categorías de Colisión

Para detectar cuando están colisionando o haciendo contacto los nodos, usaremos el motor de física integrado de Sprite Kit. Sin embargo, el comportamiento por defecto del motor de física es que todo choca contra todo cuando tienen un cuerpo físico agregado a ellos. Necesitamos una forma de separar lo que queremos que interactúe uno con otro y podemos hacer ésto al crear categorías a las que pertenecen cuerpos físicos específicos.

Defines éstas categorías usando un enmascaramiento del bit que utiliza un número entero de 32 bits con 32 flags (uno o más bits usados para almacenar un valor binario o código que tiene un significado asociado) individuales que pueden estar activos o inactivos. Ésto también significa que puedes sólo tener un máximo de 32 categorías para tu juego. Ésto no debería de suponer un problema para la mayoría de los juegos, pero es algo para considerar.

Agrega la siguiente definición de estructura a la clase GameScene debajo de la declaración invaderNum en GameScene.swift.

Usamos una estructura, CollisionCategories, para crear categorías para las clases Invader (Invasor), Player (Jugador), InvaderBullet (Bala del Invasor), y PlayerBullet (Bala del Jugador). Estamos usando bit shifting (desplazamiento de bits) para activar los bits.

5. Colisión Player (Jugador) y InvaderBullet (Bala del Invasor).

Paso 1: Configurando InvaderBullet para la Colisión.

Agregar el siguiente bloque de código al método init(imageName:bulletSound:) en InvaderBullet.swift.

Siempre hay maneras de crear un cuerpo físico. En éste ejemplo, usamos el inicializador init(texture:size:), que hará que la detección de colisión use la forma de la textura que pasamos. Hay otros inicializadores disponibles, que puedes ve in la guía de la clase SKPhysicsBody.

Pudimos fácilmente haber usado el incializador init(rectangleOfSize:), porque las balas son de forma rectangular. En un juego éste detalle menor no importa. No obstante, ten en cuenta que usar el método init(texture:size:) puede ser computacionalmente caro ya que tiene que calcular la forma exacta de la textura. Si tienes objetos que tienen forma rectangular o circular, entonces deberías usar esos tipos de inicializadores si el rendimiento del juego se está convirtiendo en un problema.

Para que funcione la detección de colisión, al menos uno de los cuerpos que estás probando tiene que ser marcado como dinámico. Al establecer la propiedad usesPreciseCollisionDetection en true, Sprite Kit usa una detección de colisión más precisa. Establece ésta propiedad en true, en cuerpos pequeños y que se muevan rápido como nuestras balas.

Cada cuerpo pertenecerá a una categoría y define ésto al establecer su categoryBitMask. Ya que ésta es la clase InvaderBullet, la establecemos a CollisionCategories.InvaderBullet.

Para señalar cuando éste cuerpo ha hecho contacto con otro cuerpo en el que estás interesado, defines el contactBitMask. Aquí queremos saber cuando el InvaderBullet ha hecho contacto con el jugador así que usamos CollisionCategories.Player. Debido a que una colisión no debería generar ninguna fuerza fisica, establecemos collisionBitMask en 0x0.

Paso 2: Configurar Player (Jugador) para la Colisión

Agregar lo siguiente al método init en Player.swift.

Mucho de ésto debería estar familiarizado con el paso anterior así que no lo repetiré aquí. Aunque hay dos diferencias a destacar. Una es que usesPreciseCollsionDetection ha sido establecido en false, que es la opción por defecto. Es importante darse cuenta que sólo uno de los cuerpos en contacto necesita ésta propiedad sea establecida en true (que fue la bala). La otra diferencia es que también queremos saber cuando el jugador hace contacto con un invasor. Puedes tener más de una categoría contactBitMask al separarlas con el bitwise u operador a nivel de bits(|) Además de eso, deberías notar que es básicamente opuesto de InvaderBullet.

6. Colisión Invader (Invasor) y PlayerBullet(Bala de Jugador)

Paso 1: Configurando el Invader para la Colisión

Agrega lo siguiente al método init en Invader.swift.

Todo esto debería entenderse si has recorrido el camino desde el principio. Configuramos el physicsBody, categoryBitMask, y contactBitMask.

Paso 2: Configurando PlayerBullet (Bala de Jugador) para la Colisión

Agrega lo siguiente a init(imageName:bulletSound:) en PlayerBullet.swift. De nuevo, la implementación debería serte familiar a estas alturas.

7. Configurando la física para GameScene

Paso 1: Configurando Physics World (el Mundo de la Física)

Tenemos que configurar la clase GameScene para implementear el SKPhysicsContactDelegate para que podamos responder cuando dos cuerpos chocan. Agrega lo siguiente para hacer que la clase GameScene cumpla con el protocolo SKPhysicsContactDelegate.

Posteriormente, tenemos que configurar algunas propiedades en physicsWorld de la escena. Ingresa lo siguiente arriba del método didMoveToView(_:) en GameScene.swift.

Establecemos la propiedad gravity de physicsWorld en 0 para que ninguno de los cuerpos físicos en la escena sean afectados por gravedad. También puedes hacer ésto en una base por cuerpo en lugar de establecer que todo el mundo no tenga gravedad al establecer la propiedad affectedByGravity. Establecemos también la propiedad contactDelegate del mundo de la física en self, la instancia GameScene.

Paso 2: Implementando el Protocolo SKPhysicsContactDelegate

Para hacer que la clase GameScene cumpla con el protocolo SKPhysicsContactDelegate, necesitamos implementar el método didBeginContact(_:). Éste método es llamado cuando dos cuerpos hacen contacto. La implementación del método didBeginContact(_:) se ve así.

Primer declaramos dos variables firstBody y secondBody. Cuando dos objetos hacen contacto, no sabemos que cuerpo es cual. Ésto significa que primero necesitamos hacer algunas revisiones para asegurarnos que firstBody es el de menor categoryBitMask.

Luego, recorremos cada escenario posible usando el operador a nivel de bits & y las categorías de colisión que definimos anteriormente para checar que está haciendo contacto. Imprimimos el resultado en la consola para asegurarnos que todo esté funcionando como debería. Si pruebas la aplicación, todos los contactos deberían funcionar correctamente.

Conclusión

Éste fue más bien un largo tutorial, pero ahora tenemos los invasores moviéndose, balas siendo disparadas desde el jugador y desde los invasores, y la detección de contactos funcionando al usar enmascaramiento de bits. Estamos en la recta final del proceso del juego. En la próxima y última parte de ésta serie, habremos completado el juego.

¡Sé el primero en conocer las nuevas traducciones–sigue @tutsplus_es en Twitter!

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.