Day 85 - 100 Days of Swift
Project 26 (part 1)
Day 85 is the first part of the twenty-sixth project. It will be a marble maze game where you tilt the iPad in different directions to get the marble to roll through the maze. Today you pretty much just get the project set up and add the code for loading a level from a text file that visually represents the layout of the level.
First, you make a new game project, do all the normal clean up stuff, and lock the orientation into just landscape right. You do that because the game will involve tilting the iPad around and it would be frustrating to have the screen spinning around on you if you accidentally tilted too far. You also pull in the artwork that he provides.
Then you add a function called loadLevel
that will load up a level from a text file:
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
guard let levelURL = Bundle.main.url(forResource: "level1",
withExtension: "txt") else {
fatalError("Could not find level1.txt in the app bundle.")
}
guard let levelString = try? String(contentsOf: levelURL) else {
fatalError("Could not load level1.txt from the app bundle.")
}
let lines = levelString.components(separatedBy: "\n")
for (row, line) in lines.reversed().enumerated() {
for (column, letter) in line.enumerated() {
let x = (64 * column) + 32
let y = (64 * row) + 32
let position = CGPoint(x: x, y: y)
if letter == "x" {
loadWall(at: position)
} else if letter == "v" {
loadVortex(at: position)
} else if letter == "s" {
loadStar(at: position)
} else if letter == "f" {
loadFinish(at: position)
} else if letter == " " {
// this is an empty space – do nothing!
} else {
fatalError("Unknown level letter: \(letter)")
}
}
}
The first part gets the text out of the file and splits it into lines that we can work with. Then it loops through the lines in reversed order because SpriteKit’s coordinate system goes from the bottom up so we need to start with the bottom lines. It also enumerates the lines so that we have something to calculate the position off of. Inside of that, there is a second loop that loops through each letter in the line, also enumerated. And inside the second loop we now have enough information to calculate the position, and we use the value of the letter to decide what sort of node to make at that position.
loadWall(at:)
looks like this:
1
2
3
4
5
6
7
8
9
func loadWall(at position: CGPoint) {
let node = SKSpriteNode(imageNamed: "block")
node.position = position
node.physicsBody = SKPhysicsBody(rectangleOf: node.size)
node.physicsBody?.categoryBitMask = CollisionTypes.wall.rawValue
node.physicsBody?.isDynamic = false
addChild(node)
}
You get the right node, set its position, give it a physics body of its own size, give it a category for collisions, set it to not be dynamic (meaning it won’t move) and then add it as a child.
The rest of the “load” functions follow pretty much the same pattern with a few extras. Here’s loadVortex(at:)
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func loadVortex(at position: CGPoint) {
let node = SKSpriteNode(imageNamed: "vortex")
node.name = "vortex"
node.position = position
node.run(SKAction.repeatForever(SKAction.rotate(byAngle: .pi,
duration: 1)))
node.physicsBody = SKPhysicsBody(circleOfRadius: node.size.width / 2)
node.physicsBody?.isDynamic = false
node.physicsBody?.categoryBitMask = CollisionTypes.vortex.rawValue
node.physicsBody?.contactTestBitMask = CollisionTypes.player.rawValue
node.physicsBody?.collisionBitMask = 0
addChild(node)
}
In addition to the stuff that loadWall
does, this just gives it a name, adds an action to rotate the node and sets the contactTestBitMask
to notify us when it collides with a player.
The loadStar(at:)
and loadFinish(at:)
methods do largely the same things:
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 loadStar(at position: CGPoint) {
let node = SKSpriteNode(imageNamed: "star")
node.name = "star"
node.physicsBody = SKPhysicsBody(circleOfRadius: node.size.width / 2)
node.physicsBody?.isDynamic = false
node.physicsBody?.categoryBitMask = CollisionTypes.star.rawValue
node.physicsBody?.contactTestBitMask = CollisionTypes.player.rawValue
node.physicsBody?.collisionBitMask = 0
node.position = position
addChild(node)
}
func loadFinish(at position: CGPoint) {
let node = SKSpriteNode(imageNamed: "finish")
node.name = "finish"
node.physicsBody = SKPhysicsBody(circleOfRadius: node.size.width / 2)
node.physicsBody?.isDynamic = false
node.physicsBody?.categoryBitMask = CollisionTypes.finish.rawValue
node.physicsBody?.contactTestBitMask = CollisionTypes.player.rawValue
node.physicsBody?.collisionBitMask = 0
node.position = position
addChild(node)
}
Finally, you just add a background node to fill out the scene:
1
2
3
4
5
6
// In didMove(to:)
let background = SKSpriteNode(imageNamed: "background.jpg")
background.position = CGPoint(x: 512, y: 384)
background.blendMode = .replace
background.zPosition = -1
addChild(background)
With that, we have code that will lay out a level based on a text file and it looks like this:

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