rawr.

RainCat: Lesson 5

How to make a simple SpriteKit game in Swift 3

Greetings and salutations! This is lesson 5 of our RainCat journey. In the previous lesson we had a long day going though some simple animations, cat behaviors, quick sound effects, and background music. Yesterday was our longest day yet, and today may be longer still.

Today we will focus on:

  • Heads up display (HUD) for scoring
  • Main menu — with buttons!
  • Mute sounds option
  • Quit game option

Even more assets!

The assets for the final lesson are available here on GitHub. Drag the images into the Assets.xcassets again, just as we did in the previous four lessons.

Heads Up!

We need a way to keep score. To do this we can create a Heads up display. This will be pretty simple; it will be a SKNode that contains the score and quit game button. For now we will just focus on the score. The font we will be using is called Pixel Digivolve, you can get the font at dafont.com. As with using images or sounds that are not yours, make sure you know the font’s licensing-status before using it. This one states that it is 100% free, but if you really like the font you can also donate to the author from the page. You can’t always make everything yourself, so it is nice to give back to those who helped you along the way.

Next we need to add the custom font to the project. This can be a tricky process the first time.

Download and move the font into our project folder under a Fonts folder. We’ve done this a few times in the previous lessons, so we’ll go through this process a little quicker. Add a group called Fonts to our project and add the Pixel digivolve.otf file.

Now comes the tricky part. If you miss this part, you probably won’t be able to use your font. We need to add to our Info.plist file. This file will be in the left pane of Xcode. Click it, and we will be viewing the plist (or property list). Right click on the list and click Add Row.

Need to add a row to our plist!

When the new row comes up enter in the following:

Fonts provided by application

Then under Item 0 we need to add in our font name. The plist should look like the following image:

Added our font to the plist successfully!

The font should be ready to use! We should do a quick test to make sure it is working as intended. Move to GameScene.swift and in sceneDidLoad add in the following code at the top of the function:

let label = SKLabelNode(fontNamed: "PixelDigivolve")
label.text = "Hello World!"
label.position = CGPoint(x: size.width / 2, y: size.height / 2)
label.zPosition = 1000
addChild(label)

Does it work?

Testing out our SKLabelNode. Oh no, the Hello World label is back…

If it works, then you did everything correctly. If not, something is wrong. For a more in depth troubleshooting guide check out this post. Note this is for an older version of Swift so there will be small tweaks you will have to make to bring it up to Swift 3.

Now that we can load in custom fonts, we can start on our HUD. We can delete the Hello World! label since this was used to make sure our font loaded. The hud will be a SKNode to act like a container for our HUD elements. We have been using SKNodes the whole time without thinking about it, SKLabelNodes and SKSpriteNodes both inherit from them.

Create the file HudNode.swift using the usual methods and enter the following code:

HudNode.swift

Before we do anything else, open up Constants.swift and add the following line to the bottom of the file — we will be using it to retrieve and persist the highscore:

let ScoreKey = "RAINCAT_HIGHSCORE"

In the code we have five variables that pertain to the scoreboard. The first variable is the actual SKLabelNode that we use to present the label. Next is our variable to hold the current score, then the variable that holds the best score. The last variable is a boolean to tell us if we are currently presenting the highscore (we use this to establish whether we need to run a SKAction to increase the scale of the scoreboard and colorize it to the yellow color of the floor).

The first function called setup(size:) is just to set everything up. We set up the SKLabelNode the same way we did earlier. The SKNode class does not have any size properties by default, so we need to create a way to set a size to position our scoreNode label. We also fetch the current high score from UserDefaults. This is a quick and easy way to save small chunks of data, but it isn’t secure. Since we’re not worried about security for this example, UserDefaults is the perfect solution.

In our addPoint(), we increment the current score variable and check to see if we achieved a highscore. If we have a highscore, we save the new score to UserDefaults and check if we are currently showing the best score. If we are achieving the highscore, we can animate the size and color of the scoreNode.

In the resetPoints() function, we set the current score to zero. We then need to check to see if we were showing the highscore, and reset the size and color to the default values if needed.

Finally we have a small function called updateScoreboard. This is an internal function to set the score to the scoreNode’s text. This is called in both addPoint() and resetPoints().

Hooking up the HUD!

We need to test if our HUD is working correctly. Move over to GameScene.swift and add the following line underneath our food sprite variable at the top of the file:

private let hud = HudNode()

Add the following two lines in our sceneDidLoad() function, near the top:

hud.setup(size: size)
addChild(hud)

Then in spawnCat() function, we need to reset the points in case the cat has fallen off of the screen. Add the following line of code after we add the cat sprite to the scene:

hud.resetPoints()

Next, in our handleCatCollision(contact:) function we need to reset the score again when the cat is hit by rain. In the switch statement at the end of the function — when the other body is a RainDropCategory — add the following line:

hud.resetPoints()

Finally, we need to tell the scoreboard when we gain points. At the end of the file in handleFoodHit(contact:) replace the following lines to where we had the line:

//TODO increment points
print(“fed cat”)

with:

hud.addPoint()

and voila!

HUD unlocked!

You should see the HUD in action. Run around and collect some food. The first time you collect food, you should see the score turn yellow and grow in scale. After you see this happen, let the cat get hit. If the score resets, you know you are on the right track!

Highest score ever! (At the time of this writing)

The next scene…

That’s right! We are moving to another scene. In fact, when completed, this will be the first screen of our app. Create the new scene, place it under the Scenes folder, and call it MenuScene. Enter the following code into the MenuScene.swift file:

MenuScene.swift

Since this scene is relatively simple, we won’t be creating any special classes. Our scene will consist of two buttons. These could (and possibly deserve to be) their own class of SKSpriteNodes, but because they are different enough we will not need to create new classes for them. This is an important tip I want to bring up for when you build your own game: you need to be able to determine where to stop and refactor code when things start to get more complex. When we add more than three or four buttons to the game, it would be time to stop and refactor the menu button’s code into its own class.

The code above isn’t doing anything special; it is setting the positions of four sprites. We are also setting the scene background color so that the whole background is the correct value. A nice tool to help generate color codes from hex strings for Xcode is uicolor.xyz. The code above is also setting the textures for our button states. The start game button has a normal and a pressed state, where the sound button is a toggle. To simplify things for the toggle, we will be changing the alpha value of the sound button on press. We are also pulling and setting the highscore SKLabelNode.

Our MenuScene is looking pretty good, now we need to show the scene when the app loads. Move to GameViewController.swift and replace the following line:

let sceneNode = GameScene(size: view.frame.size)

with:

let sceneNode = MenuScene(size: view.frame.size)

This small change will load the MenuScene by default instead of the GameScene.

Our new scene! Note the 1.0 fps, nothing moving, no need to update anything

Button States!

Buttons can be tricky in SpriteKit. There are plenty of third-party options that are available (I have even made one myself), but in theory you only need to know your three touch methods:

  • touchesBegan(_ touches: with event:)
  • touchesMoved(_ touches: with event:)
  • touchesEnded(_ touches: with event:)

We covered this briefly when updating the umbrella, but now we need to know: which button was touched; whether we release our tap or click that button; if we are still touching it. This is where our selectedButton variable comes into play. When our touches begin, we can capture the button we started clicking with that variable. If we drag outside the button, we can handle this and give the appropriate texture to it. When we release the touch, we can then see if we are still touching inside the button. If we are, then we can handle the associated action to it. Add the following lines of code to the bottom of MenuScene.swift:

MenuScene.swift

This is simple button handling for our two buttons. In touchesBegan(_ touches: with events:) we start off by checking if we have any currently-selected buttons. If we do, we need to reset the state of the button to unpressed. Then we need to check to see if either button is pressed. If we have one that is pressed, it will show the highlighted state for the button. Then we set the selectedButton to the button for use in the other two methods.

In touchesMoved(_ touches: with events:) we check to see which button was originally touched. Then we check to see if the current touch is still within the bounds of the selectedButton and update the highlighted state from there. The startButton’s highlighted state changes the texture to the pressed state texture, where the soundButton’s highlighted state has the alpha value of the sprite set to 50%.

Finally, in touchesEnded(_ touches: with event:) we check again to see which button is selected, if any, and then check to see if the touch is still within the bounds of the button. If all cases are satisfied, we will call handleStartButtonClick() or handleSoundButtonClick() for the correct button.

A time for action!

Now that we have the basic button behavior down, we need an event to trigger if they are clicked. The easier button to implement is the startButton. On click we will only need to present the GameScene. Update the handleStartButtonClick() within MenuScene.swift function to the following code:

func handleStartButtonClick() {
let transition = SKTransition.reveal(with: .down, duration: 0.75)
let gameScene = GameScene(size: size)
gameScene.scaleMode = scaleMode
view?.presentScene(gameScene, transition: transition)
}

If you run the app now and press the button, the game will start!

Now we need to implement the mute toggle. We already have a sound manager, but now we need to be able to tell it if we are muted or not. In Constants.swift we need to add a key to persist if we are muted. Add the following line:

let MuteKey = "RAINCAT_MUTED"

We will use this to save a boolean value to our UserDefaults. Now that we have this set up, we can move into SoundManager.swift. This is where we will be checking and setting UserDefaults to see if we are muted or not. At the top of the file, under the trackPosition variable, add in the following line:

private(set) var isMuted = false

This is the variable that the main menu (and anything else that will play sound) checks to determine if sound is allowed. We initialize it as false, but now we need to check UserDefaults to see what the user wants. Replace the init() function with the following:

SoundManager.swift

Now that we have a default value for isMuted we need the ability to change it. Add the following code to the bottom of SoundManager.swift:

SoundManager.swift

This method will toggle our muted variable as well as update UserDefaults. If the new value is not muted, playback of the music will begin; if the new value is muted, playback will not begin. Otherwise we will stop the current track from playing. After this we need to edit our if statement in startPlaying().

Replace the following line from:

if audioPlayer == nil || audioPlayer?.isPlaying == false {

to:

if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {

Now, if we aren’t muted and either the audio player is not set or the current audio player is no longer playing, we will play the next track.

From here we can move back into our MenuScene.swift to finish up our mute button. Replace handleSoundbuttonClick() with the following code:

MainScene.swift

This toggles the sound in SoundManager, checks the result, and then appropriately sets the texture to show the user whether the sound is muted or not. We are almost done! We only need to set the initial texture of the button on start up. In sceneDidLoad() replace the following line:

soundButton = SKSpriteNode(texture: soundButtonTexture)

with:

soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ? soundButtonTextureOff : soundButtonTexture)

The above example uses a ternary operator to set the correct texture.

Now that the music is hooked up, we can move to CatSprite.swift to disable the cat meowing when we are muted. In the hitByRain() we can add the following if statement after we remove our walking action:

if SoundManager.sharedInstance.isMuted {
return
}

This statement will return if the user muted the app. Because of this we will completely ignore our currentRainHits, maxRainHits, and meow sound effects.

After all of that, now it is time to try out our mute button. Run the app and verify if it is playing and muting sounds appropriately. Mute the sound, close and reopen the app. Make sure that the mute setting persists. Note that if you just mute and re-run the app from Xcode, you may not have given enough time for UserDefaults to save. Play the game, and make sure the cat never meows when you are muted.

Testing out the button functionality

Exiting the game!

Now that we have the first type of button for the main menu, we can get into some tricky business by adding the quit button to our game scene. Some interesting interactions can come up with our style of game; currently the umbrella will move to wherever you touch or move your touch. Obviously, having the umbrella move to the quit button when attempting to leave the game is pretty poor UX, so we will attempt to stop this from happening.

The quit button we are implementing will mimic the start game button that we added earlier, with much of the process staying the same. The change will be in how we handle touches. Get your quit_button and quit_button_pressed asset into the Assets.xcassets file and we can add the following code into our HudNode.swift file:

private var quitButton : SKSpriteNode!
private let quitButtonTexture = SKTexture(imageNamed: "quit_button")
private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")

This will handle our quitButton reference along with the textures that we will set for the button states. To ensure that we don’t inadvertently update the umbrella while trying to quit, we need a variable that tells the HUD (and the game scene) that we are interacting with the quit button and not the umbrella. Add the following code below the showingHighScore boolean variable:

private(set) var quitButtonPressed = false

Again this is a variable that only the HudNode can set, but other classes can check. Now that our variables are set up, we can add in the button to the HUD. Add in the following code into the setup(size:) function:

HudNode.swift > setup(size:)

The above code will set up the quit button with the texture of our non-pressed state. We also set the position to the upper right corner, and the zPosition to a high number to force it to always draw on top. If you run the game now, it will show up in the GameScene, but it will not be clickable yet.

Note the new quit button in our HUD

Now that the button has been placed, we need to be able to interact with it. Right now the only place that we have interaction in GameScene is when interacting with umbrellaSprite. In our example, the HUD will have priority over the umbrella so that users won’t have to move the umbrella out of the way in order to quit. We can create the same functions in HudNode.swift that mimics the touch functionality in GameScene.swift. Add the following code to HudNode.swift:

HudNode.swift

The above code is a lot like the code that we created for the MenuScene. The difference is that there is only one button to keep track of, so we can handle everything within these touch methods. Also, since we will know the location of the touch in GameScene we can just check to see if our button contains the touch point.

Move over to GameScene.swift and replace the touchesBegan(_ touches with event:) and touchesMoved(_ touches: with event:) methods to the following code:

GameScene.swift

In the code above each method handles everything in pretty much the same way. We tell the HUD that we interacted with the scene, then we check to see if the quit button is currently capturing the touches. If it is not, then we move the umbrella. We also added in the touchesEnded(_ touches: with event:) function to handle the end of the click for the quit button, but we are still not using it for the umbrellaSprite.

Clicking the quit button will not move the umbrella, but clicking elsewhere will

Now that we have a button, we need a way to have it affect the GameScene. Add the following line to the top of HudeNode.swift:

var quitButtonAction : (() -> ())?

This is a generic closure that has no input and no output. We will set this with code in the GameScene.swift file and call it when we click the button in HudNode.swift. Then we can replace the TODO in the code we created earlier in touchEndedAtPoint(point:) function with:

if quitButton.contains(point) && quitButtonAction != nil {
quitButtonAction!()
}

Now if we set the quitButtonAction closure, it is called from this point.

To setup the quitButtonAction closure, we need to move over to GameScene.swift. In sceneDidLoad() we can replace our HUD setup with the following code:

GameScene.swift > sceneDidLoad()

Run the app, press play, and then press quit. If you are back on the main menu, then your quit button is working as intended. In the closure that we created, we initialize a transition to the MenuScene. And we set this closure to the HUD node to run when the quit button is clicked.

Now move to GameViewController.swift and remove or comment out the following three lines of code:

view.showsPhysics = true
view.showsFPS = true
view.showsNodeCount = true

With the debugging data out of the way, the game is looking really good! Congratulations, we are currently into beta! Check out the final code for today here on GitHub.

Final thoughts

This was the final lesson of the initial five part tutorial, but there is still a lot of work that can go into this game. The following list is what I plan on doing before I upload it to the store:

  • Add in icons and splash screen
  • Finalize the main menu (was simplified for the tutorial)
  • Bug fixes including rogue raindrops and multiple food spawning
  • Refactoring and optimizing code
  • Color palette of the game changing based on score
  • Difficulty updated based on score
  • Animation for cat when food is right above it
  • Game center integration
  • Credits (including giving proper credit to music tracks)

Keep track of our GitHub as these changes will be made in the future. Also be sure to check out the completed project in the AppStore. If you have any questions about the code feel free to drop us a line at hello@thirteen23.com and we can discuss things. If certain topics get enough attention, maybe we can make another article discussing the topic.

Thank you!

I want to thank all the people that helped in the process of creating the game and the articles that went along with it.

  • Cathryn Rowe — for initial art, design, and editing / publishing the articles in our Garage
  • Morgan Wheaton — for final menu design and color palettes (this will look awesome once I actually implement these features, stay tuned)
  • Nikki Clark — for the awesome headers and dividers in the articles, and help with editing the articles
  • Laura Levisay — For all the awesome gifs in the articles and for sending me cute cat gifs for morale support
  • Tom Hudson — Without him this series would not have been made at all, and for help editing my articles
  • Lani DeGuire — For help editing my articles, lessons 1–4, which was a ton of work.
  • Jeff Moon — For help editing lesson 5, and ping pong. Lots of ping pong.

Seriously it took a ton of people to get everything ready to push this to Medium, and to release it to the store.

Thank you for everyone who read this sentence too.

Find us on Facebook and Twitter or get in touch at thirteen23.com.