In the previous part of this series, we implemented the stubs for the game's main classes. In this tutorial, we will get the invaders moving, bullets firing for both the invaders and player, and implement collision detection. Let's get started.
1. Moving the Invaders
We will use the scene's update
method to move the invaders. Whenever you want to move something manually, the update
method is generally where you'd want to do this.
Before we do this though, we need to update the rightBounds
property. It was initially set to 0, because we need to use the scene's size
to set the variable. We were unable to do that outside any of the class's methods so we will update this property in the didMoveToView(_:)
method.
override func didMoveToView(view: SKView) { backgroundColor = SKColor.blackColor() rightBounds = self.size.width - 30 setupInvaders() setupPlayer() }
Next, implement the moveInvaders
method below the setupPlayer
method you created in the previous tutorial.
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 } }
We declare a variable, changeDirection
, to keep track when the invaders need to change direction, moving left or moving right. We then use the enumerateChildNodesWithName(usingBlock:)
method, which searches a node’s children and calls the closure once for each matching node it finds with the matching name "invader". The closure accepts two parameters, node
is the node that matches the name
and stop
is a pointer to a boolean variable to terminate the enumeration. We will not be using stop
here, but it is good to know what it is used for.
We cast node
to an SKSpriteNode
instance which invader
is a subclass of, get half its width invaderHalfWidth
, and update its position. We then check if its position
is within the bounds, leftBounds
and rightBounds
, and, if not, we set changeDirection
to true
.
If changeDirection
is true
, we negate invaderSpeed
, which will change the direction the invader moves in. We then enumerate through the invaders and update their y position. Lastly, we set changeDirection
back to false
.
The moveInvaders
method is called in the update(_:)
method.
override func update(currentTime: CFTimeInterval) { moveInvaders() }
If you test the application now, you should see the invaders move left, right, and then down if they reach the bounds that we have set on either side.
2. Firing Invader Bullets
Step 1: fireBullet
Every so often we want one of the invaders to fire a bullet. As it stands now, the invaders in the bottom row are set up to fire a bullet, because they are in the invadersWhoCanFire
array.
When an Invader gets hit by a player bullet, then the invader one row up and in the same column will be added to the invadersWhoCanFire
array, while the invader that got hit will be removed. This way only the bottommost invader of every column can fire bullets.
Add the fireBullet
method to the InvaderBullet
class in 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) let moveBulletAction = SKAction.moveTo(CGPoint(x:self.position.x,y: 0 - bullet.size.height), duration: 2.0) let removeBulletAction = SKAction.removeFromParent() bullet.runAction(SKAction.sequence([moveBulletAction,removeBulletAction])) }
In the fireBullet
method, we instantiate an InvaderBullet
instance, passing in "laser" for imageName
, and because we don't want a sound to play we pass in nil
for bulletSound
. We set its position
to be the same as the invader's, with a slight offset on the y position, and add it to the scene.
We create two SKAction
instances, moveBulletAction
and removeBulletAction
. The moveBulletAction
action moves the bullet to a certain point over a certain duration while the removeBulletAction
action removes it from the scene. By invoking the sequence(_:)
method on these actions, they will run sequentially. This is why I mentioned the waitForDuration
method when playing a sound in the previous part of this series. If you create an SKAction
object by invoking playSoundFileNamed(_:waitForCompletion:)
and set waitForCompletion
to true
, then the duration of that action would be for as long as the sound plays, otherwise it would skip immediately to the next action in the sequence.
Step 2: invokeInvaderFire
Add the invokeInvaderFire
method below the other methods you've created in GameScence.swift.
func invokeInvaderFire(){ let fireBullet = SKAction.runBlock(){ self.fireInvaderBullet() } let waitToFireInvaderBullet = SKAction.waitForDuration(1.5) let invaderFire = SKAction.sequence([fireBullet,waitToFireInvaderBullet]) let repeatForeverAction = SKAction.repeatActionForever(invaderFire) runAction(repeatForeverAction) }
The runBlock(_:)
method of the SKAction
class creates an SKAction
instance and immediately invokes the closure passed to the runBlock(_:)
method. In the closure, we invoke the fireInvaderBullet
method. Because we invoke this method in a closure, we have to use self
to call it.
We then create an SKAction
instance named waitToFireInvaderBullet
by invoking waitForDuration(_:)
, passing in the number of seconds to wait before moving on. Next, we create an SKAction
instance, invaderFire
, by invoking the sequence(_:)
method. This method accepts a collection of action that are invoked by the invaderFire
action. We want this sequence to repeat forever so we create an action named repeatForeverAction
, pass in the SKAction
objects to repeat, and invoke runAction
, passing in the repeatForeverAction
action. The runAction method is declared in the SKNode
class.
Step 3: fireInvaderBullet
Add the fireInvaderBullet
method below the invokeInvaderFire
method you entered in the previous step.
func fireInvaderBullet(){ let randomInvader = invadersWhoCanFire.randomElement() randomInvader.fireBullet(self) }
In this method, we call what seems to be a method named randomElement
that would return a random element out of the invadersWhoCanFire
array, and then call its fireBullet
method. There is, unfortunately, no built in randomElement
method on the Array
structure. However, we can create an Array
extension to provide this functionality.
Step 4: Implement randomElement
Go to File > New > File... and choose Swift File. We are doing something different than before so just make sure you are choosing Swift File and not Cocoa Touch Class. Press Next and name the file Utilities. Add the following to Utilities.swift.
import Foundation extension Array { func randomElement() -> T { let index = Int(arc4random_uniform(UInt32(self.count))) return self[index] } }
We extend the Array
structure to have a method named randomElement
. The arc4random_uniform
function returns a number between 0 and whatever you pass in. Because Swift doesn't implicitly convert numeric types, we must do the conversion ourselves. Finally, we return the element of the array at index index
.
This example illustrates how easy it is to add functionality to the structure and classes. You can read more about creating extensions in the The Swift Programming Language.
Step 5: Firing the Bullet
With all this out of the way, we can now fire the bullets. Add the following to the didMoveToView(_:)
method.
override func didMoveToView(view: SKView) { ... setupPlayer() invokeInvaderFire() }
If you test the application now, every second or so you should see one of the invaders from the bottom row fire a bullet.
3. Firing Player Bullets
Step 1: fireBullet(scene:)
Add the following property to the Player
class in Player.swift.
class Player: SKSpriteNode { private var canFire = true
We want to limit how often the player can fire a bullet. The canFire
property will be used to regulate that. Next, add the following to the fireBullet(scene:)
method in the Player
class.
func fireBullet(scene: 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) let moveBulletAction = SKAction.moveTo(CGPoint(x:self.position.x,y:scene.size.height + bullet.size.height), duration: 1.0) let removeBulletAction = SKAction.removeFromParent() bullet.runAction(SKAction.sequence([moveBulletAction,removeBulletAction])) let waitToEnableFire = SKAction.waitForDuration(0.5) runAction(waitToEnableFire,completion:{ self.canFire = true }) } }
We first make sure the player is able to fire by checking if canFire
is set to true
. If it isn't, we immediately return from the method.
If the player can fire, we set canFire
to false
so they cannot immediately fire another bullet. We then instantiate a PlayerBullet
instance, passing in "laser" for the imageNamed
parameter. Because we want a sound to play when the player fires a bullet, we pass in "laser.mp3" for the bulletSound
parameter.
We then set the bullet's position and add it to the screen. The next few lines are the same as the Invader
's fireBullet
method in that we move the bullet and remove it from the scene. Next, we create an SKAction
instance, waitToEnableFire
, by invoking the waitForDuration(_:)
class method. Lastly, we invoke runAction
, passing in waitToEnableFire
, and on completion set canFire
back to true
.
Step 2: Firing the Player Bullet
Whenever the user touches the screen, we want to fire a bullet. This is as simple as calling fireBullet
on the player
object in the touchesBegan(_:withEvent:)
method of the GameScene
class.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { player.fireBullet(self) }
If you test the application now, you should be able to fire a bullet when you tap the screen. Also, you should hear the laser sound every time a bullet is fired.
4. Collision Categories
To detect when nodes are colliding or making contact with each other, we will use Sprite Kit's built-in physics engine. However, the default behavior of the physics engine is that everything collides with everything when they have a physics body added to them. We need a way to separate what we want interacting with each other and we can do this by creating categories to which specific physic bodies belong.
You define these categories using a bit mask that uses a 32-bit integer with 32 individual flags that can be either on or off. This also means you can only have a maximum of 32 categories for your game. This should not present a problem for most games, but it is something to keep in mind.
Add the following structure definition to the GameScene
class, below the invaderNum
declaration in GameScene.swift.
struct CollisionCategories{ static let Invader : UInt32 = 0x1 << 0 static let Player: UInt32 = 0x1 << 1 static let InvaderBullet: UInt32 = 0x1 << 2 static let PlayerBullet: UInt32 = 0x1 << 3 }
We use a structure, CollsionCategories
, to create categories for the Invader
, Player
, InvaderBullet
, and PlayerBullet
classes. We are using bit shifting to turn the bits on.
5. Player
and InvaderBullet
Collision
Step 1: Setting Up InvaderBullet
for Collision
Add the following code block to the init(imageName:bulletSound:)
method in InvaderBullet.swift.
override init(imageName: String, bulletSound:String?){ super.init(imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody(texture: self.texture, size: self.size) self.physicsBody?.dynamic = true self.physicsBody?.usesPreciseCollisionDetection = true self.physicsBody?.categoryBitMask = CollisionCategories.InvaderBullet self.physicsBody?.contactTestBitMask = CollisionCategories.Player self.physicsBody?.collisionBitMask = 0x0 }
There are several ways to create a physics body. In this example, we use the init(texture:size:)
initializer, which will make the collision detection use the shape of the texture we pass in. There are several other initializers available, which you can see in the SKPhysicsBody class reference.
We could easily have used the init(rectangleOfSize:)
initializer, because the bullets are rectangular in shape. In a game this small it does not matter. However, be aware that using the init(texture:size:)
method can be computationally expensive since it has to calculate the exact shape of the texture. If you have objects that are rectangular or circular in shape, then you should use those types of initializers if the game's performance is becoming a problem.
For collision detection to work, at least one of the bodies you are testing has to be marked as dynamic. By setting the usesPreciseCollisionDetection
property to true
, Sprite Kit uses a more precise collision detection. Set this property to true
on small, fast moving bodies like our bullets.
Each body will belong to a category and you define this by setting its categoryBitMask
. Since this is the InvaderBullet
class, we set it to CollisionCategories.InvaderBullet
.
To tell when this body has made contact with another body that you are interested in, you set the contactBitMask
. Here we want to know when the InvaderBullet
has made contact with the player so we use CollisionCategories.Player
. Because a collision shouldn't trigger any physics forces, we set collisionBitMask
to 0x0
.
Step 2: Setting Up Player
for Collsion
Add the following to the init
method in Player.swift.
override init() { let texture = SKTexture(imageNamed: "player1") super.init(texture: texture, color: SKColor.clearColor(), size: texture.size()) self.physicsBody = SKPhysicsBody(texture: self.texture,size:self.size) self.physicsBody?.dynamic = true self.physicsBody?.usesPreciseCollisionDetection = false self.physicsBody?.categoryBitMask = CollisionCategories.Player self.physicsBody?.contactTestBitMask = CollisionCategories.InvaderBullet | CollisionCategories.Invader self.physicsBody?.collisionBitMask = 0x0 animate() }
Much of this should be familiar from the previous step so I will not rehash it here. There are two differences to notice though. One is that usesPreciseCollsionDetection
has been set to false
, which is the default. It is important to realize that only one of the contacting bodies needs this property set to true
(which was the bullet). The other difference is that we also want to know when the player contacts an invader. You can have more than one contactBitMask
category by separating them with the bitwise or (|
) operator. Other than that, you should notice it is just basically opposite from the InvaderBullet
.
6. Invader
and PlayerBullet
Collision
Step 1: Setting Up Invader
for Collision
Add the following to the init
method in Invader.swift.
override init() { let texture = SKTexture(imageNamed: "invader1") super.init(texture: texture, color: SKColor.clearColor(), size: texture.size()) self.name = "invader" self.physicsBody = SKPhysicsBody(texture: self.texture, size: self.size) self.physicsBody?.dynamic = true self.physicsBody?.usesPreciseCollisionDetection = false self.physicsBody?.categoryBitMask = CollisionCategories.Invader self.physicsBody?.contactTestBitMask = CollisionCategories.PlayerBullet | CollisionCategories.Player self.physicsBody?.collisionBitMask = 0x0 }
This should all make sense if you've been following along. We set up the physicsBody
, categoryBitMask
, and contactBitMask
.
Step 2: Setting Up PlayerBullet
for Collision
Add the following to the init(imageName:bulletSound:)
in PlayerBullet.swift. Again, the implementation should be familiar by now.
override init(imageName: String, bulletSound:String?){ super.init(imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody(texture: self.texture, size: self.size) self.physicsBody?.dynamic = true self.physicsBody?.usesPreciseCollisionDetection = true self.physicsBody?.categoryBitMask = CollisionCategories.PlayerBullet self.physicsBody?.contactTestBitMask = CollisionCategories.Invader self.physicsBody?.collisionBitMask = 0x0 }
7. Setting Up Physics for GameScene
Step 1: Configuring Physics World
We have to set up the GameScene
class to implement the SKPhysicsContactDelegate
so we can respond when two bodies collide. Add the following to make the GameScene
class conform to the SKPhysicsContactDelegate
protocol.
class GameScene: SKScene, SKPhysicsContactDelegate{
Next, we have to set up some properties on the scene's physicsWorld
. Enter the following at the top of the didMoveToView(_:)
method in GameScene.swift.
override func didMoveToView(view: SKView) { self.physicsWorld.gravity = CGVectorMake(0, 0) self.physicsWorld.contactDelegate = self ... }
We set the gravity
property of physicsWorld
to 0 so that none of the physics bodies in the scene are affected by gravity. You can also do this on a per body basis instead of setting the whole world to have no gravity by setting the affectedByGravity
property. We also set the contactDelegate
property of the physics world to self
, the GameScene
instance.
Step 2: Implementing SKPhysicsContactDelegate
Protocol
To conform the GameScene
class to SKPhysicsContactDelegate
protocol, we need to implement the didBeginContact(_:)
method. This method is called when two bodies make contact. The implementation of the didBeginContact(_:)
method looks like this.
func didBeginContact(contact: SKPhysicsContact) { var firstBody: SKPhysicsBody var secondBody: SKPhysicsBody if 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") } }
We first declare two variables firstBody
and secondBody
. When two objects make contact, we don't know which body is which. This means that we first need to make some checks to make sure firstBody
is the one with the lower categoryBitMask
.
Next, we go through each possible scenario using the bitwise &
operator and the collision categories we defined earlier to check what is making contact. We log the result to the console to make sure everything is working as it should. If you test the application, all contacts should be working correctly.
Conclusion
This was a rather long tutorial, but we now have the invaders moving, bullets being fired from both the player and invaders, and contact detection working by using contact bit masks. We are on the home stretch to the final game. In the next and final part of this series, we will have a completed game.
Comments