() translation by (you can also view the original English article)



En las partes previas de ésta serie, hicimos que los invasores se movieran, que el jugador y los invasores dispararan balas, e implementamos la detección de colisión. En la cuarta y última parte de ésta serie, agregaremos la capacidad para mover al jugador usando el acelerómetro, manejar los niveles, y asegurar que el jugador muera cuando es alcanzado por una bala. Comencemos.
1. Finalizando la clase Player
Paso 1: Agregando Propiedades
Agrega las siguientes propiedades a la clase Player
debajo de la propiedad canFire
.
1 |
private var invincible = false |
2 |
private var lives:Int = 3 { |
3 |
didSet { |
4 |
if(lives < 0){ |
5 |
kill() |
6 |
}else{ |
7 |
respawn() |
8 |
}
|
9 |
}
|
10 |
}
|
La propiedad invincible
será usada para hacer al jugador temporalmente invencible cuando pierda una vida. La propiedad lives
es el número de vidas que el jugador tiene antes de morir.
Estamos usando un observador de propiedad en la propiedad lives
, que será llamado cada vez que se establezca su valor. El observador didSet
es llamado inmediatamente después de que se establezca el nuevo valor de la propiedad. Al hacer ésto, cada vez que decrementamos la propiedad lives
automáticamente revisa si lives
es menor a cero, llamando al método kill
si es verdadero. Si al jugador le quedan vidas, el método respawn
es invocado. Observadores de propiedad son muy útiles y pueden ahorra mucho código extra.
Paso 2: respawn
El método respawn
hace al jugador invencible por una pequeña cantidad de tiempo y desaparece y aparece al jugador para indicar que es temporalmente invencible. La implementación del método respawn
se ve así:
1 |
func respawn(){ |
2 |
invincible = true |
3 |
let fadeOutAction = SKAction.fadeOutWithDuration(0.4) |
4 |
let fadeInAction = SKAction.fadeInWithDuration(0.4) |
5 |
let fadeOutIn = SKAction.sequence([fadeOutAction,fadeInAction]) |
6 |
let fadeOutInAction = SKAction.repeatAction(fadeOutIn, count: 5) |
7 |
let setInvicibleFalse = SKAction.runBlock(){ |
8 |
self.invincible = false |
9 |
}
|
10 |
runAction(SKAction.sequence([fadeOutInAction,setInvicibleFalse])) |
11 |
|
12 |
}
|
Establecemos invicible
en true
y creamos un numero de objetos SKAction
. A estas alturas, deberías estar familiarizado con la forma que funciona la clase SKAction
.
Paso 3: die
El método die
es simple. Revisa si invincible
es false
y, si es así, decrementa la variable lives
.
1 |
func die (){ |
2 |
if(invincible == false){ |
3 |
lives -= 1 |
4 |
}
|
5 |
}
|
Paso 4: kill
El método kill
resetea invaderNum
a 1 y regresa al usuario a la StartGameScene para que puedan comenzar un nuevo juego.
1 |
func kill(){ |
2 |
invaderNum = 1 |
3 |
let gameOverScene = StartGameScene(size: self.scene!.size) |
4 |
gameOverScene.scaleMode = self.scene!.scaleMode |
5 |
let transitionType = SKTransition.flipHorizontalWithDuration(0.5) |
6 |
self.scene!.view!.presentScene(gameOverScene,transition: transitionType) |
7 |
}
|
Éste código debería serte familiar pues es casi idéntico al código que usamos para mover a la GameScene desde la StartGameScene. Nota que obligamos a liberar scene
para accesar a las propiedades size
y scaleMode
de la escena.
Ésto completa la clase Player
. Ahora necesitamos llamar a los métodos die
y kill
en el método didBeginContact(_:)
.
1 |
func didBeginContact(contact: SKPhysicsContact) { |
2 |
...
|
3 |
if ((firstBody.categoryBitMask & CollisionCategories.Player != 0) && |
4 |
(secondBody.categoryBitMask & CollisionCategories.InvaderBullet != 0)) { |
5 |
player.die() |
6 |
}
|
7 |
|
8 |
if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && |
9 |
(secondBody.categoryBitMask & CollisionCategories.Player != 0)) { |
10 |
player.kill() |
11 |
}
|
12 |
|
13 |
}
|
Podemos ahora probar todo. Una fórma rápida de probar el método die
es al poner como comentario moveInvaders
en el método update(_:)
. Después de que el jugador muere y reaparece tres veces, deberías ser retornado a StartGameScene.
Para probar el método kill
, asegúrate de que el llamado de moveInvaders
no esté como comentario. Establece la propiedad invaderSpeed
a un valor alto, por ejemplo 200. Los invasores deben llegar al jugador muy rápidamente, lo que resulta en una muerte instantánea. Cambia invaderSpeed
otra vez a 2 una vez que hayas terminado el testeo.
2. Terminando los Invasores que Disparan
Como está el juego en éste momento, los invasores de la fila de abajo pueden disparar balas. Ya tenemos la detección de colisión para cuando la bala de un jugador impacta a un invasor. En éste paso, removeremos un invasor que es impactado por una bala y agregamos al invasor una fila arriba al arreglo de invasores que pueden disparar. Agrega lo siguiente al método didBeginContact(_:)
.
1 |
func didBeginContact(contact: SKPhysicsContact) { |
2 |
...
|
3 |
if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && |
4 |
(secondBody.categoryBitMask & CollisionCategories.PlayerBullet != 0)){ |
5 |
if (contact.bodyA.node?.parent == nil || contact.bodyB.node?.parent == nil) { |
6 |
return
|
7 |
}
|
8 |
|
9 |
let invadersPerRow = invaderNum * 2 + 1 |
10 |
let theInvader = firstBody.node? as Invader |
11 |
let newInvaderRow = theInvader.invaderRow - 1 |
12 |
let newInvaderColumn = theInvader.invaderColumn |
13 |
if(newInvaderRow >= 1){ |
14 |
self.enumerateChildNodesWithName("invader") { node, stop in |
15 |
let invader = node as Invader |
16 |
if invader.invaderRow == newInvaderRow && invader.invaderColumn == newInvaderColumn{ |
17 |
self.invadersWhoCanFire.append(invader) |
18 |
stop.memory = true |
19 |
}
|
20 |
}
|
21 |
}
|
22 |
let invaderIndex = findIndex(invadersWhoCanFire,valueToFind: firstBody.node? as Invader) |
23 |
if(invaderIndex != nil){ |
24 |
invadersWhoCanFire.removeAtIndex(invaderIndex!) |
25 |
}
|
26 |
theInvader.removeFromParent() |
27 |
secondBody.node?.removeFromParent() |
28 |
|
29 |
}
|
30 |
}
|
Hemos removido el comando NSLog
y primero checamos si contact.bodyA.node?.parent
y contact.bodyB.node?.parent
no son nil
. Serán nil
si ya hemos procesado éste contacto. En ese caso, regresamos de la función.
Calculamos el invadersPerRow
como lo hicimos antes y establecemos theInvader
en firstBody.node?
, pasándolo a un Invader
. Posteriormente, obtenemos newInvaderRow
al restar 1 y newInvaderColumn
, que permanece igual.
Solo queremos permitir a invasores que disparen si newInvaderRow
es mayor o igual a 1, de otra manera estaríamos tratando de establecer un invasor en la fila 0 que sea capaz de disparar. No hay fila 0 así que ésto causaría un error.
Posteriormente, enumeramos los invasores, buscando el invasor que tiene la fila y columna correcta. Una vez que es encontrado, lo agregamos al arreglo invadersWhoCanFire
y ponemos stop.memory
en true
, para que la enumeración se detenga pronto.
Necesitamos encontrar al invasor que fue alcanzado con una bala en el arreglo invadersWhoCanFire
así que podemos removerlo. Normalmente, arreglos tienen alguna clase de funcionalidad como un método indexOf
o algo similar para lograr ésto. Al momento de escribir éste artículo, no hay tal método para arreglos en el lenguaje Swift. La librería estándar Swift define una función find
que pudiéramos usar, pero encontré un método en las secciones de genéricos en la Guía del Lenguaje de Programación Swift que lograría lo que necesitamos. La función es propiamente llamada findIndex
. Agrega lo siguiente a la parte de abajo de GameScene.swift.
1 |
func findIndex<T: Equatable>(array: [T], valueToFind: T) -> Int? { |
2 |
for (index, value) in enumerate(array) { |
3 |
if value == valueToFind { |
4 |
return index |
5 |
}
|
6 |
}
|
7 |
return nil |
8 |
}
|
Si eres curioso sobre cómo funciona ésta función, entonces te recomiendo que leas más sobre genéricos en la Guía del Lenguaje de Programación Swift.
Ahora que tenemos un método que podemos usar para encontrar al invasor, lo invocamos, pasando el arreglo invadersWhoCanFire
y theInvader
. Revisamos si invaderIndex
no es igual a nil
y removemos al invasor del arreglo invadersWhoCanFire
usando el método removeAtIndex(index:Int)
.
Ahora puedes probar si funciona como debería. Una forma fácil sería poner como comentario el llamado a player.die
en el método didBeginContact(_:)
. Asegúrate de remover el comentario cuando termines el testeo. Nota que el programa se congela si matas a todos los invasores. Arreglaremos ésto en el próximo paso.
La aplicación se congela, porque tenemos un SKAction
repeatActionForever(_:)
llamando a invasores para disparar balas. A éstas alturas no quedan invasores que disparan balas así que el juego se congela. Podemos arreglar ésto al revisar la propiedad isEmpty
en el arreglo invadersWhoCanFire
. Si el arreglo está vacío, el nivel se terminó. Ingresa lo siguiente en el método fireInvaderBullet
.
1 |
func fireInvaderBullet(){ |
2 |
if(invadersWhoCanFire.isEmpty){ |
3 |
invaderNum += 1 |
4 |
levelComplete() |
5 |
}else{ |
6 |
let randomInvader = invadersWhoCanFire.randomElement() |
7 |
randomInvader.fireBullet(self) |
8 |
}
|
9 |
}
|
El nivel está completo, lo que significa que incrementamos invaderNum
, que es usado para los niveles. También invocamos levelComplete
, que aún necesitamos para crear en los próximos pasos.
3. Completando un Nivel
Necesitamos tener un número definido de niveles. Si no, despues de varias rondas tendremos tantos invasores que no cabrán en la pantalla. Agregamos una propiedad maxLevels
a la clase GameScene
.
1 |
class GameScene: SKScene, SKPhysicsContactDelegate{ |
2 |
...
|
3 |
let player:Player = Player() |
4 |
let maxLevels = 3 |
Ahora agregamos el método levelComplete
en la parte inferior de GameScene.swift.
1 |
func levelComplete(){ |
2 |
if(invaderNum <= maxLevels){ |
3 |
let levelCompleteScene = LevelCompleteScene(size: size) |
4 |
levelCompleteScene.scaleMode = scaleMode |
5 |
let transitionType = SKTransition.flipHorizontalWithDuration(0.5) |
6 |
view?.presentScene(levelCompleteScene,transition: transitionType) |
7 |
}else{ |
8 |
invaderNum = 1 |
9 |
newGame() |
10 |
}
|
11 |
}
|
Primero checamos para ver si invaderNum
es menor o igual a maxLevels
que establecimos. Si es así, transicionamos a la LevelCompleteScene, de otra manera reseteamos invaderNum
a 1 y llamamos newGame
. LevelCompleteScene no existe aún y tampoco el método newGame
así que enfrentemos ésto de una vez en los dos próximos pasos.
4. Implementando la clase LevelCompleteScene
Crea una nueva Clase Cocoa Touch llamada LevelCompleteScene que es una subclase de SKScene
. La implementación de la clase se ve así:
1 |
import Foundation |
2 |
import SpriteKit |
3 |
|
4 |
class LevelCompleteScene:SKScene{ |
5 |
|
6 |
override func didMoveToView(view: SKView) { |
7 |
self.backgroundColor = SKColor.blackColor() |
8 |
let startGameButton = SKSpriteNode(imageNamed: "nextlevelbtn") |
9 |
startGameButton.position = CGPointMake(size.width/2,size.height/2 - 100) |
10 |
startGameButton.name = "nextlevel" |
11 |
addChild(startGameButton) |
12 |
}
|
13 |
|
14 |
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { |
15 |
/* Called when a touch begins */
|
16 |
for touch: AnyObject in touches { |
17 |
let touchLocation = touch.locationInNode(self) |
18 |
let touchedNode = self.nodeAtPoint(touchLocation) |
19 |
if(touchedNode.name == "nextlevel"){ |
20 |
let gameOverScene = GameScene(size: size) |
21 |
gameOverScene.scaleMode = scaleMode |
22 |
let transitionType = SKTransition.flipHorizontalWithDuration(0.5) |
23 |
view?.presentScene(gameOverScene,transition: transitionType) } |
24 |
|
25 |
|
26 |
}
|
27 |
}
|
28 |
}
|
La implementación es idéntica a la clase StartGameScreen
, excepto que establecemos la propiedad name
de startGameButton
en "nextlevel"
. Éste código debería serte familiar. Si no, entonces dirígete a la primera parte de éste tutorial para que lo recuerdes.
5. newGame
El método newGame
simplemente transiciona otra vez a la StartGameScene. Agrega lo siguiente a la parte inferior de GameScene.swift.
1 |
func newGame(){ |
2 |
let gameOverScene = StartGameScene(size: size) |
3 |
gameOverScene.scaleMode = scaleMode |
4 |
let transitionType = SKTransition.flipHorizontalWithDuration(0.5) |
5 |
view?.presentScene(gameOverScene,transition: transitionType) |
6 |
}
|
Si pruebas la aplicación, puedes jugar unos cuantos niveles o perder unos cuantos juegos, pero el jugador no tiene forma de moverse y ésto lo hace un juego aburrido. Arreglemos esto en el próximo paso.
6. Moviendo al Jugador Usando el Acelerómetro
Vamos a querer usar el acelerómetro para mover al jugador. Primero necesitamos importar el framework CoreMotion. Añade un comando import para el framework en la parte superior de GameScene.swift.
1 |
import SpriteKit |
2 |
import CoreMotion |
También necesitamos un par de nuevas propiedades.
1 |
let maxLevels = 3 |
2 |
let motionManager: CMMotionManager = CMMotionManager() |
3 |
var accelerationX: CGFloat = 0.0 |
Después, agregamos un método setupAccelermoeter
en la parte inferior de GameScene.swift.
1 |
func setupAccelerometer(){ |
2 |
motionManager.accelerometerUpdateInterval = 0.2 |
3 |
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue(), withHandler: { |
4 |
(accelerometerData: CMAccelerometerData!, error: NSError!) in |
5 |
let acceleration = accelerometerData.acceleration |
6 |
self.accelerationX = CGFloat(acceleration.x) |
7 |
})
|
8 |
}
|
Aquí establecemos el accelerometerUpdateInterval
, que es el intervalo en segundos para proporcionar actualizaciones al manejador. Encontré que 0.2 funciona bien, puedes intentar diferentes valores si quieres. Dentro del manejador, una clausura o función que maneja variables independientes, obtenemos el accelerometerData.accelereation
, que es una estructura de tipo CMAcceleration
.
1 |
struct CMAcceleration { |
2 |
var x: Double |
3 |
var y: Double |
4 |
var z: Double |
5 |
init() |
6 |
init(x x: Double, y y: Double, z z: Double) |
7 |
}
|
Estamos únicamente interesados en la propiedad x
y usamos conversión de tipo numérica para pasarla a CGFloat
para nuestra propiedad accelerationX
.
Ahora que tenemos definida la propiedad accelerationX
, podemos mover al jugador. Hacemos ésto en el método didSimulatePhysics
. Agrega lo siguiente en la parte inferior de GameScene.swift.
1 |
override func didSimulatePhysics() { |
2 |
player.physicsBody?.velocity = CGVector(dx: accelerationX * 600, dy: 0) |
3 |
}
|
Invoca setupAccelerometer
en didMoveToView(_:)
y deberías poder mover al jugador con el acelerómetro. Sólo hay un problema. El jugador puede moverse fuera de la pantalla a cualquiera de los dos lados y lleva unos cuantos segundos el regresarlo. Podemos arreglarlo usando el motor de física y colisiones. Haremos ésto en el próximo paso.
1 |
override func didMoveToView(view: SKView) { |
2 |
...
|
3 |
setupInvaders() |
4 |
setupPlayer() |
5 |
invokeInvaderFire() |
6 |
setupAccelerometer() |
7 |
}
|
7. Restringiendo el Movimiento del Jugador
Como se mencionó en el paso anterior, el jugador puede moverse fuera de la pantalla. Ésto se soluciona usando el motor de física de Sprite Kit. Primero, agrega una nueva CollisionCategory
llamada EdgeBody
.
1 |
struct CollisionCategories{ |
2 |
static let Invader : UInt32 = 0x1 << 0 |
3 |
static let Player: UInt32 = 0x1 << 1 |
4 |
static let InvaderBullet: UInt32 = 0x1 << 2 |
5 |
static let PlayerBullet: UInt32 = 0x1 << 3 |
6 |
static let EdgeBody: UInt32 = 0x1 << 4 |
7 |
}
|
Establece ésta como la collisionBitMask
del jugador en su método init
.
1 |
override init() { |
2 |
...
|
3 |
self.physicsBody?.categoryBitMask = CollisionCategories.Player |
4 |
self.physicsBody?.contactTestBitMask = CollisionCategories.InvaderBullet | CollisionCategories.Invader |
5 |
self.physicsBody?.collisionBitMask = CollisionCategories.EdgeBody |
6 |
animate() |
7 |
}
|
Finalmente, creamos un physicsBody
en la misma escena. Añade lo siguiente en el método didMoveToView(view: SKView)
en GameScene.swift.
1 |
override func didMoveToView(view: SKView) { |
2 |
self.physicsWorld.gravity=CGVectorMake(0, 0) |
3 |
self.physicsWorld.contactDelegate = self |
4 |
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: frame) |
5 |
self.physicsBody?.categoryBitMask = CollisionCategories.EdgeBody |
6 |
}
|
Inicializamos un cuerpo de física al invocar init(edgeLoopFromRect:)
, pasando frame
de la escena. El inicializador crea un bucle de borde del marco de la escena. Es importante notar que un borde no tiene volumen o masa y siempre es tratado como si la propiedad dinámica fuera igual a false
. Los bordes pueden también colisionar sólo con cuerpos físicos basados en volumen, que es nuestro jugador.
También establecemos categoryBitMast
en CollisionCategories.EdgeBody
. Si pruebas la aplicación, podrías notar que tu nave ya no puede moverse hasta afuera de la pantalla, pero a veces rota. Cuando un cuerpo físico choca con otro cuerpo físico, es posible que ésto resulte en una rotación. Éste es el comportamiento por defecto. Para remediar ésto, establecemos allowsRotation
en false
en Player.swift.
1 |
override init() { |
2 |
...
|
3 |
self.physicsBody?.collisionBitMask = CollisionCategories.EdgeBody |
4 |
self.physicsBody?.allowsRotation = false |
5 |
animate() |
6 |
}
|
8. Campo Estrellas
Paso 1: Crear el Campo de Estrellas
El cuerpo tiene un campo de estrellas en movimiento en el fondo. Podemos crear el campo de inicio usando el generador de partículas de Sprite Kit.
Crea un nuevo archivo y selecciona Resource de la sección iOS. Elige SpriteKit Particle File como la plantilla y da click en Next. Para Particle template elige rain y guárdala como StarField. Da click en Create para abrir el archivo en el editor. Para ver las opciones, abre el SKNode Inspector en la parte derecha.



En lugar de pasar cada ajuste aquí, lo que podría tomar mucho tiempo, sería mejor leer la documentación para aprender sobre cada ajuste individual. No entraré en detalles sobre los ajustes del campo inicial tampoco. Si estás interesado, abre el archivo en Xcode y observa los ajuste que utilicé.
Paso 2: Añadiendo el Campo Estrella a las Escenas
Agrega lo siguiente a didMoveToView(_:)
en StartGameScene.swift.
1 |
override func didMoveToView(view: SKView) { |
2 |
backgroundColor = SKColor.blackColor() |
3 |
let starField = SKEmitterNode(fileNamed: "StarField") |
4 |
starField.position = CGPointMake(size.width/2,size.height/2) |
5 |
starField.zPosition = -1000 |
6 |
addChild(starField) |
7 |
}
|
Usamos un SKEmitterNode
para cargar el archivo StarField.sks, establecemos su position
y le damos una zPosition
baja. La razón para la zPosition
baja es asegurarnos de que no impida al usuario presionar el botón inicio. El sistema de partículas genera cientos de partículas así que al definirlo realmente bajo resolvemos ese problema. Deberías saber también que puedes configurar manualmente todas las propiedades de partículas en un SKEmitterNode
, aunque es mucho más fácil usar el editor para crear un archivo .sks y cargarlo en el runtime.
Ahora, agregamos el campo de estrellas a GameScene.swift y LevelCompleteScene.swift. El código es exactamente el mismo que el de arriba.
9. Implementando la Clase PulsatingText
Paso 1: Crea la Clase PulsatingText
La StartGameScene y LevelCompleteScene tienen texto que crece y decrece reiteradamente. Pondremos una subclase SKLabeNode
y usamos un par de instancias SKAction
para lograr éste efecto.
Crea una nueva Clase Cocoa Touch que es una subclase de SKLabelNode
, nómbrala PulsatingText, y agrégale el siguiente código.
1 |
import UIKit |
2 |
import SpriteKit |
3 |
|
4 |
class PulsatingText : SKLabelNode { |
5 |
|
6 |
func setTextFontSizeAndPulsate(theText: String, theFontSize: CGFloat){ |
7 |
self.text = theText; |
8 |
self.fontSize = theFontSize |
9 |
let scaleSequence = SKAction.sequence([SKAction.scaleTo(2, duration: 1),SKAction.scaleTo(1.0, duration:1)]) |
10 |
let scaleForever = SKAction.repeatActionForever(scaleSequence) |
11 |
self.runAction(scaleForever) |
12 |
}
|
13 |
}
|
Una de las primeras cosas de las que pudieras haber notado es que no hay inicializador. Si tu subclase no define un inicializador designado, automáticamente hereda todos los inicializadores designados de su superclase.
Tenemos un método setTextFontSizeAndPulsate(theText:theFontSize:)
, que hace exactamente lo que dice. Establece las propiedades text
y fontSize
de SKLabelNode
, y crea un número de instancias SKAction
para escalar el texto más grande y luego regresar a su tamaño normal, creando un efecto de desaparecer y aparecer repetidamente.
Paso 2: Agrega PulsatingText
a StartGameScene
Agrega el siguiente código a StartGameScene.swift en didMoveToView(_:)
.
1 |
override func didMoveToView(view: SKView) { |
2 |
backgroundColor = SKColor.blackColor() |
3 |
let invaderText = PulsatingText(fontNamed: "ChalkDuster") |
4 |
invaderText.setTextFontSizeAndPulsate("INVADERZ", theFontSize: 50) |
5 |
invaderText.position = CGPointMake(size.width/2,size.height/2 + 200) |
6 |
addChild(invaderText) |
7 |
}
|
Inicializamos una instancia PulsatingText
, invaderText
, e invocamos setTextFontSizeAndPulsate(theText:theFontSize:)
en él. Posteriormente establecemos su position
y la agregamos a la escena.
Paso 3: Agregamos PulsatingText
a LevelCompleteScene
Agrega el siguiente codigo a LevelCompleteScene.swift en didMoveToView(_:)
.
1 |
override func didMoveToView(view: SKView) { |
2 |
self.backgroundColor = SKColor.blackColor() |
3 |
let invaderText = PulsatingText(fontNamed: "ChalkDuster") |
4 |
invaderText.setTextFontSizeAndPulsate("LEVEL COMPLETE", theFontSize: 50) |
5 |
invaderText.position = CGPointMake(size.width/2,size.height/2 + 200) |
6 |
addChild(invaderText) |
7 |
}
|
Ésto es exactamente los mismo que en el paso anterior. Sólo el texto que estamos pasando es diferente.
10. Llevando el Juego Más Lejos
Ésto completa el juego. Tengo algunas sugerencias de cómo podrías expandir el juego. Dentro del directorio images, hay tres imágenes de invasores diferentes. Cuando estás añadiendo invasores a la escena, eliges al azar una de éstas tres imágenes. Necesitarás actualizar el inicializador del invasor para aceptar una imagen como parámetro. Consulta la clase Bullet
para darte una idea.
Hay también una imagen de un OVNI. Trata de hacer aparecer ésto y moverlo en la escena cada quince segundos aproximadamente. Si el jugador lo golpea, dále una vida extra. Puedes querer limitar el número de vidas que pueden tener si haces ésto. Finalmente, intenta hacer un display para las vidas de los jugadores.
Éstas son sólo algunas sugerencias. Intenta y haz el juego tu sólo.
Conclusión
Ésto cierra la serie. Deberías tener un juego que se asemeja mucho al original de los Invasores del Espacio. Espero que te haya parecido útil éste tutorial y hayas aprendido algo nuevo. Gracias por leer.
¡Sé el primero en conocer las nuevas traducciones–sigue @tutsplus_es en Twitter!