purr.

RainCat: Lesson 3

How to make a simple SpriteKit game in Swift 3

Marc J Vandehey

--

Welcome back! If you missed out on the previous lesson, you can see it here. If you don’t have the code from the previous lesson, it is available on GitHub.

In the last lesson, we added our umbrella sprite that has a custom SKPhysicsBody generated with a CGPath. We also added touch detection and the ability to move the umbrella. We hooked up collision detection leveraging categoryBitMask and contactTestBitMask. We removed collision on raindrops when they hit anything, so they don’t pile up but rather fall through the floor after one bounce. Finally, we set up a world frame to remove any SKSpriteNode that comes into contact with it.

Today we will be focusing on the following:

  • Add in textures to make it look closer to the final game
  • Spawn the cat
  • Implement cat collision
  • Spawn food
  • Implement food collision

Get your assets again!

You can get the assets needed for this lesson here. Download and add them again to your Assets.xcassets file by dragging them in all at once. You should now have an asset for the background, cat, food dish, and raindrop.

Lots of assets!

Adding the background node!

Your background will be a pretty simple node — we just need to add a SKSpriteNode the size of the screen for our background. There are better ways to do this especially when looking into SKTiledMaps, or for simple shapes like the background to just draw SKShapeNodes. For now we will just add our background in sceneDidLoad() of our GameScene.swift file:

Some people may not like that I keep using imageNamed for setting up the SKSpriteNode. While I agree it is not idea, if we keep using this over an atlas or saving and reusing the texture, imageNamed is a great way to get started without worrying about pre-optimizing your code.

We can run the app and…

Oh…

Z Positioning!

We really did not need to tamper with zPositions before. Sure, we could just place the background in a different order, but there’s no guarantee that it will render behind everything. We need to run around and add a zPosition to each of our nodes to know exactly how things will render on screen. In sceneDidLoad(), we need to add the following code where we initialized our background node:

background.zPosition = 0

In the same sceneDidLoad() function, we can remove the red fill color on our floorNode, because we literally only need it as a line boundary. Next we need to make sure the umbrella and the rain render in front of the background.

Moving to the spawnRaindrop() function, we can add:

rainDrop.zPosition = 2

Finally, we can move to UmbrellaSprite.swift to add in the following line, right after we initialize the object in newInstance():

umbrella.zPosition = 1

This way when rain hits the umbrella, the drop will still fall in front of the umbrella. Run the app, and we should be much happier with the outcome.

That’s better

Better raindrops!

Next let’s update the raindrops so they aren’t blue. Navigate to GameScene.swift, and at the top of the file, add this variable underneath the umbrella sprite and above the sceneDidLoad() function:

private let rainDropTexture = SKTexture(imageNamed: "rain_drop")

Note that this is a reference to a texture. Instead of creating a new texture every time we create a raindrop we will just reuse this one. If we wanted to further optimize the code, we could add in an image atlas, but we can handle this when we optimize the app.

Moving down to spawnRaindrop(), update the code to look like:

Now we are playing with sprite power!

We updated the sprite from a SKShapeNode to a SKSpriteNode to hold the texture we created earlier. We also removed the fill color since it is only a member of SKShapeNode. Note that the SKPhysicsBody is still a rectangle. That’s because I personally like how it reacts to the umbrella when it gets hit. If we used the following code, we can have a SKPhysicsBody with the outline of the texture, and it works except this particular sprite will rotate wildly when it hits the angled surface:

rainDrop.physicsBody = SKPhysicsBody(texture: rainDropTexture, size: rainDrop.size)

Change it if you wish, but I will be keeping the 20x20 rectangle.

The raindrops are looking a lot better now

Cat Time!

Now it’s time to add the initial version of the cat. We need to create a new file named CatSprite. To do this right click in the left pane under the Sprites group, New File > Swift File > Create, and navigate to your Sprites folder. Name the file CatSprite and click Create.

Next we can update the code in CatSprite to be:

CatSprite.swift : Note that we set our zPosition right away this time!

We stubbed out the file with a static initializer that returns a generated cat sprite. We also stubbed out another update function. If we need to update more sprites, we should look into making this function part of a protocol for our sprites to conform to. One thing to note here: for the cat sprite, we are using a circular SKPhysicsBody. Again, we could use the texture to create the physics body, but this is an “artistic” decision. When the cat is hit by the raindrops or the umbrella, it is much more comical for the cat to roll around in what I assume is a pit of despair, instead of just sitting still.

We will need to have callbacks for when our cat comes in contact with the rain or falls off the world. Moving to the Constants.swift file, we can add the following line to it to act as a Cat Category.

let CatCategory : UInt32 = 0x1 << 5

Now let’s move back to CatSprite.swift, and update the sprite to include a categoryBitMask and contactTestBitMask. Add the following lines before we return the catSprite in newInstance():

catSprite.physicsBody?.categoryBitMask = CatCategory
catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldFrameCategory

Now we will get a callback when the cat is hit by rain or when it comes into contact with the edge of the world. After we add this, we need to add the cat to the scene.

At the top of GameScene.swift, beneath where we initialized our umbrellaSprite, add in the following line:

private var cat : CatSprite!

We can create a cat right away in sceneDidLoad(), but we want to spawn the cat from a separate function in order to reuse the code. The exclamation point (!) is like a promise to the compiler that catSprite will never be nil. If we touch our cat object before we create it, we break our promise and our app will crash. We need initialize it before using it, but in the proper function.

Next, we will create a spawnCat() function in GameScene.swift so that we can initialize our cat sprite. Add the following code near the bottom of the file, just under our spawnRaindrop() function:

GameScene.swift

Walking through this function, we first check to see if the cat is not nil, then we check to see if the scene contains a cat. If the scene does contain a cat, we remove it from the parent, remove any actions that the cat was currently performing, and clear out the SKPhysicsBody of the cat. This will only trigger if the cat came into contact with the edge of the game world. After this, we initialize a new cat and set the position to 30 pixels beneath the umbrella. We could spawn the cat anywhere, but I thought that this would be a clever place instead of dropping a cat from the sky. Finally, in sceneDidLoad(), after we position and add the umbrella, call the spawnCat() function:

umbrella.updatePosition(point: CGPoint(x: frame.midX, y: frame.midY))
addChild(umbrella)
spawnCat()

Now we can run the app, and suddenly…

Cat.

If you let the cat come into contact with raindrops or the umbrella, it will roll around. At this point the cat can roll off the screen and will be deleted as soon as it hits the world edge, so we will need to respawn the cat. Since we have a callback in place for when the cat comes into contact with raindrops and the game world currently, we can handle the collision in the didBegin(_ contact:) function.

We want to differentiate when a raindrop hits the cat and when cat hits the world frame, so let’s break out the logic into a new function. At the bottom of GameScene.swift below didBegin(_ contact:) enter the code below:

I think it may have been a missed opportunity to hot have called it “handleCatlision”

In this block of code, we are looking for the physics body that is not the cat. Once we have the other body, we need to figure out what hit the cat. For now, if a raindrop hits the cat, we just print out to console that the collision occurred. If the cat hits the edge of the game world, we will respawn the cat.

We need to call this function if contact occurs with the cat object, so let’s update didBegin(_ contact:) to the following code:

GameScene.swift

We snuck in a condition between where we remove collision from raindrops and where we cull offscreen nodes. This if statement checks to see if either body is the cat, and then we handle the cat behaviors in the handleCatCollision(contact:) function.

We can now test our cat respawning function by pushing the cat offscreen with the umbrella. The cat will now respawn underneath the umbrella! Note that if the umbrella bottom is beneath the floor, the cat will fall off screen for as long as the umbrella stays beneath the floor. This isn’t a huge issue for now, but we should think of a way around this later on.

Spawning Food!

Now seems like a good time to spawn some food for our cat to eat. Sure, the cat can’t move by itself, but we can fix that later. Before we create the food sprite, we can add a new category for food in our Constants.swift file. Add the following line below the CatCategory.

let FoodCategory : UInt32 = 0x1 << 6

Create a file exactly how we created the CatSprite file, but this time called FoodSprite under the Sprites group. After the file is created, add the following code:

FoodSprite.swift

We set the physics body to a rectangle of the sprite’s size. This is fine, since the sprite itself is a rectangle. Then we set the category to the FoodCategory that we just created, and add it to the object that we care about colliding with, (the world frame, raindrops and the cat). The zPosition of the food and the cat are the same, and they’ll never overlap, because as soon as they come into contact, the food is deleted and the player earns a point.

Heading back to GameScene.swift, we will need to add functionality to spawn and remove the food. At the top of the file, beneath the random number generator we can add this line:

private let foodEdgeMargin : CGFloat = 75.0

This will act as a margin when spawning food. We don’t want to spawn it too close to either the left or right side of the screen. We place it at the top of the file, so we won’t have to search all over to change this value if we need to later on. Next, beneath our spawnCat() function, we can add our spawnFood function:

GameScene.swift

This function acts almost exactly like our spawnRaindrop() function. We create a new FoodSprite, then place it in a random x location on the screen. We use the margin variable we set earlier to shrink the amount of screen space we can spawn the food sprite. First we get a random location from 0 to the width of the screen minus the margin multiplied by two. Then we offset the start by the margin. This stops us from spawning food anywhere between 0 and 75 from each side of the screen.

Near the top of the file in sceneDidLoad(), let’s add the following line beneath our initial call of spawnCat():

spawnCat()
spawnFood()

Now once the scene loads we will spawn an umbrella, a cat from the umbrella, some raindrops and food falling from the sky. Right now, the rain can interact with the food, it may move it around as well. The rain will act the same way to the food as the umbrella and the floor, where it bounces once then loses all of its collision until it interacts with the edge of the game world and is deleted. We need some interaction between the food and the cat.

At the bottom of the file in GameScene.swift, we will add our code handling the food collision. Add the following code below handleCatCollision():

GameScene.swift

We handle the food pretty much the same way we handle cat collisions. First we identify which physics body is the food, and then run a switch statement on the other body. From there we add cases for CatCategory — this is a stub for later to update our score. Then we fallthrough to hit the WorldFrameCategory where we remove the food sprite from the scene, and also the physicsBody associated with it. Finally we spawn the food again. When it hits the world boundary, we just remove the sprite and physicsBody. If anything else is detected, our default case will trigger. and send a generic statement to the console. Currently the only thing that can hit this statement is the RainDropCategory. As of now, we don’t care what happens when the rain hits the food. We just want the rain to act the same as it does when it hits the floor or the umbrella.

To get everything wired up, we will add in a few lines to the didBegin(_ contact). Add the following code above the check for CatCategory:

if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory {
handleFoodHit(contact: contact)
return
}

The final code for didBegin(_ contact) should look like the following code snippet:

GameScene.swift

Let’s run the app again. The cat won’t be running anywhere right now, but we can test our method by pushing the cat food off screen or by rolling the cat onto the cat food. Either case should now delete the food node and one should respawn from offscreen.

Sometimes the cat needs a little encouragement to get it to eat

so that’s it for this lesson — I hope you made it through in one piece! The next lesson will involve animating the cat, moving the cat towards the food, and giving the cat a stunned state for when it is hit by the rain. We will also look into sounds when the cat is hit by the rain.

Thank you for reading through lesson 3. The source code for today will be available on GitHub.

How did you do? Did your code look almost exactly like mine? What changed? Did you update the code for the better, or was I not being clear explaining what to do? Let me know in the comments below.

Lesson 4 coming up next!

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

--

--