meow.

RainCat: Lesson 2

How to make a simple SpriteKit game in Swift 3

Marc J Vandehey

--

Welcome back! If you missed out on the first lesson, you can see it here. If you don’t have the code from the previous lesson, it is available on GitHub. Previously we covered project setup, adding a floor, randomly generating raindrops, and a few simple SKPhysicsBodys.

Today we will be focusing on the following:

  • Adding in the umbrella object to keep our cat dry from the rain
  • Beginning collision detection with categoryBitMask and contactTestBitMask
  • Reworking collision detection, so raindrops won’t pile up
  • Creating a world boundary to remove nodes from the scene correctly

Get your assets!

You can get the assets needed for this day here. Just download and unzip the assets. Once this is complete, open Xcode, find your Assets.xcassets folder in the left pane. Then open it and drag your assets underneath the AppIcon placeholder. If everything is done correctly, your Assets.xcassets should look like this:

The umbrella top won’t show up due to being white on white, but I promise it is there

Adding interaction!

We will start by adding a new file to the project. First, right click and add a new group in the left pane and call it Sprites. We want to keep things sorted so that our project won’t later become a mess. Once that group is created, we can make a new file called UmbrellaSprite inside of it. I would recommend saving it in a folder also called Sprites instead of saving it at the top level of your project so that your folder structure will match the group structure in Xcode.

Hurrah for structure!

After we create the new file we will add the following code to create the base UmbrellaSprite.

Initial logic of the UmbrellaSprite

The umbrella will be a pretty basic object. Currently we have a static function to create a new sprite node, but we will soon add a custom physics body to it. For the physics body, we could use the function init(texture: size:) to create a physics body from the texture itself. This would work just fine, but then we would have a physics body that wraps around the handle of the umbrella. If we have a body around the handle, the cat would get hung up on the umbrella, which would not make for a fun game. We will be adding a SKPhysicsBody from a CGPath that we create in the static newInstance() function. Add the following code below in UmbrellaSprite.swift before we return the umbrella sprite newInstance() function.

Adding a custom SKPhysicsBody

We are creating a custom path for the umbrella SKPhysicsBody for two reasons. First, as previously mentioned, we only want the top part of the umbrella to have any collision. The second reason is so that we can be a little forgiving with the umbrella collision size.

The easy way to create a CGPath is to first create an UIBezierPath and append lines and points to create our basic shape. In the code above, we create this UIBezierPath and move the start point to the center of the sprite. The umbrellaSprite’s center point is 0,0 because our anchorPoint of the object is 0.5,0.5. Then we add a line to the far left side of the sprite and extending the line 30 pixels past the left edge.

Side note on usage of the word pixel in this context. Technically this is incorrect because these are points not pixels. A point on a non-retina device may be 1 pixel, 2 pixels on a retina device, and more depending on the pixel density of the device. More info can be found here about pixels and points. I will keep referring to points as pixels to not confuse anyone when talking about CGPoints or the display points for drawing.

Next, go to the top center point of the sprite for the top edge, followed by the far right side, also extended the same 30 pixels out. We extend the edge of the physics body past the texture to give us more room to block raindrops while maintaining the look of the sprite. When we add the polygon to the SKPhysicsBody it will close the path for us and give us a complete triangle. Then set the umbrella’s physics to not be dynamic, so it won’t be affected by gravity.

Finally, let’s add a line to our GameViewController.swift file to visualize the SKPhysicsBody we just added. Place it just above view.showFPS line. This way we can confirm that it looks correct.

view.showsPhysics = true

Now make your way over to GameScene.swift to initialize the umbrella object and add it to the scene. At the top of the file and below our other class variables, add in the line:

private let umbrella = UmbrellaSprite.newInstance()

Then in sceneDidLoad() beneath our floorNode setup, add in the following lines to add the umbrella to the center of the screen:

umbrella.position = CGPoint(x: frame.midX, y: frame.midY)addChild(umbrella)

Once this is added, run the app to see the umbrella. You should also blue squares falling and sliding off of it!

Now we can visualize our SKPhysicsBodys while debugging our app.

Creating movement!

We will update the umbrella to respond to touches. In GameScene.swift, look at the empty functions touchesBegan(_ touches:, with event:) and touchesMoved(_ touches:, with event:). This is where we will tell the umbrella where we interacted with the game. If we set the position of the umbrella node in both of these functions based on one of the current touches, it will snap into place and teleport from one side of the screen to the other.

Another approach could be to set a destination in the UmbrellaSprite object, and when update(dt:) is called, we can move toward that location.

Yet a third approach could be to set SKActions to move the UmbrellaSprite on touchesBegan(_ touches:, with event:) or touchesMoved(_ touches:, with event:), but I would not recommend this. This would cause us to create and destroy these SKActions frequently, and likely would not be very performant.

We will choose option #2. Update the code in UmbrellaSprite to look like the below code:

UmbrellaSprite.swift

There are a few things happening in the above code. The newInstance() function has been left untouched, but we added two variables above it. We added a destination variable (the point that we want to be moving towards), setDestination(destination:) function where we will ease the umbrella sprite to, and we added an updatePosition(point:) function. The updatePosition(point:) will act exactly like updating the position property directly, but we also need to set the destination variable so that the umbrellaSprite will start at this point, instead of move towards it after setup. We will call updatePosition(point:) in GameScene instead of setting the position or destination directly.

The setDestination(destination:) function will only update the destination property, we will do our calculations off of this property later. Finally, we added update(dt:) to compute how far we need to travel towards the destination point from our current position. We compute the distance between the two points, and if it is greater than one pixel, we compute how far we want to travel using the easing function. If it is less than one pixel, we will just jump to the final position. We do this because the easing function will approach the destination very slowly. Instead of constantly updating, computing, and moving the umbrella an extremely short distance, we just set the position and forget about it.

Moving back to GameScene.swift, we should update our touchesBegan(_ touches: with event:) and touchesMoved(_ touches: with event:) functions to the following code:

GameScene.swift

Now our umbrella will respond to our touches. In each function, we check to see if we have a valid touch. If we do, we will tell the umbrella to update its destination to the touch location. Now we need to modify the line in sceneDidLoad():

umbrella.position = CGPoint(x: frame.midX, y: frame.midY)

to:

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

This way our initial position and destination are set correctly. When we start the scene, we won’t see the umbrella move without us interacting with the app. Lastly, we need to tell the umbrella to update in our own update(currentTime:) function.

Add the following code near the end of our update(currentTime:) function:

umbrella.update(deltaTime: dt)

When we run the code, we should be able to tap and drag around the screen, and the umbrella will follow our touches and drags.

I like to move it, move it

Detect our collision!

Right now there is not a lot of collision that we care about. We can have collision between raindrops, raindrops and the umbrella, and raindrops and the floor. We do need to detect when the raindrops hit something, though, so we can tell it to be removed. We will need to create some variables to classify each SKPhysicsBody so that we can figure out what to do when they collide.

To accomplish this we should create another file called Constants.swift. Right click under the Support group, click New File > Swift File, then click the Next button. Rename the file to Constants and then click Create.

The file should contain the following code:

let RainDropCategory : UInt32 = 0x1 << 1
let FloorCategory : UInt32 = 0x1 << 2
let UmbrellaCategory : UInt32 = 0x1 << 3

We are assigning a value for each SKPhysicsBody as well as using this to determine what each body will listen for when collided.

Head over to UmbrellaSprite.swift, right before we return the object in newInstance() we need to add the following code:

umbrella.physicsBody?.categoryBitMask = UmbrellaCategory
umbrella.physicsBody?.contactTestBitMask = RainDropCategory

This is telling the umbrella sprite that its category is the UmbrellaCategory, and that it only cares if it comes into contact with the RainDropCategory. We will update the raindrops’ categoryBitMask to RainDropCategory as well.

In GameScene, under the spawnRaindrop() function, we need to add the following code before we add it to the scene:

rainDrop.physicsBody?.categoryBitMask = RainDropCategory

Next, in the same file, we need to add the following code into the sceneDidLoad function before we add the floorNode to the scene:

floorNode.physicsBody?.categoryBitMask = FloorCategory
floorNode.physicsBody?.contactTestBitMask = RainDropCategory

To listen to the contact between these nodes, we need to inherit from SKPhysicsContactDelegate, which will provide us with functions that will be called when two SKPhysicsBodys begin collision and end collision. Update the class declaration to:

class GameScene: SKScene, SKPhysicsContactDelegate {

We now need to tell our scene’s physicsWorld that we want to listen to collisions. Add in the following line in sceneDidLoad() near the top of the function.

self.physicsWorld.contactDelegate = self

Then we need to implement the SKPhysicsContactDelegate function. didBegin(_ contact:). This will be called every time there is a collision that matches any of the contactTestBitMasks that we set up earlier. Add this code to the bottom of GameScene.swift.

GameScene.swift

Now when a raindrop collides with an edge of any object, we remove the collision bit mask of the raindrop. This allows the raindrop to not collide with anything after the initial impact, which means our Tetris nightmare is finally over!

The raindrops now admittedly look a little lackluster when colliding with objects. When they hit the floor, they bounce a little bit, and only glance off of the umbrella. We can fix this by playing with some of the properties in the floor and umbrella sprites.

In UmbrellaSprite.swift in newInstance(), below the categoryBitMask, we can add the line:

umbrella.physicsBody?.restitution = 0.9

Next, in GameScene under in the sceneDidLoad() function, we can update the restitution for the floor as well. Add the following line after we initialize the floorNode’s physics body:

floorNode.physicsBody?.restitution = 0.3

Now run the app and happy bouncing raindrops will appear!

More bouncing

The raindrops are no longer piling up, but your fps is still dropping, and your node count is going through the roof. The nodes are no longer on screen, and no longer rendered, but they are still being held in memory. Time to cull the unneeded nodes!

Culling off-screen nodes!

First we will add another category to our constants file so we know that the edge of the world was hit instead of something less important. Constants.swift should now look like this:

let RainDropCategory   : UInt32 = 0x1 << 1
let FloorCategory : UInt32 = 0x1 << 2
let UmbrellaCategory : UInt32 = 0x1 << 3
let WorldFrameCategory : UInt32 = 0x1 << 4

Going back to GameScene, and into the sceneDidLoad function, we can update a physics body for the scene. We only care about the nodes on screen, so we will create a CGRect that is slightly larger than the screen bounds. When this CGRect is hit, we then remove the node from the parent and clear the SKPhysicsBody. We need to clear the physics body, since the scene will be holding onto it to update during the render cycle.

Replace the code where we set ourselves up as the contactDelegate in sceneDidLoad() from:

self.physicsWorld.contactDelegate = self

to:

GameScene.swift > sceneDidLoad()

The world frame will have a buffer of 100 points on each side. Note that we used edgeLoopFrom, so it creates an empty rectangle that allows for collision at the edges of the frame.

In the didBegin(_ contact:) function, we need to add the delete behavior to cull the nodes. This function should now look like this:

Finally, we need to set up the contactTestBitMask for the raindrop so that we can detect when it hits the edge of the world. In the spawnRaindrop() function, we need to add the following line after the categoryBitMask we added earlier:

rainDrop.physicsBody?.contactTestBitMask = WorldFrameCategory

Now when we run the app, we should be able to keep the node count at a consistent rate instead of growing forever.

No more pile ups!

If there is a problem, and the raindrops are not acting like the above gif, double check to make sure every categoryBitMasks and contactTestBitMasks are set up correctly.

So that’s lesson 2! In lesson 3, RainCat will start looking more like a game, (I promise!). We will add in the basic cat sprite along with detecting collisions with the cat. We will also spawn food for the cat to eat, and we’ll fix any issues that can come up with basic gameplay (i.e. if the cat falls off the world to respawn it, or so the cat will face the correct direction while moving).

The source code for today will be available online 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 3 coming up next!

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

--

--