File\New\Project\..., orNew Project iOS/Game template and click Next
Pikachu for Product Name Swift for Language,SpriteKit for Game Technology.None for Testing System
iPhone 16 and start the simulation
GameScene.sks and Actions.sks GameScene.swift has only the followings:
1
2
3
4
5
6
7
8
9
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
}
}
GameViewController.swift has the following contents:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var prefersStatusBarHidden: Bool {
return true
}
}
GameViewController.swift/viewDidLoad(), edit the function with the followings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
override func viewDidLoad() {
super.viewDidLoad()
//create the view
if let view = self.view as! SKView? {
//create the scene
let scene = GameScene(size: CGSize(width: 1336, height: 1024))
//set the scale mode to scale to fill the view window
scene.scaleMode = .aspectFill
//set the background colo
scene.backgroundColor = UIColor(red: 105/255, green: 157/255, blue: 181/255, alpha: 1.0)
//present the scene
view.presentScene(scene)
//set the view options
view.ignoresSiblingOrder = false
view.showsPhysics = false
view.showsFPS = true
view.showsNodeCount = true
}
}
didMove in GameScene.swift to have the following contents
1
2
3
4
5
6
7
8
9
10
11
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "background")
background.anchorPoint = CGPoint(x:0, y:0)
background.position = CGPoint(x:10, y:330)
addChild(background)
let foreground = SKSpriteNode(imageNamed: "foreground")
foreground.anchorPoint = CGPoint(x: 0, y: 0)
foreground.position = CGPoint(x: 15, y: 158)
addChild(foreground)
}
SpriteKitHelper.swift to set Z Position
1
2
3
4
5
6
7
8
import Foundation
import SpriteKit
enum Layer:CGFloat {
case background
case foreground
case player
}
Player.swift with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
import SpriteKit
class Player:SKSpriteNode {
// MARK: - PROPERTIES
// MARK: - INT
init() {
// set default textture
let texture = SKTexture(imageNamed: "frame_0")
// call to super.init
super.init(texture: texture, color: .clear, size: texture.size())
self.name = "player"
self.setScale(1.0)
self.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.zPosition = Layer.player.rawValue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
didMove function in GameScene
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let player = Player()
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "background")
background.anchorPoint = CGPoint(x:0, y:0)
background.position = CGPoint(x:10, y:330)
background.zPosition = Layer.background.rawValue
addChild(background)
let foreground = SKSpriteNode(imageNamed: "foreground")
foreground.anchorPoint = CGPoint(x: 0, y: 0)
foreground.position = CGPoint(x: 15, y: 158)
foreground.zPosition = Layer.foreground.rawValue
addChild(foreground)
player.position = CGPoint(x: size.width/2, y: foreground.frame.maxY)
addChild(player)
}
Player.swift:
1
2
3
4
func moveToPosition(pos: CGPoint, speed: TimeInterval) {
let moveAction = SKAction.move(to: pos, duration: speed)
run(moveAction)
}
GameScene.swift
1
let player = Player()
1
2
3
func touchDown (atPoint pos: CGPoint) {
player.moveToPosition(pos: pos, speed: 1.0)
}
1
2
3
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{self.touchDown(atPoint: t.location(in: self))}
}
1
2
3
4
5
func setupConstraints(floor: CGFloat) {
let range = SKRange(lowerLimit: floor, upperLimit: floor)
let lockToPlatform = SKConstraint.positionY(range)
constraints = [lockToPlatform]
}
1
player.setupConstraints(floor: foreground.frame.maxY)
moveToPosition() in Player.swift
1
2
3
4
5
6
7
8
9
10
func moveToPosition(pos: CGPoint, direction: String, speed: TimeInterval) {
switch direction {
case "L":
xScale = -abs(xScale)
default:
xScale = abs(xScale)
}
let moveAction = SKAction.move(to: pos, duration: speed)
run(moveAction)
}
touchDown() in GameScene.swift
1
2
3
4
5
6
7
func touchDown (atPoint pos: CGPoint) {
if pos.x < player.position.x {
player.moveToPosition(pos: pos, direction: "L", speed: 1.0)
} else {
player.moveToPosition(pos: pos, direction: "R", speed: 1.0)
}
}
SpriteKitHelper.swift
1
2
3
4
5
6
enum Layer:CGFloat {
case background
case foreground
case player
case collectible
}
Collectible.swift with the following contents
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
import Foundation
import SpriteKit
enum CollectibleType: String {
case none
case fruit
}
class Collectible: SKSpriteNode {
private var collectibleType: CollectibleType = .none
init(collectibleType: CollectibleType) {
var texture: SKTexture!
self.collectibleType = collectibleType
//set the texture based on the Type
switch self.collectibleType {
case .fruit:
texture = SKTexture(imageNamed: "fruit")
case .none:
break
}
super.init(texture: texture, color: SKColor.clear, size: texture.size())
//set up the collectible
self.name = "co_\(collectibleType)"
self.anchorPoint = CGPoint(x: 0.5, y: 1.0)
self.setScale(0.3)
self.zPosition = Layer.collectible.rawValue
}
required init?(coder aDecoder: NSCoder){
fatalError("init(coder:) has not been implemented")
}
}
didMove() inside GameScene.swift
1
2
3
4
5
func spawnFruit() {
let collectible = Collectible(collectibleType: CollectibleType.fruit)
collectible.position = CGPoint(x: player.position.x, y:player.position.y * 2.5)
addChild(collectible)
}
didMove()
1
2
3
...
spawnFruit()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
func drop(dropSpeed: TimeInterval, floorLevel: CGFloat) {
let pos = CGPoint(x: position.x, y: floorLevel)
let scaleX = SKAction.scaleX(to: 0.5, duration: 1)
let scaleY = SKAction.scaleY(to: 0.5, duration: 1)
let scale = SKAction.group([scaleX, scaleY])
let appear = SKAction.fadeAlpha(to: 1, duration: 0.25)
let moveAction = SKAction.move(to: pos, duration: dropSpeed)
let actionSequence = SKAction.sequence([appear, scale, moveAction])
self.scale(to: CGSize(width: 0.25, height: 1.0))
self.run(actionSequence, withKey: "drop")
}
spawnFruit() function inside GameScene.swift
1
collectible.drop(dropSpeed: TimeInterval(1.0), floorLevel: player.frame.minY)
spawnFruit() function in GameScene.swift
1
2
3
4
5
6
7
8
9
10
11
func spawnFruit() {
let collectible = Collectible(collectibleType: CollectibleType.fruit)
//set random position
let margin = collectible.size.width * 2
let dropRange = SKRange(lowerLimit: frame.minX + margin, upperLimit: frame.maxX - margin)
let randomX = CGFloat.random(in: dropRange.lowerLimit...dropRange.upperLimit)
collectible.position = CGPoint(x: randomX, y:player.position.y * 2.5)
addChild(collectible)
collectible.drop(dropSpeed: TimeInterval(1.0), floorLevel: player.frame.minY)
}
spawnMultipleFruits() in GameScene below spawnFruit()
1
2
3
4
5
6
7
8
9
func spawnMultipleFruits() {
let wait = SKAction.wait(forDuration: TimeInterval(1.0))
let spawn = SKAction.run {
self.spawnFruit()
}
let sequence = SKAction.sequence([wait, spawn])
let repeatAction = SKAction.repeat(sequence, count: 10)
run(repeatAction, withKey: "fruit")
}
spawnFruit() call in didMove() to spawnMultipleFruits() init() function of Player.swiftinit() function of Collectible.swift
1
2
3
4
5
...
// add physics body
self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x:0.0, y: self.size.height/2))
self.physicsBody?.affectedByGravity = false
}
didMove() in GameScience.swift)
1
2
3
4
5
6
7
8
9
10
...
foreground.zPosition = Layer.foreground.rawValue
//add physics body
foreground.physicsBody = SKPhysicsBody(edgeLoopFrom: foreground.frame)
foreground.physicsBody?.affectedByGravity = false
foreground.physicsBody?.categoryBitMask = PhysicsCategory.foreground
foreground.physicsBody?.contactTestBitMask = PhysicsCategory.collectible
foreground.physicsBody?.collisionBitMask = PhysicsCategory.none
addChild(foreground)
...
1
2
3
4
5
6
enum PhysicsCategory {
static let none: UInt32 = 0 // a body that does not belong to any physics category
static let player: UInt32 = 0b1 // 1: player objects
static let collectible: UInt32 = 0b10 // 2: collectible objects
static let foreground: UInt32 = 0b100 // 4: ground platforms or scenery that affects physics
}
PhysicsCategory enumeration is a common pattern used in SpriteKit to define bitmask categories for physics bodies.0b...) makes it visually clear that we are assigning non-overlapping bits. didMove()
1
2
3
4
5
foreground.physicsBody?.categoryBitMask = PhysicsCategory.foreground // set category
foreground.physicsBody?.contactTestBitMask = PhysicsCategory.collectible //test collision with collectible
foreground.physicsBody?.collisionBitMask = PhysicsCategory.none
addChild(foreground)
...
init() of Player.swift
1
2
3
self.physicsBody?.categoryBitMask = PhysicsCategory.player // set category
self.physicsBody?.contactTestBitMask = PhysicsCategory.collectible //test collision with collectible
self.physicsBody?.collisionBitMask = PhysicsCategory.none
init() of Collectible.swift
1
2
3
self.physicsBody?.categoryBitMask = PhysicsCategory.collectible // set category
self.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.foreground //test collision with collectible
self.physicsBody?.collisionBitMask = PhysicsCategory.none
didMove() function in GameScene.swift
1
physicsWorld.contactDelegate = self
1
2
3
4
5
6
7
8
9
10
11
12
13
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == PhysicsCategory.player | PhysicsCategory.collectible {
print("player hit collectible")
}
if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
print("collectible hit foreground")
}
}
}
1
2
3
4
5
6
7
func collected() {
self.run(SKAction.removeFromParent())
}
func missed() {
self.run(SKAction.removeFromParent())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func didBegin(_ contact: SKPhysicsContact) {
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == PhysicsCategory.player | PhysicsCategory.collectible {
print("player hit collectible")
let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node
if let sprite = body as? Collectible {
sprite.collected()
}
}
if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
print("collectible hit foreground")
let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node
if let sprite = body as? Collectible {
sprite.missed()
}
}
}
1
var scoreLevel: SKLabelNode = SKLabelNode()
case ui to enum Layer in SpriteKitHelper.swiftsetupLabels() to GameScene.swift
1
2
3
4
5
6
7
8
9
10
11
func setupLabels() {
scoreLevel.name = "score"
scoreLevel.fontColor = .black
scoreLevel.fontSize = 55.0
scoreLevel.horizontalAlignmentMode = .right
scoreLevel.verticalAlignmentMode = .center
scoreLevel.zPosition = Layer.ui.rawValue
scoreLevel.position = CGPoint(x: frame.maxX - 50, y: 700)
scoreLevel.text = "Score: 0"
addChild(scoreLevel)
}
setupLabels() after spawnMultipleFruits()
1
2
3
4
5
var score : Int = 0 {
didSet {
scoreLevel.text = "Score: \(score)"
}
}
collides with collectible
1
score += 10
gameOver() function to GameScene
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func gameOver() {
removeAction(forKey: "fruit")
// loop through child nodes and stop actions on collectibles
enumerateChildNodes(withName: "//co_*") {
(node, stop) in
node.removeAction(forKey: "drop")
node.physicsBody = nil
}
let restartButton = SKSpriteNode(color: .blue, size: CGSize(width: 120, height:60))
restartButton.name = "restartButton"
restartButton.zPosition = Layer.ui.rawValue
addChild(restartButton)
let buttonText = SKLabelNode(text: "Restart")
buttonText.fontColor = .white
buttonText.fontSize = 30
buttonText.verticalAlignmentMode = .center
restartButton.addChild(buttonText)
}
missedCount and maxAllowedMisses to GameScene class
1
2
var missedCount = 0
var maxAllowedMisses = 5
didBegin() in extension GameScene:SKPhysicsContactDelegate
1
2
3
4
5
6
7
8
9
10
11
12
if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
print("collectible hit foreground")
let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node
if let sprite = body as? Collectible {
sprite.missed()
missedCount += 1
if missedCount >= maxAllowedMisses {
gameOver()
}
}
}
restartGame() to GameScene
1
2
3
4
5
6
7
8
9
10
func restartGame() {
score = 0
missedCount = 0
enumerateChildNodes(withName: "//co_*") { (node, _) in node.removeFromParent()
}
enumerateChildNodes(withName: "restartButton") { (node, _) in
node.removeFromParent()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{
let location = t.location(in: self)
let nodes = nodes(at: location)
if nodes.contains(where: { $0.name == "restartButton"}) {
//restart game
restartGame()
} else {
self.touchDown(atPoint: t.location(in: self))
}
}
}
spawnMultipleFruits to support restart after a game successfully ended.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func spawnMultipleFruits() {
let wait = SKAction.wait(forDuration: TimeInterval(1.0))
let spawn = SKAction.run {
self.spawnFruit()
}
let sequence = SKAction.sequence([wait, spawn])
let repeatAction = SKAction.repeat(sequence, count: 10)
let gameOverAction = SKAction.run {
self.gameOver()
}
let fullSequence = SKAction.sequence([repeatAction, gameOverAction])
run(fullSequence, withKey: "fruit")
}