How to Make Pong in SpriteKit Part II: Touch Events
Moving the Paddles

by Lou Franco

In the previous article, we set up the visible elements of Pong and the basic physics that governs the movement of the ball.

At the end of the article, we had the ball bouncing off the walls and paddles and moving in a gravity-free, drag-free environment.

The next thing we'll tackle is letting the players move the paddles.

Manual Movement in SpriteKit

As we've discussed in the previous articles in this series, you can think of a SpriteKit game as a physics simulation. If you assign a physics body object to a node, it can have forces applied to it. It will have a velocity. The objects will bounce off each other.

If you set everything up, most movement will be taken care of for you.

But sometimes it's easier to move the nodes manually. You might want a node to take part in the simulation, but not to be moved by it.

This is common in walls and the ground. Objects might bounce off of them, but they aren't moved by the bounce.

For something like the paddles in Pong, we don't want them to be moved by the physics simulation, but we do need to move them manually.

To do that, we used the manualMovement() category function that I introduced in this article about SKPhysicsBody extensions.

Here was the code:

paddle.physicsBody = SKPhysicsBody(rectangleOf: paddleSize)
  .ideal()
  .manualMovement()

Remember that ideal() makes the body behave like an ideal object that has perfect bounces but no friction or drag. manualMovement() maintains the body as a physical body that objects bounce off of, but makes sure it won't be moved by the physics simulation.

But, now we have to update its position ourselves. We'll do that by detecting touches and then moving the paddles based on them.

Detecting touches

A SpriteKit game is implemented inside a subclass of SKScene, which has functions that we can override to process touch events. We made stubs in the last article.

// This function is called if a finger is put on the screen
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}

// This function is called if a finger is lifted off the screen
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}

The first function is called when the user first puts a finger down on the screen, and the second is called when a finger is lifted.

Since we have multiple players, and they have multiple fingers, we can get simultaneous touches, which is why we are passed a set.

For our game, we're going to break the screen into quadrants. When the bottom-left area has a finger down in it, we'll move the bottom paddle to the left. When the bottom-right has one, we'll move the bottom paddle to the right. And we'll move the top paddle based on the top quadrants.

To help with that, let's define this enum:

enum PaddleDirection {
  case left
  case right
  case still
}

And these two state property variables

var topPaddleDirection = PaddleDirection.still
var bottomPaddleDirection = PaddleDirection.still

In each touches function, we need to loop through the touches and see what quadrant they are in by using touch.location(in:) passing in the scene (self). Then, we just set the paddle direction based on it.

Here's touchesBegan:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  guard let view = self.view else { return }

  for touch in touches {
    let location = touch.location(in: self)
    let direction: PaddleDirection =
      (location.x < view.frame.midX) ? .left : .right

    if location.y < view.frame.midY {
      bottomPaddleDirection = direction
    } else {
      topPaddleDirection = direction
    }
  }
}

And in touchesEnded, we need to set the direction to still when the finger is lifted.

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  guard let view = self.view else { return }
  for touch in touches {
    let location = touch.location(in: self)

    if location.y < view.frame.midY {
      bottomPaddleDirection = .still
    } else {
      topPaddleDirection = .still
    }
  }
}

Now that the direction is set, we can update the paddle position based on it.

Using the update function

As we saw, SKScene also has a function called update(currentTime:) that we can override. We made a stub in the last article.

override func update(_ currentTime: TimeInterval) { 
}

As we covered, this function is called in every iteration of the game loop.

It's a convenient place to do any other kind of movement we might want in addition to the physics simulation.

To make it easier, let's first make a movePaddle() function.

func movePaddle(_ paddle: SKNode?, direction: PaddleDirection, amount: CGFloat) {
  guard let view = self.view else { return }
  guard let paddle = paddle else { return }

  var pos = paddle.position
  switch direction {
  case .left:
    pos.x = pos.x - amount
    if pos.x < wallWidth + paddleSize.width/2 {
      pos.x = wallWidth + paddleSize.width/2
    }
  case .right:
    pos.x = pos.x + amount
    if pos.x > view.frame.maxX - wallWidth - paddleSize.width / 2 {
      pos.x = view.frame.maxX - wallWidth - paddleSize.width / 2
    }
  case .still:
    return
  }
  paddle.position = pos
}

The if statements inside the cases just make sure the paddle stays on the screen. Remember that the position is the center of the shape.

To implement update correctly, we need to take the currentTime parameter into account. To do that, it's useful to keep track of the time since the last time update was called with a property.

var lastUpdateTime: TimeInterval = 0

Then, at the top of update, we need to do this:

defer {
  lastUpdateTime = currentTime
}

guard lastUpdateTime > 0 else {
  return
}

Now, in the rest of the function we can know the time since the last call by using currentTime - lastUpdateTime. For example, add this code to update to figure out how much to move the paddle

let amount = CGFloat(50.0 * (currentTime - lastUpdateTime))

Which can be interpreted as a constant 50 points per second.

We can't predict how fast the game loop is running since it depends on the current frame rate of the game. If we didn't base the position changes on the time difference, then the animations would be choppy.

Finally, add this code to the end of update

movePaddle(bottomPaddle, direction: bottomPaddleDirection, amount: amount)
movePaddle(topPaddle, direction: topPaddleDirection, amount: amount)

Run the game, the paddles should move based on where you touch the screen.

Subscribe to be notified of the next SpriteKit article

We'll continue this in the next article where we'll detect when the ball goes off the screen.

Make sure to sign up to my newsletter to get notified when a new article is published.

Contact me if you are working on a game and need help.

Next Article: How to Make Pong in SpriteKit Part III: Detecting a miss

Never miss an article

Get more articles like this in your inbox.