Introduction
Hi and thanks for following this tutorials so far! This would be the fourth and final part.
So far, if you have followed the previous 3 parts you should have a working simple game.
The current project files are located here: SpriteKit tutorial part 3 files
In this part we’ll look into adding some extra features to our game, to make it a little bit more fancy looking and more fun to play. If you think you’re ready, visit now and discover the best slot games and test your luck.
The Plan
We’ll be looking into:
- adding visual effects
- adding audio effects
- background music
- simulating motion
I might add more later on.
Let’s get to it.
Visual Effects
A game is not fun without visual effects. For our little game it would be nice if the bullets create a little explosion when they hit. For this purpose we’ll use the SKEmitterNode. Usually you’ll end up having more than one, so I would suggest creating a new group, call it Emitters and then right click, create new file and select iOS->Resources->SpriteKit Particle File:
We’ll select the type ‘Fire‘ and call it ‘Explosion1‘
This will create 2 files: explosion1.sks and the spark.png
The png file is the texture that the emitter uses, you could use a custom texture if you want, but in our case we’ll just stick with the spark.
Open the .sks file ( sometimes you won’t see anything – try closing Xcode and re-opening it, if that happens ), feel free to play around with it, however here are the settings I made for our explosion:
We’ll create a new function to generate an explosion, so we can just call it whenever we need it. The function will take on parameter – the point where we are going to need it – in our case the point of contact:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func explosion1(atPoint: CGPoint) { let explosion = SKEmitterNode(fileNamed: "explosion1")! explosion.position = atPoint explosion.name = "explosion1" explosion.xScale = 0.4 explosion.yScale = 0.4 explosion.zPosition = 10 objectsLayer.addChild(explosion) delay(0.2) { self.objectsToRemove.append(explosion) } } |
We are passing a CGPoint, creating the SKEmitterNode at that point, then in 0.2 seconds removing it, adding it to the objectsToRemove array.
Then we’ll call it at the top of the didBeginContact like this:
1 2 3 4 5 6 7 |
func didBeginContact(contact: SKPhysicsContact) { if contact.bodyA.categoryBitMask == bitMasks.hero && contact.bodyB.categoryBitMask == bitMasks.projectileEnemy || contact.bodyB.categoryBitMask == bitMasks.hero && contact.bodyA.categoryBitMask == bitMasks.projectileEnemy { explosion1(contact.contactPoint) .... |
This is enough to add the effect for the enemy bullets.
To add the explosion to the Hero’s bullets, just modify the custom contact method checkIfHit like this:
1 2 3 4 5 6 |
... if distance < 100 { self.explosion1(bullet.position) self.objectsToRemove.append(bullet) print("Enemy got Hit") ... |
We now have explosion effects to our bullets – great !
Let’s go back and change our contact method for the Enemy to be using the gameOver, rather than our Hero’s HP.
Please change the bottom of the didBeginContact like this:
1 2 3 4 5 6 7 8 9 10 11 |
... print("Hero got hit by a projectile !!!") spawnedHero.takeDamage(1) if !gameOver { healthHero.text = "Hero HP: \(spawnedHero.health)" } else { healthHero.text = "Hero is Dead :/" objectsLayer.addChild(restartLabel) ... |
Run your project and you should now have explosions on both type of bullets.
Audio FX and Playing Music
First we need to import a new framework to our game scene. All the way at the top, right below import SpriteKit, add this line:
1 |
import AVFoundation |
Then right before didMoveToView, below the ‘var shootingAllowed: Bool = true‘ add this:
1 2 |
var bgMusicPlayer: AVAudioPlayer! var audioPlayer: AVAudioPlayer! |
We are adding 2 separate players – one for FXs and one for the music.
Feel free to use your own audio files, but I created a simple loop in Garage band and an explosion effect, feel free to download and use them:
Music Loop
Explosion FX
Download the 2 files and drag them into your project.
Now we’ll create our method for playing the music at the bottom of our scene:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
func playMusic(filename: String, loops: Int) { let url = NSBundle.mainBundle().URLForResource( filename, withExtension: nil) if (url == nil) { print("Could not find file: \(filename)") return } var error: NSError? = nil do { bgMusicPlayer = try AVAudioPlayer(contentsOfURL: url!) } catch let error1 as NSError { error = error1 bgMusicPlayer = nil } if bgMusicPlayer == nil { print("Could not create audio player: \(error!)") return } bgMusicPlayer.numberOfLoops = loops bgMusicPlayer.prepareToPlay() bgMusicPlayer.play() } |
We could’ve created a simpler method, but this will allow you to use it for other things as well. We need to call it at the start of our game in didMoveToView like this:
1 |
playMusic("musicBg.m4a", loops: -1) |
Loops: -1 means that it will loop forever.
We also need to stop it and restart it whenever we restart the game, so add this to the end of
your restartGame() method:
1 2 |
bgMusicPlayer.stop() playMusic("musicBg.m4a", loops: -1) |
To play our explosion audio we’ll make a much more simpler method, add this to the bottom of the scene:
1 2 3 4 5 6 |
func audioHit() { let action = SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: true) self.runAction(action, withKey: "hitAudio") } |
Now, where to play it ? Well, the easiest way is in our explosion method, since they should happen at the same time. Add it to the top of the explosion1 method and you should have it looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func explosion1(atPoint: CGPoint) { audioHit() let explosion = SKEmitterNode(fileNamed: "explosion1")! explosion.position = atPoint explosion.name = "explosion1" explosion.xScale = 0.4 explosion.yScale = 0.4 explosion.zPosition = 10 objectsLayer.addChild(explosion) delay(0.2) { self.objectsToRemove.append(explosion) } } |
Play your game and you should now have both the music and the Audio FX on hit!
Simulating motion
Let’s first see about creating a new emitter. This can be used to simulate rain, snow or stars in order to simulate motion in space, just as before create a new emitter, but this time choose type: rain and name it ‘stars’ . Play around with it however these are the settings I got:
We’ll create a new function to add the stars:
1 2 3 4 5 6 7 8 9 |
func callStars() { let stars = SKEmitterNode(fileNamed: "stars.sks")! stars.position = CGPoint(x: frameW/2, y: frameH/2) stars.name = "stars1" stars.zPosition = 1 self.addChild(stars) } |
As you can see I am choosing zPosition to be 1 – above the background image, but underneath everything else ( feel free to just add a new layer to the struct and do it this way… )
Now we only need to call our new function in the didMoveToView, right after the setupBg():
1 2 3 4 5 6 7 8 |
gameScene = self setupLayers() setupBg() callStars() spawnedHero = spawnHero(heroPosition) spawnedEnemy = spawnEnemy(enemyPosition, level: 2) setupLabels() checkIfHit() |
Ok, we got our stars, you could even comment the setupBg() and you’ll see that the stars do create a feeling of motion. If you haven’t changed the default background for your project it’s probably gray, you can change it to black really easily – just select your GameScene.sks and then change it in the Attribute Inspector:
Another great way of simulating motion in a game is to add custom moving objects – like spaceships, planets, buildings, mountains ..etc. So let’s try and do that.
Let’s add some flying asteroids in the background – add these 3 to your project ( curtesy of OpenGameArt.org ) :
I’ll rename my pngs to red.png , grey1.png and grey2.png
Then I am just going to create 3 textures in our global.swift file like this:
1 2 3 |
let textureAsteroidRed = SKTexture(imageNamed: "red.png") let textureAsteroidGrey1 = SKTexture(imageNamed: "grey1.png") let textureAsteroidGrey2 = SKTexture(imageNamed: "grey2.png") |
Then we’ll create our method for calling the Red Asteroid at the bottom of the scene:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func callRed() { let startY = frameH // point A let endY = self.frame.origin.y - 100 // point B let redAsteroid = SKSpriteNode(texture: textureAsteroidRed) redAsteroid.position = CGPoint(x: (screenW / 2) - 100, y: 0) redAsteroid.zPosition = layers.paralax self.addChild(redAsteroid) let path = CGPathCreateMutable() // create a path CGPathMoveToPoint(path, nil, 0, startY) // path is at our point A CGPathAddLineToPoint(path, nil, 0, endY) // add the second point B to our path let followLine = SKAction.followPath(path, asOffset: true, orientToPath: false, duration: 10) // create the follow path action redAsteroid.runAction(followLine) // run the action on our Red Asteroid } |
Call the method in the didMoveToView() and in the restartGame()
Run your game and you should now see your asteroid flying down through the screen.
But what if you wanted to spawn multiple asteroids ? And for them to be with a random texture and size ?
Now that’s a bit more work, but it’s quite achievable 🙂
First we’ll add our own method for a random Int( I’ll also add the one for a random Float ) to the bottom of our scene:
1 2 3 4 5 6 7 |
func randomIntBetweenNumbers(firstNum: Int, secondNum: Int) -> Int{ return firstNum + Int(arc4random_uniform(UInt32(secondNum - firstNum + 1))) } func randomFloatBetweenNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat{ return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum) } |
Now we’ll create our Asteroid spawning action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
func spawnAsteroids() { let asteroidCreateAction = SKAction.runBlock { () -> Void in var randomAsteroid = SKSpriteNode() //create empty SpriteNode switch self.randomIntBetweenNumbers(1, secondNum: 3) { // set it's texture randomly case 1 : randomAsteroid = SKSpriteNode(texture: textureAsteroidRed) case 2: randomAsteroid = SKSpriteNode(texture: textureAsteroidGrey1) default: randomAsteroid = SKSpriteNode(texture: textureAsteroidGrey2) } // get a random X anywhere betwwen 0 and the far right of the game scene let randomPositionX = self.randomIntBetweenNumbers(0, secondNum: Int(frameW)) let startY = frameH // point A let endY = self.frame.origin.y - 100 // point B randomAsteroid.position = CGPoint(x: randomPositionX, y: 0) randomAsteroid.zPosition = layers.paralax objectsLayer.addChild(randomAsteroid) let path = CGPathCreateMutable() // create a path CGPathMoveToPoint(path, nil, 0, startY) // path is at our point A CGPathAddLineToPoint(path, nil, 0, endY) // add the second point B to our path let followLine = SKAction.followPath(path, asOffset: true, orientToPath: false, duration: 4) // create the follow path action randomAsteroid.runAction(followLine) // run the action on our Asteroid } let asteroidWaitAction = SKAction.waitForDuration(3, withRange: 2) let asteroidSequenceAction = SKAction.sequence([asteroidCreateAction,asteroidWaitAction]) let asteroidRepeatAction = SKAction.repeatActionForever(asteroidSequenceAction) objectsLayer.runAction(asteroidRepeatAction, withKey: "asteroidSpawner") } |
I know it might look a bit confusing at first, but everything in this method should already be familiar to you, maybe besides cases , but that’s pretty obvious. You can also read up on ‘flow control’
Basically we are creating one big SKAction that creates a random Asteroid SKSpriteNode and then running a sequence of the Creation Action and a WaitAction. Pretty simple ! 🙂
It you want to use Physics you will need to add a physics body to each Asteroid as well.
Then you can just use the gravity or apply velocity to them instead.
You should be able to figure that on your own by now, but here is how it would look.
Now we’ll also prep some more things to make the Asteroid sizes random as well.
Add this in your global.swift file, below the Asteroid textures:
1 2 3 4 5 6 |
let grey1Width = textureAsteroidGrey1.size().width let grey1Height = textureAsteroidGrey1.size().height let grey2Width = textureAsteroidGrey2.size().width let grey2Height = textureAsteroidGrey2.size().height let redWidth = textureAsteroidRed.size().width let redHeight = textureAsteroidRed.size().height |
Don’t forget to change the gravity ( we set it at the top of the scene ):
1 |
physicsWorld.gravity = CGVectorMake(0.0, -3.0) |
Here is the refined method, in order to use PhysicsBody and Gravity and random sizes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
func spawnAsteroids() { let asteroidCreateAction = SKAction.runBlock { () -> Void in var randomAsteroid = SKSpriteNode() //create empty SpriteNode let randomSizeFactor = self.randomFloatBetweenNumbers(0.3, secondNum: 1.0) // get a random Float let grey1Size = CGSize(width: grey1Width*randomSizeFactor, height: grey1Height*randomSizeFactor) // random size for grey1 let grey2Size = CGSize(width: grey2Width*randomSizeFactor, height: grey2Height*randomSizeFactor) // random size for grey2 let redSize = CGSize(width: redWidth*randomSizeFactor, height: redHeight*randomSizeFactor) // random size for red switch self.randomIntBetweenNumbers(1, secondNum: 3) { // set it's texture randomly case 1 : randomAsteroid = SKSpriteNode(texture: textureAsteroidRed, size: redSize) case 2: randomAsteroid = SKSpriteNode(texture: textureAsteroidGrey1, size: grey1Size) default: randomAsteroid = SKSpriteNode(texture: textureAsteroidGrey2, size: grey2Size) } let randomPositionX = self.randomIntBetweenNumbers(0, secondNum: Int(frameW)) randomAsteroid.position = CGPoint(x: randomPositionX, y: Int(frameH) + 100) randomAsteroid.zPosition = layers.paralax // Yes, I added another layer, named Paralax ( it's 1 ) randomAsteroid.physicsBody = SKPhysicsBody(circleOfRadius: 10*randomSizeFactor) randomAsteroid.physicsBody?.affectedByGravity = true randomAsteroid.physicsBody?.collisionBitMask = bitMasks.noContact randomAsteroid.name = "asteroid" // set the name to remove them later objectsLayer.addChild(randomAsteroid) } let asteroidWaitAction = SKAction.waitForDuration(3, withRange: 2) let asteroidSequenceAction = SKAction.sequence([asteroidCreateAction,asteroidWaitAction]) let asteroidRepeatAction = SKAction.repeatActionForever(asteroidSequenceAction) objectsLayer.runAction(asteroidRepeatAction, withKey: "asteroidSpawner") // Add Random Rotation let randomRotateInt = self.randomIntBetweenNumbers(0, secondNum: 1) if randomRotateInt == 1 { randomAsteroid.runAction(repeatLeftRotate) } else { randomAsteroid.runAction(repeatRightRotate) } } |
Please look at all the notes in the code, at the end I also added a bit of code to add a random rotation to each asteroid. In order to make the rotation work we need to add the rotation actions, I added them to the global.swift :
1 2 3 4 5 |
// ROTATION let rotateRightAction = SKAction.rotateByAngle(5, duration: 6.0) let repeatRightRotate = SKAction.repeatActionForever(rotateRightAction) let rotateLeftAction = SKAction.rotateByAngle(-5, duration: 6.0) let repeatLeftRotate = SKAction.repeatActionForever(rotateLeftAction) |
Don’t forget to enumerate for the asteroids and remove any of them that go out of the screen. If you don’t you’ll end up with a ton of nodes and your performance will drop. Just look up the method we used for removing of the bullets 😉
Run your game and you should have this:
Bonus – moving hero !
Let’s see how to add user interaction to the Hero.
We’ll add two methods – one for moving the hero to wherever we tap on the screen and another to follow your finger if you just like to drag it across. Please test this on a real device.
First method will go in touchesBegan:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if !gameOver { let distanceX = location.x - spawnedHero.position.x let distanceY = location.y - spawnedHero.position.y let newLocation = CGPoint(x: location.x, y: location.y + 50) let distance = CGFloat(sqrt(distanceX*distanceX + distanceY*distanceY)) let speed: CGFloat = 250 let time = distance / speed let timeToTravelDistance = NSTimeInterval(time) let duration: NSTimeInterval = timeToTravelDistance let move = SKAction.moveTo(newLocation, duration: duration) move.timingMode = SKActionTimingMode.EaseInEaseOut spawnedHero.runAction(move) } |
Second method for the dragging needs to go into a method we haven’t used yet – touchesMoved, so you can just add that to the game scene:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { if let touch = touches.first as UITouch! { let location = touch.locationInNode(self) if !gameOver { let distanceX = location.x - spawnedHero.position.x let distanceY = location.y - spawnedHero.position.y let newLocation = CGPoint(x: location.x, y: location.y + 50) let distance = CGFloat(sqrt(distanceX*distanceX + distanceY*distanceY)) let speed: CGFloat = 250 let time = distance / speed let timeToTravelDistance = NSTimeInterval(time) let duration: NSTimeInterval = timeToTravelDistance let move = SKAction.moveTo(newLocation, duration: duration) move.timingMode = SKActionTimingMode.Linear spawnedHero.runAction(move) } } } |
Run your game and you should now be able to move your Hero using your finger! Congratz ! 🙂
You can find the full project here in my repo:
SpriteKit tutorial part 4 files
You can find the previous parts here:
Part 1
Part 2
Part 3
What next ?
You could replace the Asteroid textures with textures of buildings or mountains and have them scroll from right to the left in order to create a typical scrolling game.
What if you wanted to add effects to the bullets ? How about adding a SKParticleEmitter to each bullet ?
Do you think you can make a particle emitter look like a jet engine ? How about a torch ? Play around with the variables to see the results – speeds, gravity, sizes. See what you can do if you se a custom mass to an asteroid with .physicalBody.mass . Mass is a CGFloat.
Let me know if you have questions or comments below! Cheers !