Day 86 - 100 Days of Swift

5 minute read

Project 26 (part 2)

Day 86 is the second part of the twenty-sixth project. Today you get the marble maze game into a playable state by adding a marble, detecting the tilt of the accelerometer, and detecting the player’s contact with the various objects in the game.

First, you add a player node to the game:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var player: SKSpriteNode!

func createPlayer() {
    player = SKSpriteNode(imageNamed: "player")
    player.position = CGPoint(x: 96, y: 672)
    player.zPosition = 1
    player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
    player.physicsBody?.allowsRotation = false
    player.physicsBody?.linearDamping = 0.5

    player.physicsBody?.categoryBitMask = CollisionTypes.player.rawValue
    player.physicsBody?.contactTestBitMask = CollisionTypes.playerContactTests
    player.physicsBody?.collisionBitMask = CollisionTypes.wall.rawValue
    addChild(player)
}

I added a static property on CollisionTypes to abstract away the bit mask math a little bit:

1
2
3
4
5
6
7
8
static let playerContactTests: UInt32 = {
    var val: UInt32 = 0
    val |= CollisionTypes.star.rawValue
    val |= CollisionTypes.vortex.rawValue
    val |= CollisionTypes.finish.rawValue

    return val
}()

This basically just says that we want to be notified when the player comes into contact with stars, vortexes, or the finish node.

Then you set the starting gravity to be none, so that the ball doesn’t start rolling until the player tilts the iPad. He walks you through a little hack for adding touch tracking in the simulator because it doesn’t have an accelerometer, but I skipped that because I was testing on a device so I didn’t really care about it. The big takeaway for me there was that you can use compiler directives like #if targetEnvironment(simulator) and #endif to conditionally compile code depending on where it is going. I skipped that though and just went straight to using CoreMotion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import CoreMotion

var motionManager: CMMotionManager!

// In didMove(to:)
physicsWorld.gravity = .zero

motionManager = CMMotionManager()
motionManager.startAccelerometerUpdates()

// In update(_:)
if let accelerometerData = motionManager.accelerometerData {
    let dx = accelerometerData.acceleration.y * -50
    let dy = accelerometerData.acceleration.x * 50
    physicsWorld.gravity = CGVector(dx: dx, dy: dy)
}

This lets you move the ball around the screen by tilting the iPad.

Next, you add a score label so the player can see how many points they’ve gotten:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var scoreLabel: SKLabelNode! = {
    let label = SKLabelNode(fontNamed: "Chalkduster")
    label.horizontalAlignmentMode = .left
    label.position = CGPoint(x: 16, y: 16)
    label.zPosition = 2
    return label
}()

var score = 0 {
    didSet {
        scoreLabel.text = "Score: \(score)"
    }
}

// In didMove(to:)
addChild(scoreLabel)
score = 0

Then you set the GameScene to be the contactDelegate of the physicsWorld:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In didMove(to:)
physicsWorld.contactDelegate = self

extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        guard let nodeA = contact.bodyA.node else { return }
        guard let nodeB = contact.bodyB.node else { return }

        if nodeA == player {
            playerCollided(with: nodeB)
        } else if nodeB == player {
            playerCollided(with: nodeA)
        }
    }
}

The playerCollided(with:) function looks like this:

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
switch node.name {
case "vortex":
    player.physicsBody?.isDynamic = false
    isGameOver = true
    score -= 1

    let move = SKAction.move(to: node.position,
                             duration: 0.25)
    let scale = SKAction.scale(to: 0.0001,
                               duration: 0.25)
    let remove = SKAction.removeFromParent()
    let actions = [move, scale, remove]
    let sequence = SKAction.sequence(actions)

    player.run(sequence) { [weak self] in
        self?.createPlayer()
        self?.isGameOver = false
    }
case "star":
    node.removeFromParent()
    score += 1
case "finish":
    print("Should load next level here.")
default:
    fatalError("What did we collide with?\n\(node)")
}

It just checks what kind of node the player collided with. If it is a vortex, it stops the player from moving, subtracts one from the score, kicks of a sequence that will move the marble into the vortex and shrink it down so it looks like it is getting sucked in, and the recreates the player at the starting point once it is done. If it is a star, if just removes the stars and gives the player a point. If it is the finish line, it should end the level and set up the next one, but you are left to write the code for that tomorrow.

The isGameOver property works like this:

1
2
3
4
var isGameOver = false

// At the beginning of didMove(to:)
guard !isGameOver else { return }

At the point the game works, except that there is no end condition. There is no way to lose the game and there is no way to really win it or move on from this level. I’m guessing those will be added tomorrow. Right now it looks like this:

You can find my version of this project at the end of day 86 on Github here.