SpriteKit Basics: Putting It All Together

In this post we’ll build a simple game from scratch. Along the way, we’ll touch on some of the most important aspects of the SpriteKit library.

SpriteKit Basics: Putting It All Together

SpriteKit Basics: Putting It All Together
This post builds on what we’ve learned earlier in the SpriteKit Basics series. If you want to refresh your SpriteKit knowledge, take a look at some of my other posts.

  • SpriteKit Basics: Putting It All Together
    SpriteKit
    Introducing SpriteKit
    James Tyner
  • SpriteKit Basics: Putting It All Together
    SpriteKit
    SpriteKit Basics: Nodes
    James Tyner
  • SpriteKit Basics: Putting It All Together
    iOS SDK
    SpriteKit Basics: Sprites
    James Tyner
  • SpriteKit Basics: Putting It All Together
    SpriteKit
    SpriteKit Basics: Actions and Physics
    James Tyner

New Project

Open Xcode and start a new project from the menu File > New > Project. Make sure iOS is selected and choose Game as your template.

SpriteKit Basics: Putting It All Together

Give your project a name, and make sure that Language is set to Swift, Game Technology is set to SpriteKit, and Devices is set to iPad.

SpriteKit Basics: Putting It All Together

Planning the Game Scenes

One of the first things I like to do when creating a project is to determine how many scenes I will need for the project. I will usually have at least three scenes: an intro scene, a main game scene, and a scene to show high scores, etc.

For this example, we just need an intro and main gameplay scene since we won’t be keeping track of lives, scores, etc. SpriteKit already comes with one scene when you create a new project, so we just need an intro scene.

From Xcode’s menu, choose File > New > File. Make sure iOS is selected, and choose Cocoa Touch Class.

SpriteKit Basics: Putting It All Together

Name the class StartGameScene, and make sure that Subclass of is set to SKScene and Language is set to Swift.

SpriteKit Basics: Putting It All Together

Setting Up GameViewController

Open GameViewController.swift. Delete everything in that file and replace it with the following.

import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = StartGameScene(size: view.bounds.size)
        let skView = self.view as! SKView
        skView.showsFPS = false
        skView.showsNodeCount = false
        skView.ignoresSiblingOrder = false
        scene.scaleMode = .aspectFill
        skView.presentScene(scene)
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
}

When you create a new project, GameViewController.swift is set up to load GameScene.sks from disk. GameScene.sks is used along with SpriteKit’s built-in scene editor, which allows you to visually lay out your projects. We will not be using GameScene.sks, and will instead create everything from code, so here we initiate a new instance of StartGameScene and present it.

Create the Intro Scene

Add the following to the newly created StartGameScene.swift.

import UIKit
import SpriteKit

class StartGameScene: SKScene {

    override func didMove(to view: SKView){
        scene?.backgroundColor = .blue
        let logo = SKSpriteNode(imageNamed: "bigplane")
        logo.position = CGPoint(x: size.width/2, y: size.height/2)
        addChild(logo)
        
        let newGameBtn = SKSpriteNode(imageNamed: "newgamebutton")
        newGameBtn.position = CGPoint(x: size.width/2, y: size.height/2 - 350)
        newGameBtn.name = "newgame"
        addChild(newGameBtn)        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else {
            return
        }
        
        let touchLocation = touch.location(in: self)
        let touchedNode = self.atPoint(touchLocation)
        
        if(touchedNode.name == "newgame"){
            let newScene = GameScene(size: size)
            newScene.scaleMode = scaleMode
            view?.presentScene(newScene)       
        }
    }}

This scene is pretty simple. In the didMove method, we add a logo and a button. Then, in touchesBegan, we detect touches on the new game button and respond by loading the main scene GameScene.

Planning Game Classes

The next thing I like to do when creating a new game is decide which classes I will need. I can tell right away that I will need a Player class and an Enemy class. Both of these classes will extend SKSpriteNode. I think for this project we will just create the player and enemy bullets right from within their respective classes. You could make separate player bullet and enemy bullet classes if you prefer, and I suggest you try to do that as an exercise on your own.

Lastly, there are the islands. These do not have any specific functionality but to move down the screen. In this case, since they’re just decorations, I think it’s also okay not to create a class, and instead just create them in the main GameScene.

Creating the Player Class

From Xcode’s menu, choose File > New > File.  Make sure iOS is selected and choose Cocoa Touch Class.

SpriteKit Basics: Putting It All Together

Make sure that Class is set to Player, Subclass of: is set to SKSpriteNode, and Language is set to Swift.

SpriteKit Basics: Putting It All Together

Now add the following to Player.swift.

import UIKit
import SpriteKit

class Player: SKSpriteNode {
    private var canFire = true
    private var invincible = false
    private var lives:Int = 3 {
        didSet {
            if(lives < 0){
                kill()
            }else{
                respawn()
            }
        }
    }
    init() {
        let texture = SKTexture(imageNamed: "player")
        super.init(texture: texture, color: .clear, size: texture.size())
        self.physicsBody = SKPhysicsBody(texture: self.texture!,size:self.size)
        self.physicsBody?.isDynamic = true
        self.physicsBody?.categoryBitMask = PhysicsCategories.Player
        self.physicsBody?.contactTestBitMask = PhysicsCategories.Enemy | PhysicsCategories.EnemyBullet
        self.physicsBody?.collisionBitMask = PhysicsCategories.EdgeBody
        self.physicsBody?.allowsRotation = false
        generateBullets()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
   
    func die (){
        if(invincible == false){
            lives -= 1
        }
    }
        
    func kill(){
        let newScene = StartGameScene(size: self.scene!.size)
        newScene.scaleMode = self.scene!.scaleMode
        let doorsClose = SKTransition.doorsCloseVertical(withDuration: 2.0)
        self.scene!.view?.presentScene(newScene, transition: doorsClose)
    }
    
    func respawn(){
        invincible = true
        let fadeOutAction = SKAction.fadeOut(withDuration: 0.4)
        let fadeInAction = SKAction.fadeIn(withDuration: 0.4)
        let fadeOutIn = SKAction.sequence([fadeOutAction,fadeInAction])
        let fadeOutInAction = SKAction.repeat(fadeOutIn, count: 5)
        let setInvicibleFalse = SKAction.run {
            self.invincible = false
        }
        run(SKAction.sequence([fadeOutInAction,setInvicibleFalse]))        
    }
    
    func generateBullets(){
        let fireBulletAction = SKAction.run{ [weak self] in
            self?.fireBullet()
        }
        let waitToFire = SKAction.wait(forDuration: 0.8)
        let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
        let fire = SKAction.repeatForever(fireBulletSequence)
        run(fire)
    }
    
    func fireBullet(){
        let bullet = SKSpriteNode(imageNamed: "bullet")
        bullet.position.x = self.position.x
        bullet.position.y = self.position.y + self.size.height/2
        bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
        bullet.physicsBody?.categoryBitMask = PhysicsCategories.PlayerBullet
        bullet.physicsBody?.allowsRotation = false
        scene?.addChild(bullet)
        let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y:(scene?.size.height)! + bullet.size.height), duration: 1.0)
        let removeBulletAction = SKAction.removeFromParent()
        bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction]))
    }
}

Within the init() method, we set up the physicsBody and invoke generateBullets(). The generateBullets method repeatedly calls fireBullet(), which creates a bullet, sets its physicsBody, and moves it down the screen.

When the player loses a life, the respawn() method is invoked. Within the respawn method, we fade the plane in and out five times, during which time the player will be invincible. One the player has exhausted all the lives, the kill() method is invoked. The kill method simply loads the StartGameScene.

Creating the Enemy Class

Choose File > New > File from Xcode’s menu. Make sure iOS is selected and choose Cocoa Touch Class.

SpriteKit Basics: Putting It All Together

Make sure that Class is set to Enemy, Subclass of: is set to SKSpriteNode, and Language is set to Swift.

SpriteKit Basics: Putting It All Together

Add the following to Enemy.swift.

import UIKit
import SpriteKit

class Enemy: SKSpriteNode {

    init() {
        let texture = SKTexture(imageNamed: "enemy1")
        super.init(texture: texture, color: .clear, size: texture.size())
        self.name = "enemy"
        self.physicsBody = SKPhysicsBody(texture: self.texture!, size: self.size)
        self.physicsBody?.isDynamic = true
        self.physicsBody?.categoryBitMask = PhysicsCategories.Enemy
        self.physicsBody?.contactTestBitMask = PhysicsCategories.Player | PhysicsCategories.PlayerBullet
        self.physicsBody?.allowsRotation = false
        move()
        generateBullets()        
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    func fireBullet(){
        let bullet = SKSpriteNode(imageNamed: "bullet")
        bullet.position.x = self.position.x
        bullet.position.y = self.position.y - bullet.size.height * 2
        bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
        bullet.physicsBody?.categoryBitMask = PhysicsCategories.EnemyBullet
        bullet.physicsBody?.allowsRotation = false
        scene?.addChild(bullet)
        let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y: 0 - bullet.size.height), duration: 2.0)
        let removeBulletAction = SKAction.removeFromParent()
        bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction])
        )
    }
    
    func move(){   
        let moveEnemyAction = SKAction.moveTo(y: 0 - self.size.height, duration: 12.0)
        let removeEnemyAction = SKAction.removeFromParent()
        let moveEnemySequence = SKAction.sequence([moveEnemyAction, removeEnemyAction])
        run(moveEnemySequence)
    }
    
    func generateBullets(){
        let fireBulletAction = SKAction.run{ [weak self] in
            self?.fireBullet()
        }
        let waitToFire = SKAction.wait(forDuration: 1.5)
        let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
        let fire = SKAction.repeatForever(fireBulletSequence)
        run(fire)
    }
}

This class is pretty similar to the Player class. We set its physicsBody and invoke generateBullets(). The move() simply moves the enemy down the screen.

Creating the Main Game Scene

Delete everything within GameScene.swift and add the following.

import SpriteKit
import GameplayKit
import CoreMotion

class GameScene: SKScene, SKPhysicsContactDelegate {
    let player = Player()
    let motionManager = CMMotionManager()
    var accelerationX: CGFloat = 0.0
    
    override func didMove(to view: SKView) {
        physicsWorld.gravity = CGVector(dx:0.0, dy:0.0)
        self.physicsWorld.contactDelegate = self
        scene?.backgroundColor = .blue
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        physicsBody?.categoryBitMask = PhysicsCategories.EdgeBody
        player.position = CGPoint(x: size.width/2, y: player.size.height)
        addChild(player)
        setupAccelerometer()
        addEnemies()
        generateIslands()
    }
        
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {    
    }
    
    func addEnemies(){
        let generateEnemyAction = SKAction.run{ [weak self] in
            self?.generateEnemy()
        }
        let waitToGenerateEnemy = SKAction.wait(forDuration: 3.0)
        let generateEnemySequence = SKAction.sequence([generateEnemyAction,waitToGenerateEnemy])
        run(SKAction.repeatForever(generateEnemySequence))
    }
    
    func generateEnemy(){
        let enemy = Enemy()
        addChild(enemy)
        enemy.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width - enemy.size.width))), y: size.height - enemy.size.height)
    }
    
    func didBegin(_ 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 & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.Enemy != 0)){
            player.die()
            secondBody.node?.removeFromParent()
            createExplosion(position: player.position)            
        }
                
        if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EnemyBullet != 0)){
            player.die()
            secondBody.node?.removeFromParent()            
        }
        if((firstBody.categoryBitMask & PhysicsCategories.Enemy != 0) && (secondBody.categoryBitMask & PhysicsCategories.PlayerBullet != 0)){
            if(firstBody.node != nil){
                createExplosion(position: (firstBody.node?.position)!)
            }
            firstBody.node?.removeFromParent()
            secondBody.node?.removeFromParent()            
        }
    }
    
    func createExplosion(position: CGPoint){
        let explosion = SKSpriteNode(imageNamed: "explosion1")
        explosion.position = position
        addChild(explosion)
        var explosionTextures:[SKTexture] = []
        
        for i in 1...6 {
            explosionTextures.append(SKTexture(imageNamed: "explosion(i)"))
        }
        
        let explosionAnimation = SKAction.animate(with: explosionTextures,
                                                  timePerFrame: 0.3)
        explosion.run(SKAction.sequence([explosionAnimation, SKAction.removeFromParent()]))
    }
    
    func  createIsland() {
        let island = SKSpriteNode(imageNamed: "island1")

        island.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width - island.size.width))), y: size.height - island.size.height - 50)
        island.zPosition = -1
        addChild(island)
        let moveAction = SKAction.moveTo(y: 0 - island.size.height, duration: 15)
        island.run(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
    }
    
    func generateIslands(){
        let generateIslandAction = SKAction.run { [weak self] in
            self?.createIsland()
        }
        let waitToGenerateIslandAction = SKAction.wait(forDuration: 9)
        run(SKAction.repeatForever(SKAction.sequence([generateIslandAction, waitToGenerateIslandAction])))
    }
        
    func setupAccelerometer(){
        motionManager.accelerometerUpdateInterval = 0.2
        motionManager.startAccelerometerUpdates(to: OperationQueue(), withHandler: { accelerometerData, error in
        guard let accelerometerData = accelerometerData else {
            return
        }
            let acceleration = accelerometerData.acceleration
            self.accelerationX = CGFloat(acceleration.x)
        })
    }
    
    override func didSimulatePhysics() {        
        player.physicsBody?.velocity = CGVector(dx: accelerationX * 600, dy: 0)
    }
}

We create an instance of Player and an instance of CMMotionManager. We are using the accelerometer to move the player in this game.

Within the didMove(to:) method we turn off the gravity, set up the contactDelegate, add an edge loop, and set the player‘s position before adding it to the scene. We then invoke setupAccelerometer(), which sets up the accelerometer, and invoke the addEnemies() and generateIslands() methods.

The addEnemies() method repeatedly calls the generateEnemy() method, which will create an instance of Enemy and add it to the scene.

The generateIslands() method works similarly to the addEnemies() method in that it repeatedly calls createIsland() which creates an SKSpriteNode and adds it to the scene. Within createIsland(), we also create an SKAction that moves the island down the scene.

Within the didBegin(_:) method, we check to see which nodes are making contact and respond by removing the appropriate node from the scene and invoking player.die() if necessary. The createExplosion() method creates an explosion animation and adds it to the scene. Once the explosion is finished, it is removed from the scene.

Conclusion

During this series, we learned some of the most important concepts used in almost all SpriteKit games. We ended the series by showing how simple it is to get a basic game up and running. There are still some improvements that could be made, like a HUB, high scores, and sounds (I included a couple of MP3s you can use for this in the repo). I hope you learned something useful throughout this series, and thanks for reading!

If you want to learn more about game programming with SpriteKit, check out one of our comprehensive video courses! You’ll learn how to build a SpriteKit game from A to Z.

  • SpriteKit Basics: Putting It All Together
    Swift
    Code a Side-Scrolling Game With Swift 3 and SpriteKit
    Derek Jensen
  • SpriteKit Basics: Putting It All Together
    Game Development
    Game Development With Swift and SpriteKit
    Derek Jensen