Check out our Bootcamp Schedule View Schedule

tvOS Games, Part 1: Using the Game Controller Framework

Steve Sparks

Extended Inuse

My colleagues and I just shipped a tvOS project for a client in December. Toward the end, it was a high-powered rush to knock out some troublesome bugs so that we could ship on time. When we finally shipped, I had a lot of momentum. I had been thinking about the interaction model of tvOS and the applicability of game controllers, and so I decided to invest some time in learning about them.

Apple handles this through the Game Controller Framework on tvOS and iOS. Apple’s Game Controller Programming Guide is a good place to start. It’s written for iOS originally, as they talk about supporting those device-wrapping controllers; but like so many things in tvOS, it’s close enough to iOS that the pattern is still applicable.

In brief, there are static methods for starting and stopping controller discovery. Via notification, that process vends instances of GCController as needed. Any given GCController might have a GCMicroGamepad or a GCExtendedGamepad. Each gamepad has an assortment of GCControllerButtonInput and GCControllerDirectionPad properties.

I built the Controlla app (repo here) to detect and list all the controllers and show you all their inputs. It’s a simple app: there is only a centered vertical stack view, which will hold a horizontal panel for each control. The horizontal panels are built from simple building blocks.

Detecting Controllers

To get started, I needed to discover the available controllers. This is done through the time-honored process of subscribing to a few notifications and then kicking off the discovery thread. When the discovery thread has a controller for us, we’re going to get a call to our observer. I stubbed in add and remove methods, and after we have controller panels, we’ll manage the panel stack there.

func startWatchingForControllers() {
     // Subscribe for the notes
    let ctr = NotificationCenter.default
    ctr.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .main) { note in
        if let ctrl = note.object as? GCController {
            self.add(ctrl)
        }
    }
    ctr.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .main) { note in
        if let ctrl = note.object as? GCController {
            self.remove(ctrl)
        }
    }
    // and kick off discovery
    GCController.startWirelessControllerDiscovery(completionHandler: {})
}

func stopWatchingForControllers() { 
    // Same as the first, 'cept in reverse!
    GCController.stopWirelessControllerDiscovery()

    let ctr = NotificationCenter.default
    ctr.removeObserver(self, name: .GCControllerDidConnect, object: nil)
    ctr.removeObserver(self, name: .GCControllerDidDisconnect, object: nil)
}

func add(_ controller: GCController) {
    let name = String(describing:controller.vendorName)
    if let gamepad = controller.extendedGamepad {
        print("connect extended (name)")
    } else if let gamepad = controller.microGamepad {
        print("connect micro (name)")
    } else {
        print("Huh? (name)")
    }
}

func remove(_ controller: GCController) {

}

What to do with my new-found controller(s)? Well, as you may have surmised from the code above, they’re going to be one of two types.

The Micro Gamepad

If you wish to support the use of the Siri Remote as a gameplay controller, you’re going to interact with its GCMicroGamepad.
It has an XY input (the touch-sensitive top area) and two buttons: A (Play/Pause) and X (pressing the top area.) It also offers up a GCMotion, which aligns closely with Core Motion’s CMMotion classes.

The Extended Gamepad

The GCExtendedGamepad documentation lists the set of inputs available:

  • Two shoulder buttons.
  • Two triggers.
  • Four face buttons arranged in a diamond pattern. (“ABXY”)
  • One directional pad.
  • Two thumbsticks.

Also, there is an array of LEDs. This isn’t an input but an output; the controller has a playerIndex property which takes an enum. You should honor the spirit of this and set the value appropriately for your game and your use.

Reading Inputs

Here are all the ways it is possible to observe the values of the direction pad.

guard let gamepad = gamepad as? GCMicroGamepad else { return }

// method one
gamepad.valueChangedHandler = { (gamepad, element) in
    if let dpad = element as? GCControllerDirectionPad {
        print("CTRL : ( dpad )")
    } else {
        print("OTHR : ( element )")
    }
}

// method two
gamepad.dpad.valueChangedHandler = { (dpad, xValue, yValue) in
    print("DPAD : ( dpad )")
}

// method three
gamepad.dpad.xAxis.valueChangedHandler = { (axis, value) in
    print("AXIS: ( axis ) -> ( value ) ")
}
    
// A bonus: grab the play/pause event
if let ctrl = gamepad.controller {
    ctrl.controllerPausedHandler = { controller in
        // play/pause here
        print("PLAY: ( controller ) ")
    }
}

A diagonal swipe produces…

AXIS: Axis -> 0.0435127 
DPAD : DirectionPad (x: 0.044, y: -0.025)
DPAD : DirectionPad (x: 0.044, y: -0.025)
CTRL : DirectionPad (x: 0.044, y: -0.025)
AXIS: Axis -> 0.0873253 
DPAD : DirectionPad (x: 0.087, y: -0.068)
DPAD : DirectionPad (x: 0.087, y: -0.068)
CTRL : DirectionPad (x: 0.087, y: -0.068)
AXIS: Axis -> 0.0913773 
DPAD : DirectionPad (x: 0.091, y: -0.086)
DPAD : DirectionPad (x: 0.091, y: -0.086)
CTRL : DirectionPad (x: 0.091, y: -0.086)

… and so on.

Notice that the handlers are called outward: First the axis’s handler, then the pad’s handler, then the controller’s handler. Also notice that the axis has a bad description implementation.

There is a fourth method for getting the controller’s values, which you might use if your game has an internal time loop separate from the display loop. It looks like this:

let shot = gamepad.saveSnapshot()

This returns a GCMicroGamepadSnapshot, which offers all the values of the GCMicroGamepad for a moment in time that you can process at your leisure.

Displaying the Controller

I pulled out my notebook and sketched a few horizontal designs for the micro and extended gamepads. This is what I came up with, recreated in Omnigraffle:

Gamepad Layout

The directional pad view needed to display values between (-1.0, 1.0) as shown in the diagram above. I handled this by adding a circular CALayer to a square view, with some logic to translate the pad values to coordinates for the circle’s center. That’s implemented in XYPositionView, which is subclassed as DirectionPadView and MotionView. The latter class also changes the circle’s radius to indicate the Z dimension, and the effect is quite striking:

Micro In-Use

I saw that I was going to need a view to display the on-off state of a controller button. I began with a UIButton subclass for my ButtonIndicatorView. Simply by modifying the enabled flag, I thought I could display on and off states. This didn’t work well at all! The focus engine thought I was trying to navigate a sea of buttons. What a holy interaction mess that was! The left stick and D-pad would move focus from button to button. The additional highlight and sizing provided by the focus engine made the display ugly, and worse, confusing. The focus engine—in a game, I’d almost always want to handle that interaction myself.

Disabling Focus

GameControllerKit includes an GCEventViewController, which is a simple view controller subclass that adds a controllerUserInteractionEnabled toggle. When it is set to false, none of the standard actions (eg. direction commands, Menu, and Select) are honored. Since Menu is what exits the app, I’d have to deal with that eventually, but not just yet. I wanna see those controls!

I swapped UILabel for UIButton. After, all, they’re only for display, and don’t need to be interactive. The label dims the background color when the button is active. Then, I created a container view for the ABXY buttons. It creates the buttons, colors their labels, attaches them to the controller, and sets their constraints.

Rather than code-dump these classes here, you can go see them in the Github repository.

Escape!

Remember how Menu no longer worked? I handled it here:

override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
    var menuPressed = false
    if presses.contains(where: { press in press.type == .menu }) {
        menuPressed = true
    }
    if (menuPressed) {
        self.controllerUserInteractionEnabled = true
        let alert = UIAlertController(title: "Quit?", message: "You sure you're out?", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Yep, I'm out", style: .default) { _ in
            super.pressesBegan(presses, with: event)
        })
        alert.addAction(UIAlertAction(title: "Nevermind", style: .default) { _ in
            self.controllerUserInteractionEnabled = false
        })
        self.present(alert, animated: true, completion: nil)
    }
}

Knowledge is Power

Okay, now I’ve got a nice handle on how controllers work. In my next installment, we’ll build a game inspired by the classic Robotron: 2084 game in SpriteKit. We’ll use what we learned here, add what we learned putting SpriteKit on the watch. Then we’ll add in some state machines using GameplayKit, and finally a leaderboard using GameKit.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project