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.
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.
- SpriteKitIntroducing SpriteKit
- SpriteKitSpriteKit Basics: Nodes
- iOS SDKSpriteKit Basics: Sprites
- SpriteKitSpriteKit Basics: Actions and Physics
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.
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.
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.
Name the class StartGameScene, and make sure that Subclass of is set to SKScene and Language is set to Swift.
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.
Make sure that Class is set to Player, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
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.
Make sure that Class is set to Enemy, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
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.
- SwiftCode a Side-Scrolling Game With Swift 3 and SpriteKit
- Game DevelopmentGame Development With Swift and SpriteKit