What Makes a Game a Game?

Jorge Jordán

December 2015

In this article by Jorge Jordán, the author of the book Getting Started with SpriteKit, you will learn about the key elements that take part in every game, such as moving sprites on the scene, detecting touches, and handling collisions. You will also learn how to add labels to the scene and how to play music and sound effects.

You will learn about the following topics along with this article:

  • Detecting touch interaction
  • Executing actions on sprites
  • Handling collisions
  • Creating and updating labels
  • Playing music and sound effects

(For more resources related to this topic, see here.)

Handling touch events

Our loved main character is supposed to run though several doors, but some of them are closed and others are open, so it will need to move laterally in order to choose the right ones. We will need to handle the player's interaction to help our rabbit select properly.

By default, SpriteKit listens to touch events, and we can manage them by implementing some of the methods provided by the UIResponder class, which is the parent class of the SKNode class.

There are four methods available to detect and handle touches, and they are as follows:

  • touchesBegan: This method is triggered as soon as the user touches the screen, and it can detect one or more touches; that's why it receives a set of UITouch instances. We can use this method to select the place where we want our rabbit to be moved in our game.
  • touchesMoved: This method will be triggered when one or more of the fingers touching the screen begin moving. We can take advantage of this method to update a node's position while it is being dragged.
  • touchesEnded: This method will be triggered as soon as one or more of the fingers touching the screen release the touch. This method can be useful to recognize when the user wants to stop dragging a node.
  • touchesCancelled: We can implement this method in order to take actions when a touch finishes due to a system event, such as a phone call or a memory warning.

Among the previous methods, we will take advantage of touchesBegan, which has been inherited on SKScene from the UIResponder class. For this purpose, open GameScene.swift and implement touchesBegan with the following lines:

if let touch = touches.first {
           // Moving the rabbit to the touched position
          let location = touch.locationInNode(self)
           self.moveRabbitToNextLocation(location)
         }

As the device detects all the touches happening on screen, the touches object will contain a set of UITouch instances from where we can just get the first of them by executing the first method.

Once we have a single touch event, we just need to know the location where it is placed. This task is very easy to perform, as we just need to call the locationInNode method passing the node where the touch has taken place (the scene), and we will get the CGPoint object value corresponding to it.

Once we have the location of the touch, we just need to pass it to the moveRabbitToNextLocation method, where all the magic is going to happen. To know what this method does, add the following code after touchesBegan:

func moveRabbitToNextLocation(touchLocation: CGPoint) {
       // The constant rabbit's speed
       let rabbitSpeed: CGFloat = 360.0

       var moveAction:SKAction!
       var duration: CGFloat = 0.0
       var nextPosition: CGPoint

       if touchLocation.x <= view!.bounds.size.width/3 {          
           // Setting the next position
           nextPosition = CGPoint(x: view!.bounds.size.width/6 + 25 * rabbit.frame.width/40, y: rabbit.position.y)
           // We want the rabbit to move on a constant speed
           duration = self.distanceBetween(point: rabbit.position, andPoint: nextPosition) / rabbitSpeed
           // Move the rabbit to the touched position
           moveAction = SKAction.moveToX(nextPosition.x, duration: Double(duration))          
       } else if touchLocation.x > view!.bounds.size.width/3 && touchLocation.x <= 2 * view!.bounds.size.width/3 {          
            // Setting the next position
           nextPosition = CGPoint(x: view!.bounds.size.width/2, y: rabbit.position.y)
           // We want the rabbit to move on a constant speed
           duration = self.distanceBetween(point: rabbit.position, andPoint: nextPosition) / rabbitSpeed
           // Move the rabbit to the touched position
           moveAction = SKAction.moveToX(nextPosition.x, duration: Double(duration))          
       } else {          
           // Setting the next position
           nextPosition = CGPoint(x: 5*view!.bounds.size.width/6 - 25 * rabbit.frame.width/40, y: rabbit.position.y)
           // We want the rabbit to move on a constant speed
           duration = self.distanceBetween(point: rabbit.position, andPoint: nextPosition) / rabbitSpeed
           // Move the rabbit to the touched position
           moveAction = SKAction.moveToX(nextPosition.x, duration: Double(duration))          
       }      
       // Executing the action
       rabbit.runAction(moveAction)
   }

Okay, I know that it's a big piece of code, but don't worry; it's very easy to understand. We want to move our little rabbit laterally in a constant speed (360.0), and that's why we are declaring these rabbitSpeed, moveAction, duration, and nextPosition variables.

The rabbit will need to choose between three doors, placed centered on the first, the second, and the third of the screen's width. This is why after declaring the variables, we check whether the touch has taken place on the first, the second, or the third of the screen's width.

Let's pay attention, for example, to the first condition:

if touchLocation.x <= view!.bounds.size.width/3 {

With this line, we are checking whether the x coordinate of the touch location is lower than the view's width divided by 3 (the third of the screen on the left-hand side). If this condition matches, we initialize the nextPosition variable with the desired place, which corresponds to the middle of the position of each of the three doors that the rabbit will need to avoid.

So, once we know the next position, we just need to focus on the movement itself. SpriteKit provides a large number of methods to perform all the actions we will need in a game. At this moment, we will just focus on movement actions, specifically on moveToX, which will move a sprite to a desired position (taking only the x coordinate into account) in a specified duration and can be concurrently called, resulting in a movement that will be the sum of different movements. There are similar actions, such as moveToY, moveTo (a specific point), and moveBy, that generate a movement by the next position using relative coordinates, but we want to move to an absolute position to be sure that we pass through the doors properly.

As we want our rabbit to move at the same speed (360.0) always, we will need to update the duration of the action using basic physics. Do you remember the formula to calculate the speed? Here it is:

We already know the speed, but we need to calculate the distance between the main character and its next position value from nextPosition variable and then we will be able to get the time (duration) for which this movement will last.

For the purpose of calculating the distance between the rabbit and its desired position, I've implemented a very useful method, distanceBetween, which will retrieve the value we are looking for. Add the following method to GameScene:

func distanceBetween(point p1:CGPoint, andPoint p2:CGPoint) -> CGFloat {
       return sqrt(pow((p2.x - p1.x), 2) + pow((p2.y - p1.y), 2))
   }

This method is basic mathematics and returns the distance between the points p1 and p2.

Once we have the distance and the speed values, we can calculate duration and create the movement action by specifying the position and the time for which we want the action to last.

The last line in the moveRabbitToNextLocation method is rabbit.runAction(moveAction), which will trigger the action, and without it, there won't be any movement at all as it sends the runAction message with the action we just created to the node we want to move.

Okay, enough code for now; run the project and see how our rabbit moves happily left and right!

However, if you touched the screen several times, you would notice some strange behavior in the movement action. Don't worry; it's due to the moveToX nature itself. When I introduced this action, I specified that it could be called concurrently, resulting in a movement that will be the sum of the individual movements, but in our case, it's making our rabbit look a bit crazy. To take control of the actions, we need to be aware of some methods that can be used to stop them whenever we need to.

Handling actions

In SpriteKit, we can trigger and stop actions whenever we want. This way, we can control what is happening at every moment. This is thanks to a collection of methods provided by SKNode, which are as follows:

  • hasActions(): This returns a Boolean value that indicates whether the node is running an action. We can take advantage of this method to check whether we can run an action as it may cause some wrong behavior if it takes place at the same time as the existing actions.
  • runAction(_:): As we have seen previously, this method runs SKAction passed as input parameter.
  • runAction(_:, completion:): This method is similar to the previous one, but the difference is that we can specify a block of code that we want executed as soon as the action finishes. It will be very useful to, for example, reset an enemy's position when its movement has completed.
  • runAction(_:, withKey:): Thanks to this method, we can specify a character chain to indicate the action; this way, we will be able to have direct control of the action.
  • actionForKey(_:): The previous method will allow us to get the specified key in order to retrieve the action, if it exists. If there is no matching key, it will return a nil value.
  • removeAllActions(): This method will stop all the running actions in a node. However, it could happen that when an action is removed, it makes a final change to the scene as it corresponds to changes previous to the removal.
  • removeActionForKey(_:): If we have stored the key value of one action that was previously run, we can use it to stop the action directly and leave the rest of the actions running.

All these methods can be useful during a game's development and, in fact, we will make use of several of them as we proceed. In our case, we just want to stop all actions so they don't concatenate, resulting in a strange behavior.

We just need to make one change in our code. In touchesBegan, add the following lines just after if let touch = touches.first {:

   // Controlling actions
           if rabbit.hasActions() {
               rabbit.removeAllActions()
           }

If you run the game, you will realize that now, the rabbit moves without any weird behavior even though you touch the screen several times.

Building the wall

When the game starts, our rabbit will start running, trying to find the exit of the top hat and, in its course, it will need to avoid closed doors. For this purpose, and first of all, we will need to create a wall, so let's start by adding the required images:

  1. Unzip 7338_02_Resources.zip and go back to Xcode.
  2. Right-click on Art and select Add Files to InsideTheHat.
  3. You'll find wall.png, wall@2x.png, wall@2x~ipad.png, wall@2x~iphone.png, and wall@3x.png on the 7338_02_Resources folder you just unzipped; select these three files and click on Add.

Now that we have the resources, let's call the method that will create the wall, so add the following line at the end of the didMoveToView method of GameScene:

self.initializeWall()

Before implementing the method, we will need to declare a variable for the wall, as we did with the rabbit, so add the following line just after the declaration of the rabbit variable:

private var wall: SKSpriteNode!

Now, implement the initializeWall method with the following block of code:

func initializeWall() {
       // Creating the wall sprite using an image file
       wall = SKSpriteNode(imageNamed: "wall")
       // Positioning the wall centered
       wall.position = CGPoint(x:(view!.bounds.size.width/2), y: view!.bounds.size.height/2)
     // Specifying zPosition
       wall.zPosition = 2
       // Adding the wall to the scene
       addChild(wall)
   }

This method is pretty similar to the one we used to create the rabbit. We initialize the wall variable using the image we just provided, set its position just in the center of the screen, and finally, add the new node to the scene. We have chosen the center of the screen just to see how the wall looks like, but that will change.

We also specify the zPosition value of the wall in order to achieve the result we want after adding all the objects required in the scene.

Now, it's time to run the game and check how it looks so far!

Running through the doors

We are going to manage the wall behavior. As we are simulating that our little rabbit is running, it will need to avoid the doors and the wall that will appear on top of the screen. To help simulate the rabbit's run, we will move the walls from the top to the bottom of the screen, and we already have the required knowledge to perform this task.

First of all, we need to place the original wall's position out of the screen, so take a look at the following line in initializeWall:

wall.position = CGPoint(x:(view!.bounds.size.width/2), y: view!.bounds.size.height/2)

Replace the preceding line with the following one:

wall.position = CGPoint(x:(view!.bounds.size.width/2), y: view!.bounds.size.height + wall.frame.size.height/2)

This way, we have set the wall in the following position:

Now that the wall is in its initial position, it's time to apply some movement to it, so let's call a new method by adding the following line at the end of the didMoveToView method of GameScene:

self.initializeWallMovement()

Implement it with the following block of code:

func initializeWallMovement() {
       // The constant wall's speed
       let wallSpeed: CGFloat = 250.0
       // Setting the wall's final position
       let nextWallPosition = CGPoint(x: wall.position.x, y: -wall.frame.size.height/2)
       // We want the wall to move on a constant speed
       let duration = self.distanceBetween(point: wall.position, andPoint: nextWallPosition) / wallSpeed
       // Move the wall to the next position
       let moveWallAction = SKAction.moveToY(nextWallPosition.y, duration: Double(duration))

       // Reset the wall's position
       let resetPositionAction = SKAction.runBlock {
         self.wall.position = CGPoint(x:(self.view!.bounds.size.width/2), y: self.view!.bounds.size.height + self.wall.frame.size.height/2)
       }

      // Executing the actions
       wall.runAction(SKAction.sequence([moveWallAction, resetPositionAction]))
   }

The preceding code is very similar to the one we used to move the rabbit. We first declare a constant variable for the wall's speed, and we specify the final position we want the wall to reach. Note that this final position is centered outside the view but on the bottom of the screen this time.

As we want the wall to move at a constant speed, we calculate the duration with the same strategy we created for the rabbit and then we create a moveToY action with these values as we just want the object to scroll vertically.

Once the movement action finishes, we want the wall to recover its initial position, and that's why we are going to take advantage of a special type of SKAction instance: sequences. A sequence is an action that allows us to execute an array of actions synchronously, so the first instance in the array will be run first, and as soon as it finishes, it will trigger the second action in the array, and so on.

Now that we know what a sequence is, we need a way to reset the wall's position, and that's why we are declaring resetPositionAction, a runBlock action that allows us to execute all the code we want whenever we need to. If you look at the block of code, you will see that we are just specifying the original wall's position.

One important thing when coding blocks is the scope of the variables. As we want to use a class variable, we need to specify self.wall or self.view, for example.

Finally, we execute a sequence with both the required actions in order to achieve the desired behavior. Run the game at this point, and you will see how the node disappears at the bottom of the screen. If you want, you can reset the position to a visible one so that you can check whether the sequence is running properly.

Summary

In this article, we focused our efforts on giving playability to the game.

We started learning the different ways we can take advantage of in order to handle touch interaction, and we used them to detect where the user touched the screen to traduce it on a movement of our main character.

In order to make the rabbit move, we learned the methods provided by the SKAction class in order to create and manage actions. Thanks to these utilities, we managed the lateral rabbit's movement and the looped movement of the wall and doors; this way, we created a group of objects that the user will need to avoid.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Getting Started with SpriteKit

Explore Title