Handling touch events

Categories: Adventure Game

In this series of posts I describe the process I've followed, as an amateur game designer, for developing an adventure game written in Swift using the SpriteKit framework for iOS.

Introduction

All games support some kind of user interaction, right?

In the case of classic point and click adventure games, the player should be able to move the cursor and click to interact with the game environment, talking to other characters, and selecting actions from the HUD.

In the next two posts I’m going to add support for mouse and touch events, depending on the target device, for moving the cursor node.

Let’s start by handling touch events on iOS and tvOS.

Controlling User Interaction on Nodes

SKNode subclasses UIResponder in iOS and tvOS, and NSResponder in macOS, that means that I can override the methods touches* or mouse* respectively for handling touch or mouse events.

There are two main strategies to handle user interaction:

  1. Centralize the handling in the scene node
  2. Delegate the handling to every node that needs to react to user interactions

I’ve decided to adopt the first strategy because it seems simpler and more straight forward to implement.

Moving the cursor (on iOS and tvOS)

For touch devices, I’ll change the position of the cursor node on touchesBegan and on touchesMoved:

1
2
3
4
5
6
7
8
9
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: self) else { return }
    cursor.position = location
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: self) else { return }
    cursor.position = location
}

The implementation of both handlers is the same, so I could share their common implementation in a function, but for the sake of simplicity let’s just duplicate some code.

touches is a set of UITouch because iOS devices have multi-touch support, but for now I just care about the first touch. The guard in first line of the function does exactly that. It assigns the position of the first touch in the set to location or it returns if touches is empty.

location(in: self) is used to return a CGPoint for the position of the touch in the scene (self).

In the second line I assign location to the cursor’s position instance property, that’s the easiest way to move the cursor node in the scene.

In order to be able to reference cursor in these functions I’ve updated the setUpScene implementation. I’ve replaced the assignment of a new Cursor instance to the local variable cursor:

1
let cursor = Cursor(120)

with a new private instance property cursor that’s lazily initialized:

1
private lazy var cursor = Cursor(120)

This is the result on iPhone:

There’s a small issue with the movements of the cursor on tvOS. The cursor starts moving always from the center of the screen:

The reason why this is happening is because on tvOS touchesBegan resets the position of touch events in the current touches series.

I couldn’t find any official documentation for this behavior, so if you find something let me know in the Reddit discussion.

The easiest way I’ve found to handle this issue is to make the touch position relative to the last cursor position. To do so I add a lastPosition instance property in Cursor:

1
var lastPosition: CGPoint = .zero

This instance property will keep track of the last cursor position at the end of each touch events series. Its initial value is the cursor’s position and it’s assigned right after setting the initial cursor position in setUpScene:

1
2
3
cursor.position = CGPoint(x: frame.midX, y: frame.midY)
cursor.lastPosition = cursor.position
addChild(cursor)

lastPosition is updated in both touchesEnded and touchesCancelled, when a touch events series ends, with the current cursor position:

1
2
3
4
5
6
7
8
9
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard touches.count == 1 else { return }
    cursor.lastPosition = cursor.position
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard touches.count == 1 else { return }
    cursor.lastPosition = cursor.position
}

The new implementation of touchesBegan and touchesMoved call touchHandler, that is a GameScene’s private instance method (the one that I lazily refused to create earlier), to update the cursor position relatively to its last position if the device os is tvOS:

1
2
3
4
5
6
7
8
9
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: self) else { return }
    touchHandler(location)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: self) else { return }
    touchHandler(location)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func touchHandler(_ location: CGPoint) {
    #if os(iOS)
    let nextPosition = location
    #elseif os(tvOS)
    let delta = CGPoint(
        x: location.x - frame.midX,
        y: location.y - frame.midY
    )
    let nextPosition = CGPoint(
        x: cursor.lastPosition.x + delta.x,
        y: cursor.lastPosition.y + delta.y
    )
    #endif

    cursor.position = nextPosition
}

If os(iOS) is true nothing changes, the next cursor position is simply the touch event position. Alternatively, if os(tvOS) is true, the function computes the delta between the touch event position and the screen mid point. This offsets the event position by (frame.midX, frame.midY) because location starts always at the screen mid point. Then the function computes the next cursor position by adding the delta to the cursor last position.

This is the result on tvOS:

Conclusion

Now the app can react to touch events by moving the cursor on the screen.

In the next post I’ll handle mouse events for moving the cursor on macOS.

Resources

Discussion on r/iOSProgramming

Next: Handling mouse events