Crea Space Invaders con Swift y Sprite Kit implementando el juego

Lo que vas a crear

En la parte anterior de esta serie, implementamos los talones para las clases principales del juego. En este tutorial, haremos que los invasores se muevan, disparen balas tanto para los invasores como para el jugador, e implementemos la detección de colisiones. Empecemos.

1. Moviendo a los invasores

Usaremos la escena. actualizar Método para mover a los invasores. Cuando quiera mover algo manualmente, el actualizar El método es generalmente donde querrías hacer esto..

Antes de hacer esto, sin embargo, necesitamos actualizar el a la derecha propiedad. Inicialmente se estableció en 0, Porque necesitamos usar la escena tamaño para establecer la variable. No pudimos hacer eso fuera de cualquiera de los métodos de la clase, así que actualizaremos esta propiedad en el didMoveToView (_ :) método.

anular func didMoveToView (ver: SKView) backgroundColor = SKColor.blackColor () rightBounds = self.size.width - 30 setupInvaders () setupPlayer ()

A continuación, implementa el mover los invasores método debajo del setupPlayer Método que creaste en el tutorial anterior..

func moveInvaders () var changeDirection = false enumerateChildNodesWithName ("invader") node, stop in let invader = node as! SKSpriteNode let invaderHalfWidth = invader.size.width / 2 invader.position.x - = CGFloat (self.invaderSpeed) if (invader.position.x> self.rightBounds - invaderHalfWidth || invader.position.x < self.leftBounds + invaderHalfWidth) changeDirection = true   if(changeDirection == true) self.invaderSpeed *= -1 self.enumerateChildNodesWithName("invader")  node, stop in let invader = node as! SKSpriteNode invader.position.y -= CGFloat(46)  changeDirection = false  

Declaramos una variable., cambia la direccion, para realizar un seguimiento cuando los invasores necesitan cambiar de dirección, moverse hacia la izquierda o hacia la derecha. Entonces usamos el enumerateChildNodesWithName (usingBlock :) Método, que busca a los hijos de un nodo y llama al cierre una vez para cada nodo coincidente que encuentre con el nombre correspondiente. "invasor". El cierre acepta dos parámetros., nodo es el nodo que coincide con el nombre y detener es un puntero a una variable booleana para terminar la enumeración. No vamos a utilizar detener Aquí, pero es bueno saber para qué se utiliza..

Echamos nodo a una SKSpriteNode instancia que invasor Es una subclase de, obtener la mitad de su ancho. InvaderHalfWidth, y actualizar su posición. Entonces comprobamos si es posición está dentro de los límites, Límite izquierdo y a la derecha, y, si no, ponemos cambia la direccion a cierto.

Si cambia la direccion es cierto, negamos invaderSpeed, que cambiará la dirección en la que se mueve el invasor. Luego enumeramos a través de los invasores y actualizamos su posición y. Por último, nos propusimos cambia la direccion de regreso falso.

los mover los invasores método se llama en el actualizar(_:) método.

anular la actualización de la función (currentTime: CFTimeInterval) moveInvaders ()

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

2. Disparando balas invasoras

Paso 1: FireBullet

De vez en cuando queremos que uno de los invasores dispare una bala. Tal como está ahora, los invasores en la fila inferior están configurados para disparar una bala, porque están en la InvadersWhoCanFire formación.

Cuando un invasor es golpeado por una bala de jugador, entonces el invasor una fila arriba y en la misma columna se agregará a la InvadersWhoCanFire matriz, mientras que el invasor que recibió el golpe será eliminado. De esta manera, solo el invasor de abajo de cada columna puede disparar balas..

Añade el FireBullet método para el InvaderBullet clase en InvaderBullet.swift.

func fireBullet (scene: SKScene) let bullet = InvaderBullet (imageName: "laser", bulletSound: nil) bullet.position.x = self.position.x bullet.position.y = self.position.y - self.size. height / 2 scene.addChild (bullet) deja moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: 0 - bullet.size.height), duración: 2.0) deja removeBulletAction = SKAction.removeFromParent () bullet .runAction (SKAction.sequence ([moveBulletAction, removeBulletAction])) 

En el FireBullet método, instanciamos un InvaderBullet instancia, pasando en "láser" para Nombre de la imágen, y porque no queremos que suene un sonido, pasamos nulo para bulletSound. Establecemos su posición para ser el mismo que el del invasor, con un ligero desplazamiento en la posición y, y agregarlo a la escena.

Creamos dos SKAcción instancias, moveBulletAction y removeBulletAction. los moveBulletAction La acción mueve la bala a un cierto punto durante un cierto tiempo, mientras que la acción removeBulletAction La acción lo saca de la escena. Invocando el secuencia(_:) método en estas acciones, se ejecutarán secuencialmente. Por eso mencioné el waitForDuration Método al reproducir un sonido en la parte anterior de esta serie. Si creas un SKAcción objeto invocando playSoundFileNamed (_: waitForCompletion :) y establecer waitForCompletion a cierto, entonces la duración de esa acción será por el tiempo que se reproduzca el sonido, de lo contrario, saltará inmediatamente a la siguiente acción en la secuencia.

Paso 2: invokeInvaderFire

Añade el invokeInvaderFire Método debajo de los otros métodos que has creado en GameScence.swift.

func invokeInvaderFire () permite a fireBullet = SKAction.runBlock () self.fireInvaderBullet () permite a waitToFireInvaderBullet = SKAction.wait recreación en el estado de la embarcación (1.5) ) runAction (repeatForeverAction)

los runBlock (_ :) método de la SKAcción clase crea un SKAcción instancia e inmediatamente invoca el cierre pasado a la runBlock (_ :) método. En el cierre, invocamos el fireInvaderBullet método. Debido a que invocamos este método en un cierre, tenemos que usar yo llamarlo.

Entonces creamos un SKAcción instancia nombrada waitToFireInvaderBullet invocando waitForDuration (_ :), pasando el número de segundos a esperar antes de continuar. A continuación, creamos un SKAcción ejemplo, InvaderFire, invocando el secuencia(_:) método. Este método acepta una colección de acciones que son invocadas por el InvaderFire acción. Queremos que esta secuencia se repita para siempre, así que creamos una acción llamada repetir para siempre, pasar en el SKAcción Objetos a repetir, e invocar. runAction, pasando en el repetir para siempre acción. El método runAction se declara en el SKNode clase.

Paso 3: fireInvaderBullet

Añade el fireInvaderBullet método debajo del invokeInvaderFire método que ingresaste en el paso anterior.

 func fireInvaderBullet () let randomInvader = invadersWhoCanFire.randomElement () randomInvader.fireBullet (self) 

En este método, llamamos a lo que parece ser un método llamado elemento aleatorio Eso devolvería un elemento aleatorio fuera de la InvadersWhoCanFire matriz, y luego llamar a su FireBullet método. Lamentablemente, no hay ninguna elemento aleatorio método en el Formación estructura. Sin embargo, podemos crear un Formación extensión para proporcionar esta funcionalidad.

Paso 4: Implementar elemento aleatorio

Ir Expediente > Nuevo > Expediente… y elige Archivo rápido. Estamos haciendo algo diferente que antes, así que asegúrate de que estás eligiendo Archivo rápido y no Clase de Cocoa Touch. prensa Siguiente y nombra el archivo Utilidades. Agregue lo siguiente a Utilities.swift.

importar la extensión de Foundation Array func randomElement () -> T let index = Int (arc4random_uniform (UInt32 (self.count))) return self [index]

Extendemos el Formación estructura para tener un método llamado elemento aleatorio. los arc4random_uniform La función devuelve un número entre 0 y lo que pase. Debido a que Swift no convierte implícitamente los tipos numéricos, nosotros debemos hacer la conversión nosotros mismos. Finalmente, devolvemos el elemento de la matriz en el índice. índice.

Este ejemplo ilustra lo fácil que es agregar funcionalidad a la estructura y las clases. Puede leer más sobre la creación de extensiones en The Swift Programming Language.

Paso 5: disparando la bala

Con todo esto fuera del camino, ahora podemos disparar las balas. Agregue lo siguiente a la didMoveToView (_ :) método.

 función de reemplazo didMoveToView (ver: SKView) … setupPlayer () invokeInvaderFire ()

Si prueba la aplicación ahora, cada segundo o así debería ver a uno de los invasores de la fila inferior disparar una bala.

3. Disparando balas de jugador.

Paso 1: FireBullet (escena :)

Agregue la siguiente propiedad al Jugador clase en Jugador..

Clase Player: SKSpriteNode private var canFire = true

Queremos limitar la frecuencia con la que el jugador puede disparar una bala. los canFire La propiedad será utilizada para regular eso. A continuación, agregue lo siguiente a la FireBullet (escena :) método en el Jugador clase.

func fireBullet (escena: SKScene) if (! canFire) return else canFire = false let bullet = PlayerBullet (imageName: "laser", bulletSound: "laser.mp3") bullet.position.x = self.position. x bullet.position.y = self.position.y + self.size.height / 2 scene.addChild (bullet) deja moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: scene.size.height + bullet.size.height), duración: 1,0) dejar que removeBulletAction = SKAction.removeFromParent () bullet.runAction (SKAction.sequence ([moveBulletAction, removeBulletAction])) dejar que waitToEnableFire = SKAction.waitForDuration (0.5) runAction (waitToEnableFire, completado: self.canFire = true) 

Primero nos aseguramos de que el jugador pueda disparar al verificar si canFire se establece en cierto. Si no es así, volvemos inmediatamente del método..

Si el jugador puede disparar, ponemos canFire a falso por lo que no pueden disparar de inmediato otra bala. Entonces creamos una instancia de PlayerBullet instancia, pasando en "láser" Para el imageNamed parámetro. Porque queremos que se reproduzca un sonido cuando el jugador dispara una bala, pasamos "laser.mp3" Para el bulletSound parámetro.

Luego establecemos la posición de la bala y la agregamos a la pantalla. Las siguientes líneas son las mismas que las InvasoresFireBullet Método en el que movemos la bala y la sacamos de la escena. A continuación, creamos un SKAcción ejemplo, waitToEnableFire, invocando el waitForDuration (_ :) método de clase. Por último, invocamos runAction, pasando en waitToEnableFire, y en el set de finalización canFire de regreso cierto.

Paso 2: Disparando la bala del jugador

Siempre que el usuario toque la pantalla, queremos disparar una bala. Esto es tan simple como llamar FireBullet sobre el jugador objeto en el TocaBegan (_: conEvento :) método de la GameScene clase.

 anular func toques Empiezan (toques: Establecer, Evento withEvent: UIEvent) player.fireBullet (self) 

Si prueba la aplicación ahora, debería poder disparar una bala cuando toque la pantalla. Además, debe escuchar el sonido del láser cada vez que se dispara una bala.

4. Categorías de colisión

Para detectar cuándo los nodos chocan o hacen contacto entre sí, usaremos el motor de física incorporado del Sprite Kit. Sin embargo, el comportamiento predeterminado del motor de física es que todo choca con todo cuando se les agrega un cuerpo de física. Necesitamos una forma de separar lo que queremos interactuar unos con otros y podemos hacerlo creando categorías a las que pertenecen cuerpos físicos específicos..

Estas categorías se definen mediante una máscara de bits que utiliza un entero de 32 bits con 32 indicadores individuales que pueden estar activados o desactivados. Esto también significa que solo puedes tener un máximo de 32 categorías para tu juego. Esto no debería presentar un problema para la mayoría de los juegos, pero es algo a tener en cuenta.

Agregue la siguiente definición de estructura a la GameScene clase, debajo de la invasorNum declaración en GameScene.swift.

struct CollisionCategories estático deja Invader: UInt32 = 0x1 << 0 static let Player: UInt32 = 0x1 << 1 static let InvaderBullet: UInt32 = 0x1 << 2 static let PlayerBullet: UInt32 = 0x1 << 3 

Usamos una estructura, CollsionCategories, para crear categorías para el Invasor, Jugador, InvaderBullet, y PlayerBullet clases Estamos usando cambio de bits para activar los bits.

5. Jugador y InvaderBullet Colisión

Paso 1: Configuración InvaderBullet para colisión

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

 sobrescribir init (imageName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (textura: self.texture, size: self.size) self.physicsBody? .dynamic = true self.physicsBody? .usesPreciseCollisionDetection = true self.physicsBody? .categoryBitMask = CollisionCategories.InvaderBullet self.physicsBody? .contactcikacity.com

Hay varias formas de crear un cuerpo de física. En este ejemplo, usamos el init (textura: tamaño :) Inicializador, que hará que la detección de colisiones use la forma de la textura que pasamos. Hay varios otros inicializadores disponibles, que se pueden ver en la referencia de la clase SKPhysicsBody..

Podríamos haber usado el init (rectangleOfSize :) Inicializador, porque las balas tienen forma rectangular. En un juego tan pequeño no importa. Sin embargo, tenga en cuenta que el uso de init (textura: tamaño :) El método puede ser computacionalmente costoso ya que tiene que calcular la forma exacta de la textura. Si tienes objetos de forma rectangular o circular, deberías usar esos tipos de inicializadores si el rendimiento del juego se está convirtiendo en un problema..

Para que la detección de colisiones funcione, al menos uno de los cuerpos que está probando debe estar marcado como dinámico. Estableciendo el usesPreciseCollisionDetection propiedad a cierto, Sprite Kit utiliza una detección de colisión más precisa. Establezca esta propiedad en cierto En cuerpos pequeños, rápidos como nuestras balas..

Cada cuerpo pertenecerá a una categoría y usted lo define estableciendo su categoríaBitMask. Ya que esta es la InvaderBullet clase, lo ponemos a CollisionCategories.InvaderBullet.

Para saber cuándo este cuerpo se ha puesto en contacto con otro cuerpo en el que está interesado, debe configurar contactBitMask. Aquí queremos saber cuándo el InvaderBullet Ha hecho contacto con el jugador por lo que usamos. CollisionCategories.Player. Debido a que una colisión no debería desencadenar ninguna fuerza física, establecemos collisionBitMask a 0x0.

Paso 2: Configuración Jugador para Collsion

Agregue lo siguiente a la en eso método en Jugador..

 anular init () let texture = SKTexture (imageNamed: "player1") super.init (texture: texture, color: SKColor.clearColor (), size: texture.size ()) self.physicsBody = SKPhysicsBody (texture: self. textura, tamaño: self.size) self.physicsBody? .dynamic = true self.physicsBody? .PreciseCollisionDetection = false self.physicsBody? .categoryBitMask = CollisionCategories.Player selfies.physicsBody?. CollisionCategories.Invader self.physicsBody? .CollisionBitMask = 0x0 animate ()

Mucho de esto debería ser familiar del paso anterior, así que no lo repetiré aquí. Hay dos diferencias para notar sin embargo. Uno es que usesPreciseCollsionDetection se ha establecido en falso, cual es el predeterminado Es importante darse cuenta de que solo uno de los organismos de contacto necesita esta propiedad configurada para cierto (que fue la bala). La otra diferencia es que también queremos saber cuándo el jugador se pone en contacto con un invasor. Puedes tener más de una contactBitMask categoría separándolos con el bitwise o (|) operador. Aparte de eso, deberías notar que es básicamente opuesto a la InvaderBullet.

6. Invasor y PlayerBullet Colisión

Paso 1: Configuración Invasor para colisión

Agregue lo siguiente a la en eso método en Invader.swift.

 anular init () let texture = SKTexture (imageNamed: "invader1") super.init (texture: texture, color: SKColor.clearColor (), size: texture.size ()) self.name = "invader" self.physicsBody = SKPhysicsBody (textura: self.texture, tamaño: self.size) self.physicsBody? .Dynamic = true self.physicsBody? .UsesPreciseCollisionDetection = false self.physicsBody? Recreo de animales / animales / animales / animales / animales / animales / animales PlayerBullet | CollisionCategories.Player self.physicsBody? .CollisionBitMask = 0x0

Todo esto debería tener sentido si has estado siguiendo a lo largo. Establecimos el fisica cuerpo, categoríaBitMask, y contactBitMask.

Paso 2: Configuración PlayerBullet para colisión

Agregue lo siguiente a la init (imageName: bulletSound :) en JugadorBullet.swift. De nuevo, la implementación ya debería ser familiar..

 sobrescribir init (imageName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (textura: self.texture, size: self.size) self.physicsBody? .dynamic = true self.physicsBody? .usesPreciseCollisionDetection = true self.physicsBody? .categoryBitMask = CollisionCategories.PlayerBullet self.physicsBody? .contactcikisionBitMacityCity

7. Configuración de la física para GameScene

Paso 1: Configurando el mundo de la física

Tenemos que configurar el GameScene clase para implementar el SKPhysicsContactDelegate Así podemos responder cuando chocan dos cuerpos. Agregue lo siguiente para hacer el GameScene clase conforme a la SKPhysicsContactDelegate protocolo.

clase GameScene: SKScene, SKPhysicsContactDelegate 

A continuación, tenemos que configurar algunas propiedades en la escena fisica mundo. Introduzca lo siguiente en la parte superior de la didMoveToView (_ :) método en GameScene.swift.

anular la función didMoveToView (ver: SKView) self.physicsWorld.gravity = CGVectorMake (0, 0) self.physicsWorld.contactDelegate = self ...

Establecemos el gravedad propiedad de fisica mundo a 0 de modo que ninguno de los cuerpos físicos en la escena es afectado por la gravedad. También puede hacer esto por cada cuerpo en lugar de configurar el mundo entero para que no tenga gravedad al configurar el afectado por Gravedad propiedad. También establecemos el contactDelegate propiedad del mundo de la física para yo, la GameScene ejemplo.

Paso 2: Implementando SKPhysicsContactDelegate Protocolo

Para conformar el GameScene clase a SKPhysicsContactDelegate protocolo, necesitamos implementar el didBeginContact (_ :) método. Este método se llama cuando dos cuerpos hacen contacto. La implementación de la didBeginContact (_ :) el metodo se ve asi.

func didBeginContact (contact: SKPhysicsContact) var firstBody: SKPhysicsBody var secondBody: SKPhysicsBody si contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask  firstBody = contact.bodyA secondBody = contact.bodyB  else  firstBody = contact.bodyB secondBody = contact.bodyA  if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && (secondBody.categoryBitMask & CollisionCategories.PlayerBullet != 0)) NSLog("Invader and Player Bullet Conatact")  if ((firstBody.categoryBitMask & CollisionCategories.Player != 0) && (secondBody.categoryBitMask & CollisionCategories.InvaderBullet != 0))  NSLog("Player and Invader Bullet Contact")  if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && (secondBody.categoryBitMask & CollisionCategories.Player != 0))  NSLog("Invader and Player Collision Contact")  

Primero declaramos dos variables. FirstBody y SecondBody. Cuando dos objetos hacen contacto, no sabemos qué cuerpo es cuál. Esto significa que primero tenemos que hacer algunas comprobaciones para asegurarnos FirstBody es el que tiene el inferior categoríaBitMask.

A continuación, vamos a través de cada escenario posible utilizando el bitwise Y operador y las categorías de colisión que definimos anteriormente para verificar qué se está haciendo contacto. Registramos el resultado en la consola para asegurarnos de que todo funciona como debería. Si prueba la aplicación, todos los contactos deberían estar funcionando correctamente..

Conclusión

Este fue un tutorial bastante largo, pero ahora los invasores se están moviendo, las balas se disparan desde el jugador y los invasores, y la detección de contactos funciona mediante el uso de máscaras de bits de contacto. Estamos en la recta final hasta el juego final. En la siguiente y última parte de esta serie, tendremos un juego completo.