How to read MFi Controller input in iOS

Nimbus ControllerI bought a SteelSeries Nimbus controller the other day. It’s Made For iOS (MFi) and supported by many games, including those on OS X and tvOS.

Seriously, it’s a phenomenal controller! Naturally I wanted to see how easy (or difficult) it was to use it in my own projects.

Turns out that for game developers, it’s not that tough to make it work – thanks to Apple’s great GameController Framework that was introduced in iOS 7.

Here’s how I did it, following Apple’s document called Game Controller Programming Guide.

Reacting to Controller connections / disconnections

The first thing we should probably do is make our app become aware if a controller has been switched on or off. An MFi controller must be paired only once and will then connect automatically to our device. We’ll assume a user has already paired the controller (via the Settings app, under Bluetooth).

iOS will let us know when a new controller has been connected or disconnected via notifications, so let’s react to them appropriately. I’ll do this in viewDidLoad:

@import GameController;

...

- (void)viewDidLoad {

    [super viewDidLoad];

    
    // don't fall asleep while our app is running

    [[UIApplication sharedApplication]setIdleTimerDisabled:YES];

    

    // notifications for controller (dis)connect

    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(controllerWasConnected:) name:GCControllerDidConnectNotification object:nil];

    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(controllerWasDisconnected:) name:GCControllerDidDisconnectNotification object:nil];
}

- (void)controllerWasConnected:(NSNotification *)notification {


    // a controller was connected

    GCController *controller = (GCController *)notification.object;

    NSString *status = [NSString stringWithFormat:@"Controller connected\nName: %@\n", controller.vendorName];

    self.statusLabel.text = status;


    self.mainController = controller;

    [self reactToInput];
}


- (void)controllerWasDisconnected:(NSNotification *)notification {


    // a controller was disconnected

    GCController *controller = (GCController *)notification.object;

    NSString *status = [NSString stringWithFormat:@"Controller disconnected:\n%@", controller.vendorName];

    self.statusLabel.text = status;

 
    self.mainController = nil;
}

We need to import the GameController framework into our class for any of the following to work. I’ve also added a line that prevents the device from going to sleep – very important during a gaming session that expects no touch input while we’re holding the controller.

Each observer reacts when a controller was either connected or disconnected and calls an appropriate method we need to write via a selector. The notification itself contains the controller in its object property, so we’ll typecast it as a GCController object. We can then read out the various attributes of the controller (for example, if it supports the micro, standard or extended profile, or if it’s physically attached to the device, etc).

In our demo we’ll simply print a message and start processing input detection. We can also use this opportunity to give the controller a player index: up to 4 controllers can be connected to a device (I’ll dispense with this for now though).

NOTE: Apple demand that a controller can only be an additional input device on iOS. We’re not allowed to create games that demand such a controller and must implement touch controls first, and make the use of a controller optional. Our demo doesn’t follow these guidelines – but a real life app needs to process touch input if no controller is connected.

Reacting to Controller Input

There are two ways of reacting to controller input: either via a callback block that is executed if input from a controller is received, or by directly accessing each value at a moment determined by your app (say once every frame, or once every drawing cycle). I’ll choose the first method because the demo doesn’t really do anything in regards to frame cycles.

I’ll put all this logic into a huge method that needs to be called once for the blocks to register. Here’s a sample of reading one element each:

- (void)reactToInput {    

    // register block for input change detection

    GCExtendedGamepad *profile = self.mainController.extendedGamepad;

    profile.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element)

    {

        NSString *message = @"";

        // left trigger

        if (gamepad.leftTrigger == element && gamepad.leftTrigger.isPressed) {

            message = @"Left Trigger";

        }

        // right shoulder button

        if (gamepad.rightShoulder == element && gamepad.rightShoulder.isPressed) {

            message = @"Right Shoulder Button";

        }        

        // X button

        if (gamepad.buttonX == element && gamepad.buttonX.isPressed) {

            message = @"X Button";

        }

        // d-pad

        if (gamepad.dpad == element) {

            if (gamepad.dpad.up.isPressed) {

                message = @"D-Pad Up";

            }

            if (gamepad.dpad.down.isPressed) {

                message = @"D-Pad Down";

            }

        }    

        // left stick

        if (gamepad.leftThumbstick == element) {

            if (gamepad.leftThumbstick.up.isPressed) {

                message = [NSString stringWithFormat:@"Left Stick %f", gamepad.leftThumbstick.yAxis.value];

            }

            if (gamepad.leftThumbstick.down.isPressed) {

                message = [NSString stringWithFormat:@"Left Stick %f", gamepad.leftThumbstick.yAxis.value];

            }

            if (gamepad.leftThumbstick.left.isPressed) {

                message = [NSString stringWithFormat:@"Left Stick %f", gamepad.leftThumbstick.xAxis.value];

            }

            if (gamepad.leftThumbstick.right.isPressed) {

                message = [NSString stringWithFormat:@"Left Stick %f", gamepad.leftThumbstick.xAxis.value];

            }

        [self displayMessage:message];

    };

}

This looks more complicated than it really is, and I’ve shortened it a bit already. Let’s go through this bit by bit.

The method starts by assuming our controller does support the extended profile. A real life app would have to test this, as not all buttons are available in the micro or standard profile and cannot be read out. The controller’s profile has a valueChangedHandler block in which we write a giant if/then tree. We do this because the block is called no matter what input has changed on our controller, so we need to figure out which button the user has pressed. As a result, we’ll populate a string with an appropriate message and display it onscreen.

Digital elements can be read out using the isPressed property, giving a BOOL as a result. But we can also read analogue values from most elements, such as the thumb sticks, using the xAxis and yAxis value properties. These are floats, giving values from 1 to -1.

iOS deals with deadzones automatically, so there’s no need to worry about values close to zero – a VERY nice touch you’ll appreciate if you’ve ever had to deal with frigging deadzones.

Reacting to the Pause / Menu Button

The Pause button needs his own handler block. Since this is a common element on all controller profiles, this block is called on the actual controller object rather than the profile. Still within the reactToInput method we’ll add something like this:

// we need a weak self here for in-block access

__weak typeof(self) weakSelf = self;

self.mainController.controllerPausedHandler = ^(GCController *controller){

    // check if we're currently paused or not

    // then bring up or remove the paused view controller

    if (weakSelf.currentlyPaused) {

        weakSelf.currentlyPaused = NO;

        [weakSelf dismissViewControllerAnimated:YES completion:nil];

    } else {  

        weakSelf.currentlyPaused = YES;

        [weakSelf presentViewController:weakSelf.pausedViewController animated:YES completion:nil];

    }

};

Again, it’s less complicated than it looks: in essence, we’ll check a BOOL property on our class to see if the game is currently paused or not, and switch its value accordingly to the opposite when the pause button is pressed. If we’re not paused, we’ll bring up another view controller, perhaps with an in-game menu. Likewise, if we’re paused, we’ll dismiss said controller and return to our game.

The only weird bit here is the use of the weakSelf. I’ve explained more about this in my previous article. In a nutshell, accessing a property from inside the block in the current class results in an Xcode warning (self.currentlyPaused). By using a weak reference to our own class inside the block that warning is suppressed. Hence, inside the block, weakSelf works just like self would.

Demo Project on GitHub

The full code for this demo is on GitHub. Feel free to check it out:

Enjoy!





Leave a Reply