Day 94 - 100 Days of Swift

6 minute read

Project 29 (part 1)

Day 94 is the first part of the twenty-ninth project. It is a re-creation of an old game called “Gorillas” where two players launch exploding bananas back and forth at each other in an attempt to blow each other up. Today is pretty much just getting set up. You start the project and get the buildings rendering to make the terrain, and then you layout some basic UI in the view controller and hook things up so that you can communicate between the view controller (UIKit) and the game scene (SpriteKit).

First, you set things up like normal for a game, setting the anchor point and cleaning out most of the stuff that comes with the template. Then you add a new class called BuildingNode which is a subclass of SKSpriteNode which will take care of rendering a building and updating it’s physics. Then you add an enum to keep track of the types of collision we’ll be tracking in the game:

1
2
3
4
5
enum CollisionTypes: UInt32 {
    case banana = 1
    case building = 2
    case player = 4
}

In the BuildingNode you add three methods, setup, configurePhysics, and drawBuilding:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
func setup() {
    name = "building"

    currentImage = drawBuilding(size: size)
    texture = SKTexture(image: currentImage)

    configurePhysics()
}

func configurePhysics() {
    physicsBody = SKPhysicsBody(texture: texture!, size: size)
    physicsBody?.isDynamic = false
    physicsBody?.categoryBitMask = CollisionTypes.building.rawValue
    physicsBody?.contactTestBitMask = CollisionTypes.banana.rawValue
}

func drawBuilding(size: CGSize) -> UIImage {
    let renderer = UIGraphicsImageRenderer(size: size)
    let image = renderer.image { context in
        let rectangle = CGRect(x: 0,
                               y: 0,
                               width: size.width,
                               height: size.height)
        let color: UIColor

        switch Int.random(in: 0...2) {
        case 0:
            color = UIColor(hue: 0.502,
                            saturation: 0.98,
                            brightness: 0.67,
                            alpha: 1)
        case 1:
            color = UIColor(hue: 0.999,
                            saturation: 0.99,
                            brightness: 0.67,
                            alpha: 1)
        default:
            color = UIColor(hue: 0,
                            saturation: 0,
                            brightness: 0.67,
                            alpha: 1)
        }

        color.setFill()
        context.cgContext.addRect(rectangle)
        context.cgContext.drawPath(using: .fill)

        let lightOnColor = UIColor(hue: 0.190,
                                   saturation: 0.67,
                                   brightness: 0.99,
                                   alpha: 1)

        let lightOffColor = UIColor(hue: 0,
                                    saturation: 0,
                                    brightness: 0.34,
                                    alpha: 1)

        let heightRange = stride(from: 10,
                                 to: Int(size.height - 10),
                                 by: 40)

        let widthRange = stride(from: 10,
                                to: Int(size.width - 10),
                                by: 40)

        for row in heightRange {
            for col in widthRange {
                if Bool.random() {
                    lightOnColor.setFill()
                } else {
                    lightOffColor.setFill()
                }

                context.cgContext.fill(CGRect(x: col,
                                              y: row,
                                              width: 15,
                                              height: 20))
            }
        }
    }
    return image
}

Then, you add an array of BuildingNodes to the GameScene and draw a bunch of them in a method called createBuildings:

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
var buildings: [BuildingNode] = []

private func createBuildings() {
    var currentX: CGFloat = -15

    while currentX < 1024 {
        let width = Int.random(in: 2...4) * 40
        let height = Int.random(in: 300...600)
        let size = CGSize(width: width,
                          height: height)
        currentX += size.width + 2

        let building = BuildingNode(color: UIColor.red,
                                    size: size)
        let x = currentX - (size.width / 2)
        let y = size.height / 2
        building.position = CGPoint(x: x, y: y)
        building.setup()
        addChild(building)

        buildings.append(building)
    }
}

// in didMove(to:)
backgroundColor = UIColor(hue: 0.669,
                          saturation: 0.99,
                          brightness: 0.67,
                          alpha: 1)
createBuildings()

That leads to some buildings of various sizes being drawn across the screen on top up a “night sky” background.

Next, to give the user some UI to interact with, you add some sliders, labels and a button to the storyboard and then hook those up to the view controller. He gives you specific values to lay the elements out at, but I won’t repeat them here. Here’s the code:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// In GameViewController
var currentGame: GameScene!
@IBOutlet weak var angleSlider: UISlider!
@IBOutlet weak var angleLabel: UILabel!
@IBOutlet weak var velocitySlider: UISlider!
@IBOutlet weak var velocityLabel: UILabel!
@IBOutlet weak var launchButton: UIButton!
@IBOutlet weak var nameLabel: UILabel!

// In viewDidLoad
currentGame = scene as? GameScene
currentGame.viewController = self
changeAngle(angleSlider!)
changeVelocity(velocitySlider!)

// MARK: - Actions

@IBAction func changeAngle(_ sender: Any) {
    angleLabel.text = "Angle: \(Int(angleSlider.value))°"
}

@IBAction func changeVelocity(_ sender: Any) {
    velocityLabel.text = "Velocity: \(Int(velocitySlider.value))"
}

@IBAction func launchBanana(_ sender: Any) {
    angleSlider.isHidden = true
    angleLabel.isHidden = true

    velocitySlider.isHidden = true
    velocityLabel.isHidden = true

    launchButton.isHidden = true

    currentGame.launch(angle: Int(angleSlider.value),
                       velocity: Int(velocitySlider.value))
}

func activatePlayer(number: Int) {
    if number == 1 {
        nameLabel.text = "<<< PLAYER ONE"
    } else {
        nameLabel.text = "PLAYER TWO >>>"
    }

    angleSlider.isHidden = false
    angleLabel.isHidden = false

    velocitySlider.isHidden = false
    velocityLabel.isHidden = false

    launchButton.isHidden = false
}

And the corresponding code in GameScene:

1
2
3
4
5
weak var viewController: GameViewController!

func launch(angle: Int, velocity: Int) {
    // Empty for now
}

With that, everything is hooked up and ready to build out the logic of the game. It looks like this so far:

Screenshot of working app

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