How to Support External Game Controllers with Swift 2 and Sprite Kit for the new Apple TV

How to Support External Game Controllers with Swift 2 and Sprite Kit for the new Apple TV
January 21, 2016 Justin at CartoonSmart

 

How to Support External Game Controllers with Swift 2 and Sprite Kit for the New Apple TV

And add finer controls for the Micro Controller (Apple TV Remote)

 

Today I taught my second lesson on how to support external game controllers for either tvOS or iOS apps. This lesson was part of CartoonSmart’s course on how to make a Swift and Sprite Kit pinball game, and the previous lesson was for a fun series all about two player gameplay with the new Apple TV. For those of you stumbling around the internet looking for some info on this, I’d love for you to subscribe to CartoonSmart and fully dive into either tutorial, but this is now such an important topic, I thought we should at least write a free article on the topic.

A little background: I’ve made a few apps that support external controllers (all approved with flying colors by Apple), so what you’ll read below definitely works in the “real world”.  If you’re curious to play those apps, check out Tank Battle (only available when searching from the new Apple TV), Horde, or Horde 2. Those last two are for both iOS or tvOS, and external controllers are supported for both device types.

So enough intro, lets get down to it!


 

Starting a new Sprite Kit project for tvOS or iOS

Start a new Sprite Kit based project using Swift (you can choose either a tvOS or iOS project)….

Sprite Kit Project for tvOS

The starting template includes the GameScene.swift file already created for you. What we’ll do is create a new file called GameScene_GameControllers.swift which is simply an extension of the GameScene.swift.

Extension for a class in Swift 2

It is not a separate class, it is just a new file to write code in for the GameScene. Why do this? I think it is a great way to organize your code. If you’ve ever written 2 or 3 thousand lines in a single file, you know that Xcode can slow down to a crawl after a while, so separating classes into multiple files can alleviate some of that misery.

To extend a class, you simply need to write extension after your import statements, then the class name you are extending from. From there, you can write any function as you normally would within an opening and closing bracket. Take a look at our first function below, named setUpControllerObservers,  within the class extension….

Extension for a class in Swift 2

Unfold to copy the code...


import Foundation

import SpriteKit

import GameController

extension GameScene {

func setUpControllerObservers(){

NSNotificationCenter.defaultCenter().addObserver(self, selector: “connectControllers”, name: GCControllerDidConnectNotification, object: nil)

NSNotificationCenter.defaultCenter().addObserver(self, selector: “controllerDisconnected”, name: GCControllerDidDisconnectNotification, object: nil)

}

}

The setUpControllerObservers function creates two NSNotifications that listen for an external game controller being connected or disconnected. In the tvOS world, this includes the Micro Controller (the new Apple TV remote). The function that runs when a controller is connected is named, connectControllers and the function that runs when a controller is disconnected is named controllerDisconnected .

Before we get ahead of ourselves, we need to actually call setUpControllerObservers. You’ll most likely want to do this in your didMoveToView class (back in the main GameScene.swift file). Below you can see how that looks…

didMoveToView statement

Notice too, we are calling connectControllers ourselves right after setUpControllerObservers. The first time the app runs, setUpControllerObservers will cause our NSNotification to also call connectControllers, but if we were to go back and forth between our GameScene and other class, we can’t rely on connectControllers getting called again from the notification. Which is why we call it ourselves.

We’ll spend a lot  of this article discussing the connectControllers statement, so lets go ahead and knock out the controllerDisconnected function before that. Here’s all there is to it….

Game Controller disconnected statement

Apple requires us to pause the scene when a controller is disconnected.  You can of course add anything else to this statement you want.


Connecting Controllers

Onto our connectControllers statement. One of the first things we’ll do is un-pause the scene in case it was temporarily paused from a controller disconnecting. We will then use a for statement to iterate through every controller in the GCController.controllers() array.

Connecting external controllers with Swift and Sprite Kit

Within the for statement, you can see above we have an if…else if… statement to test if the controller’s extendedGamepad or gamepad statement does not equal nil, which means the controller either is an extendedGamepad or (standard) gamepad. An extended gamepad includes the right and left thumbstick as well as two top buttons on each side. Here’s an example for an extended gamepad controller…

Extended Game Controller

Whereas a standard game controller has fewer buttons, see the image below…

what does a standard gamepad controller look like

Back to the code. Within both if statements, we set a valueChangedHandler to nil, which really only matters if we are going back and forth between two classes that both are setting up external controllers. Basically we are clearing out what the controller does, then use the setUpExtendedController function or setUpStandardController function to define how the controller reacts to button, thumbstick, or directional pad presses. We’ll write those functions in just a moment, but we still have some work in our connectControllers function.

So let’s continue writing where we left off and address the microGamepad (which is the Apple TV remote). Notice we are writing a new for statement for the microGamepad. Connecting micro controllers with Swift and Sprite Kit 3

We do this for a couple reasons. First, we only need to run this for tvOS apps which is why the entire for statement is within  the #if os(tvOS) and #endif.   Second, this gives us an opportunity to prioritize the extended and standard gamepads over the microGamepad. Let’s face it, the new Apple TV remote is not a great game controller. To favor a particular controller, you could set the player index for it to Player 1 instead of Player 2. So for example, in a two player game, whoever was player 1 would always have the better controller. Not exactly fair, I know, but hey, maybe that’s just House Rules.  To set a player index you do so with the code….

controller.playerIndex = .Index1

The example doesn’t require us to set the player index so I’ve left it out of that code, but that’s something you might want to consider adding in the first for statement. But for a one player only game, the index really doesn’t matter anyway.

To copy all the code for the connectControllers function, you can do so below…

Unfold to copy the code...


func connectControllers(){

self.paused = false

for controller in GCController.controllers() {

if (controller.extendedGamepad != nil ) {

controller.extendedGamepad?.valueChangedHandler = nil

setUpExtendedController (controller)

}  else if (controller.gamepad != nil ) {

controller.gamepad?.valueChangedHandler = nil

setUpStandardController (controller)

}

}

//… to be continued…

#if os(tvOS)

for controller in GCController.controllers() {

if ( controller.extendedGamepad != nil) {

//ignore

} else if ( controller.gamepad != nil) {

//ignore

} else if ( controller.microGamepad != nil) {

controller.microGamepad?.valueChangedHandler = nil

setUpMicroController( controller )

}

}

#endif

}

Detecting Button and Directional Presses

Here comes the real fun. Next we’ll write the function setUpExtendedController. You can see the function has one object being passed into it: the controller of type GCController. The first thing we do is attach a valueChangedHandler to the controller. What follows after the initial opening bracket is a block of code that is called any time a value changes with the controller. This includes buttons or directional pads being pressed or released. So anything that happens with the controller triggers this. Below you can see the beginning of the function, along with some code commented out, and the if statements to test if the right or left thumbstick were what triggered the handler…

Swift and Sprite Kit, detetecting button presses on extended game pad controllers

The code that is commented out is irrelevant to this example, but this is how you could check to see if it was Player 1, Player 2, etc, that was triggering the value changed handler.  If you didn’t check who was playing, then multiple controllers could potentially control a single character. For a one player game, that’s fine though. You probably want both the Apple TV remote and external controller to do the same thing.

So lets look at the first if statement and it’s interior if statement….

 


if (gamepad.leftThumbstick == element) {

if (gamepad.leftThumbstick.left.value > 0.2) {

//do something for a left thumbstick movement in the left direction

} else if (gamepad.leftThumbstick.left.pressed == false) {

// do something if the left movement is let go of

}

}

First we check to see if the element is the gamepad.leftThumbstick. This isolates which element on the controller is being manipulated. For an extended game controller possible elements include:

  • leftThumbstick
  • leftShoulder
  • leftTrigger
  • rightThumbstick
  • rightShoulder
  • rightTrigger
  • dpad
  • buttonX
  • buttonY
  • buttonB
  • buttonA

Notice in the interior if statement, we’re only checking the left thumbstick’s left value. Yes I wrote that right. I mean, correctly. We could also check it’s up, down and right values as well. But to keep things simple, let’s just look at the left value. You can check a range from 0 to 1. Here we are checking to see if it is over 0.2, which (to me) means the left thumbstick has intentionally been moved left. Below 0.2 the user might have accidentally triggered it by playing like a 4 year old (as my 4 year old has demonstrated).

That’s one way to detect a left movement. We could also have done something similar to the else if statement, which checks if gamepad.leftThumbstick.left.pressed == false. So we could have just checked if it equaled true, but that’s obviously not as fine-tuned of a test. Checking false though works great to see if the user has stopped pushing left. In which case, you could do any number of things, for example, you might let go of a pinball flipper or make a game character stop moving altogether.

The rightThumbstick works the same way.

Let’s look at the gamepad.dpad code now….

Directional pad code for external game controllers with Swift and Sprite kit

By the way, dpad is short for directional pad, and is this part of the controller…

Which is the dpad on the extended controller

Did I really just explain what a dpad is short for and go as far as circling AND using an arrow to point out the dpad. I must be assuming my typical reader is far less versed in video games as me.

So here’s the code…


else if (gamepad.dpad == element) {

if (gamepad.dpad.right.pressed == true){

// do something for pressing right

} else if (gamepad.dpad.right.pressed == false){

// do something for releasing right

}

if (gamepad.dpad.left.pressed == true){

// do something for pressing left

} else if (gamepad.dpad.left.pressed == false){

// do something for releasing left

}

}

Is it obvious yet this code comes from my recent pinball tutorials (since it’s mostly listening for right and left presses)?

So this time we aren’t checking for a particular value, only whether or not the dpad’s right or left .pressed property is true or false.  Obviously we could check up or down as well.

Guess what? All other buttons work the same way. So in that previous block of code, you could replace dpad.left or dpad.right with any of the other elements mentioned above: leftTrigger, rightTrigger, buttonA, buttonY, etc.

You can cut and paste the entire function, including if statements for every button, from the snippets below. The setUpStandardController function is included as well, as it’s nearly identical but has fewer buttons to test for…

 

Show code for the entire setUpExtendedController


func setUpExtendedController( controller:GCController) {

controller.extendedGamepad?.valueChangedHandler = {

(gamepad: GCExtendedGamepad, element:GCControllerElement) in

if (gamepad.controller?.playerIndex == .Index1) {

// this is player 1 playing the controller

} else if (gamepad.controller?.playerIndex == .Index2) {

// this is player 1 playing the controller

}

 

if (gamepad.leftThumbstick == element) {

if (gamepad.leftThumbstick.left.value > 0.2) {

print(“pressed leftThumbstick left”)

} else if (gamepad.leftThumbstick.left.pressed == false) {

print (“left go of leftThumbstick left”)

}

} else if (gamepad.rightThumbstick == element) {

if (gamepad.rightThumbstick.right.value > 0.2) {

print(“pressed rightThumbstick right”)

} else if (gamepad.rightThumbstick.right.pressed == false) {

print (“left go of rightThumbstick right”)

}

} else if (gamepad.dpad == element) {

if (gamepad.dpad.right.pressed == true){

print(“pressed dpad right”)

} else if (gamepad.dpad.right.pressed == false){

print(“let go of dpad right”)

}

if (gamepad.dpad.left.pressed == true){

print(“pressed dpad left”)

} else if (gamepad.dpad.left.pressed == false){

print(“let go of dpad left”)

}

} else if (gamepad.leftShoulder == element){

if ( gamepad.leftShoulder.pressed == true){

print(“leftShoulder pressed”)

} else if ( gamepad.leftShoulder.pressed == false) {

print(“leftShoulder released”)

}

}

else if (gamepad.leftTrigger == element){

if ( gamepad.leftTrigger.pressed == true){

print(“leftTrigger pressed”)

} else if ( gamepad.leftTrigger.pressed == false) {

print(“leftTrigger released”)

}

}

else if (gamepad.rightShoulder == element){

if ( gamepad.rightShoulder.pressed == true){

print(“rightShoulder pressed”)

} else if ( gamepad.rightShoulder.pressed == false) {

print(“rightShoulder released”)

}

}

else if (gamepad.rightTrigger == element){

if ( gamepad.rightTrigger.pressed == true){

print(“rightTrigger pressed”)

} else if ( gamepad.rightTrigger.pressed == false) {

print(“rightTrigger released”)

}

} else if ( gamepad.buttonA == element) {

if ( gamepad.buttonA.pressed == true){

print(“buttonA pressed”)

} else if ( gamepad.buttonA.pressed == false) {

print(“buttonA released”)

}

} else if ( gamepad.buttonY == element) {

if ( gamepad.buttonY.pressed == true){

print(“buttonY pressed”)

} else if ( gamepad.buttonY.pressed == false) {

print(“buttonY released”)

}

} else if ( gamepad.buttonB == element) {

if ( gamepad.buttonB.pressed == true){

print(“buttonB pressed”)

} else if ( gamepad.buttonB.pressed == false) {

print(“buttonB released”)

}

} else if ( gamepad.buttonX == element) {

if ( gamepad.buttonX.pressed == true){

print(“buttonX pressed”)

} else if ( gamepad.buttonX.pressed == false) {

print(“buttonX released”)

}

}

}

}

Show code for the entire setUpStandardController


func setUpStandardController( controller:GCController) {

controller.gamepad?.valueChangedHandler = {

(gamepad: GCGamepad, element:GCControllerElement) in

if (gamepad.controller?.playerIndex == .Index1) {

// this is player 1 playing the controller

} else if (gamepad.controller?.playerIndex == .Index2) {

// this is player 1 playing the controller

}

if (gamepad.dpad == element) {

if (gamepad.dpad.right.pressed == true){

print(“pressed dpad right”)

} else if (gamepad.dpad.right.pressed == false){

print(“let go of dpad right”)

}

if (gamepad.dpad.left.pressed == true){

print(“pressed dpad left”)

} else if (gamepad.dpad.left.pressed == false){

print(“let go of dpad left”)

}

} else if (gamepad.leftShoulder == element){

if ( gamepad.leftShoulder.pressed == true){

print(“leftShoulder pressed”)

} else if ( gamepad.leftShoulder.pressed == false) {

print(“leftShoulder released”)

}

}

else if ( gamepad.buttonA == element) {

if ( gamepad.buttonA.pressed == true){

print(“buttonA pressed”)

} else if ( gamepad.buttonA.pressed == false) {

print(“buttonA released”)

}

} else if ( gamepad.buttonY == element) {

if ( gamepad.buttonY.pressed == true){

print(“buttonY pressed”)

} else if ( gamepad.buttonY.pressed == false) {

print(“buttonY released”)

}

} else if ( gamepad.buttonB == element) {

if ( gamepad.buttonB.pressed == true){

print(“buttonB pressed”)

} else if ( gamepad.buttonB.pressed == false) {

print(“buttonB released”)

}

} else if ( gamepad.buttonX == element) {

if ( gamepad.buttonX.pressed == true){

print(“buttonX pressed”)

} else if ( gamepad.buttonX.pressed == false) {

print(“buttonX released”)

}

}

}

}

 

The MicroGamepad

The Micro Gamepad (aka new Apple TV remote) has direction properties for the dpad, and a buttonX / buttonA. Where the heck are those buttons you might ask? Well, here’s a diagram to help you remember. ButtonA is considered a hard press down on the entire touch surface area, and buttonX is the Play / Pause button. With a simple line of code, which you’ll see in a moment, you can even rotate the remote to more accurately simulate a standard game controller. Yes, you’ll feel like you grew 10 feet taller but it’s better than trying to play a side-scroller game with the remote in upright mode.

The touch surface is consider the dpad, and you can see in the image where the Up and other buttons are in both orientations.

MicroGame Pad (new Apple TV Remote) buttonA and ButtonX

You can see some of the initial code for the setUpMicroController below. It is nearly identical to what we’ve seen before so I won’t over explain it. But note that we are using the #if os(tvOS) statement before the entire function and bookending it with the #endif statement so this entire function is ignored on iOS.

Then within the valueChangedHandler we are setting reportsAbsoluteDpadValues to true and allowsRotation to true. If you don’t set reportsAbsoluteDpadValues to true, testing values on the dpad isn’t as responsive and setting allowsRotation to true lets players turn the remote sideways.  

Code for the microgamepad with Swift and SpriteKit

As you can see below, we still test to see what the element is, whether thats gamepad.buttonA, gamepad.buttonX or dpad . From there, we do the same if testing as before.  Notice though in my dpad tests, I check if the value is greater than 0.1 in a specific direction or else if it equals 0.0.  I found this to work best in the apps I was working on. You might want to tinker with those values for your particular app.

Code for the microgamepad with Swift and SpriteKit 2

Below you can copy and paste the entire setUpMicroController function.

Show code for the entire setUpMicroController function...


#if os(tvOS)

func setUpMicroController( controller:GCController) {

controller.microGamepad?.valueChangedHandler = {

(gamepad:GCMicroGamepad, element:GCControllerElement) in

gamepad.reportsAbsoluteDpadValues = true

gamepad.allowsRotation = true

if ( gamepad.buttonX == element) {

 

if (gamepad.buttonX.pressed == true){

//Button X is the play / pause button on the new Apple TV remote

print(“pressed buttonX on the microGamepad”)

} else if (gamepad.buttonX.pressed == false ){

print(“released buttonX on the microGamepad”)

}

} else if ( gamepad.buttonA == element) {

//Button A is usually activated by a harder press on the touchpad.

if (gamepad.buttonA.pressed == true){

print(“pressed buttonA on the microGamepad”)

} else if (gamepad.buttonA.pressed == false ){

print(“released buttonA on the microGamepad”)

}

}

else if (gamepad.dpad == element) {

if (gamepad.dpad.right.value > 0.1) {

print(“pressed right”)

} else if (gamepad.dpad.right.value == 0.0) {

print(“released right”)

}

if (gamepad.dpad.left.value > 0.1) {

print(“pressed left”)

} else if (gamepad.dpad.left.value == 0.0) {

print(“released left”)

}

}

}

}

#endif

Folks, thats it!

If you’re a CartoonSmart subscriber, you can download the entire Xcode project below…

This content is restricted to buyers of The CartoonSmart Subscription.

Never lose your place.

Sign up for the newsletter to get a free CartoonSmart account and track your progress in every course.

I'm cool. Sign me up!