Game Developers Diary 3: Getting in Control

In the last chapter we created a pretty cool scene with movement and sound.

Today we will take a look at the most important UI interface for games ever: Controls. Until now, our scene was pretty static, with the camera flying around wildly on a kind of weird, prescripted path.

Let’s implement some controls, to steer the camera on a spherical path around the main cube. Imagine a sphere around the cube and the camera as a kind of bubble on the outside of that sphere; always looking into the center. The user can choose to fling the camera-bubble along the latitude and longitude of this imaginary sphere, as well as shrink or grow the sphere itself (make the radius closer or wider).

We want to end up with a program, where the user is able to control the scene via a Gamepad, the keyboard or the mouse as he chooses.

The Gamepad

Let’s start with the gamepad. On one hand, because it is presumably the hardest to integrate, since we need to dig down to direct USB/HID control, on the other hand, because I love those things.

Since we wouldn’t want to clutter our main view with all the heavy lifting of the HID controls, we create a new class, called the GameController. This class will expose values for any scene modifications we want to make and enable us to encapsulate what actually controls the game.

Here is the new header file:

@interface MyGameController : NSObject {}

-(void) setupInput;

@property float centerDistance;
@property float latitude;
@property float longitude;

@end

The class exposes access to three properties, we can later use to control our camera and a method to setup whatever input method we choose.

The bare implementation file should therefore look something like this:

#import <Foundation/Foundation.h>
#import <IOKit/hid/IOHIDLib.h>

#import "MyGameController.h"

@implementation MyGameController

@synthesize centerDistance, latitude, longitude;

-(void) setupInput {
}

Arguably that doesn’t do much, so let’s get straight to the meet of it:

The Accessor Functions

The first thing to notice here, is that the HIDManager api works via classic C callback functions. For that reason, we are going to need some in our implementation. Let’s add them just after the synthesizing commands.

void gamepadWasAdded(void* inContext, IOReturn inResult,
                     void* inSender, IOHIDDeviceRef device);
void gamepadWasRemoved(void* inContext, IOReturn inResult, 
                       void* inSender, IOHIDDeviceRef device);
void gamepadAction(void* inContext, IOReturn inResult, 
                   void* inSender, IOHIDValueRef value);

void gamepadWasAdded(void* inContext, IOReturn inResult,
                     void* inSender, IOHIDDeviceRef device) {
    NSLog(@"Gamepad was plugged in: %@", device);
}

void gamepadWasRemoved(void* inContext, IOReturn inResult, 
                       void* inSender, IOHIDDeviceRef device) {
    NSLog(@"Gamepad was unplugged");
}

void gamepadAction(void* inContext, IOReturn inResult, 
                   void* inSender, IOHIDValueRef value) {
    NSLog(@"Gamepad talked!");
}

Firstly: I am writing some function prototypes here, although in that context I don’t actually need to. The reason for that is simply to keep Xcode happy, because otherwise it would flicker warning lights all over me.

Basically, the api offers us the obvious three hook-points: When a USB device was added, removed or did send a signal. If we examine the arguments, we see an inContext object, which can be any object, specified when the method is registered. For the sake of object oriented programming, that will later be the self object of the GameController class.

The most import end parameter we get here however, is the value object of type IOHIDValueRef, that we get handed in the gamepadAction function. This holds a reference to the exact value, which’s change resulted in the call of this method.

Initialization

The whole HID api is organized around the IOHIDManager. This token-like C struct is used to refer to a specific communication session, between your code and the usb devices. We can create a reference to it, by calling:

IOHIDManagerRef hidManager = IOHIDManagerCreate(kCFAllocatorDefault, 
                                    kIOHIDOptionsTypeNone);

Throughout the HIDManager, USB devices can be found via matching filter definitions. These definitions refer to a so-called Usage Page key and a Usage key, the combination of which will define a type of device sufficient enough, to get what we need. Most Gamepads register themselves to the system as joysticks. Therefore, we need to be looking for devices on the GenericDesktop usage page, with the usage Joystick.

NSMutableDictionary* criterion = [[NSMutableDictionary alloc] init];
[criterion setObject: [NSNumber numberWithInt: kHIDPage_GenericDesktop] 
              forKey: (NSString*)CFSTR(kIOHIDDeviceUsagePageKey)];
[criterion setObject: [NSNumber numberWithInt: kHIDUsage_GD_Joystick] 
              forKey: (NSString*)CFSTR(kIOHIDDeviceUsageKey)];

The HID api provides filter functions, to perform this device search via a specifically filled NSDictionary object, where all the search criteria are and linked together.

IOHIDManagerSetDeviceMatching(hidManager, 
                                  (__bridge CFDictionaryRef)criterion);

To interact with hardware events, that fall into the search criteria for our HIDManager, we need to register callback functions at the manager, which it will call when the corresponding event occurs. To be able to access the full object instance of our Objective C object, while inside the C functions, we will also pass ourselves as context pointer to the functions.

IOHIDManagerRegisterDeviceMatchingCallback(hidManager, gamepadWasAdded, 
                                           (__bridge void*)self);
IOHIDManagerRegisterDeviceRemovalCallback( hidManager, gamepadWasRemoved, 
                                           (__bridge void*)self);
IOHIDManagerRegisterInputValueCallback(    hidManager, gamepadAction, 
                                           (__bridge void*)self);

The HIDManager functions as a basic event dispatcher, which turns low-level hardware events into higher level events, that can be interpreted by your code. In order to do this in a Cocoa environment, it needs to be registered with an event loop, so that it can get called by the system on actual hardware events.

IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetCurrent(), 
                                kCFRunLoopDefaultMode);

Finally, the HIDManager does not start dispatching events to the callbacks, before it has been opened for business.

IOHIDManagerOpen(hidManager, kIOHIDOptionsTypeNone);

That is all the initialization, needed for the HIDManager. Here’s how the complete initialization function should look by now:

-(void) setupInput {
    //get a HID manager reference
    IOHIDManagerRef hidManager = IOHIDManagerCreate(kCFAllocatorDefault, 
                                    kIOHIDOptionsTypeNone);
    
    //define the device to search for, via usage page and usage key
    NSMutableDictionary* criterion = [[NSMutableDictionary alloc] init];
    [criterion setObject: [NSNumber numberWithInt: kHIDPage_GenericDesktop] 
                  forKey: (NSString*)CFSTR(kIOHIDDeviceUsagePageKey)];
    [criterion setObject: [NSNumber numberWithInt: kHIDUsage_GD_Joystick] 
                  forKey: (NSString*)CFSTR(kIOHIDDeviceUsageKey)];
    
    //search for the device
    IOHIDManagerSetDeviceMatching(hidManager, 
                                  (__bridge CFDictionaryRef)criterion);
    
    //register our callback functions
    IOHIDManagerRegisterDeviceMatchingCallback(hidManager, gamepadWasAdded, 
                                               (__bridge void*)self);
    IOHIDManagerRegisterDeviceRemovalCallback(hidManager, gamepadWasRemoved, 
                                              (__bridge void*)self);
    IOHIDManagerRegisterInputValueCallback(hidManager, gamepadAction, 
                                           (__bridge void*)self);
    
    //scedule our HIDManager with the current run loop, so that we
    //are able to recieve events from the hardware.
    IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetCurrent(), 
                                    kCFRunLoopDefaultMode);
    
    //open the HID manager, so that it can start routing events
    //to our callbacks.
    IOHIDManagerOpen(hidManager, kIOHIDOptionsTypeNone);
    
}

This should now get our Gamepad to talk to us, through our callback functions. To see that in action, let’s include our MyGameController class in out main view.

The Wiring

In our MyOpenGLView object, we need some new instance variables, to control the camera. We need an instance of our controller class and float variables to hold the latitude (cameraLat), longitude (cameraLon) and distance to the center (cameraDist) of our camera.

@interface MyOpenGLView : NSOpenGLView {
    long lastTicks;
    ALuint satelitteALSourceId;
    MyGameController* controller;
    float cameraLat, cameraLon, cameraDist;
}

We then need to initialize those variables in our awakeFromNib function.

- (void)awakeFromNib{
    lastTicks = clock();
    self.mainCube = [[MyCube alloc] init];
    self.flyingCube = [[MyCube alloc] init];
    [mainCube setScale:1];
    [mainCube setPositionX:0 Y:0 Z:0];
    [flyingCube setScale:.2];
    [flyingCube setPositionX:5 Y:0 Z:0];
    cameraLat = 10;
    cameraLon = 0;
    cameraDist = 10;
    controller = [[MyGameController alloc] init];
    [controller setupInput];
    NSLog(@"Gamepad Controller initialized.");
    
    [super awakeFromNib];
}

When we start the application now, we should see the initialization for the gamepad already working. It doesn’t do anything at this point, but we can see from all the “Gamepad talked” log messages we receive, when we press buttons on the gamepad, that we actually receive notifications from the gamepad.

To calculate any camera movements in response to controller input, we need to get into our drawRect function and rework the way we position the camera. There are three steps involved in this process:

  1. We need to update the instance variables cameraLat, cameraLon and cameraDist according to input from the controller
  2. We need to calculate our real camera position in x, y and z from those variables
  3. We need to change the calls to gluLookAt and alListener3f according to those new position values

To recalculate the logical camera position values (latitude, longitude and distance) we assume, that our controller will set values between 0.0f and 1.0f according to it’s input command. We will use those as delta values, to update our absolute positions. The code below takes care of that, while also taking delta_t into account, to tie the movement to absolute time. This code should be written after the timer initialization.

    cameraLat = cameraLat+([controller latitude] * delta_t*.05);
    while (cameraLat >= 2*pi)
        cameraLat -= 2*pi;
    while (cameraLat <= -2*pi)
        cameraLat += 2*pi;
    cameraLon = cameraLon+([controller longitude] * delta_t*.005);
    while (cameraLon >= 2*pi)
        cameraLon -= 2*pi;
    while (cameraLon <= -2*pi)
        cameraLon += 2*pi;
    cameraDist = cameraDist+([controller centerDistance] * delta_t*.05);

At first glance, those while loops may seem very wired. The are here, to prevent the values from getting to big.

To describe a full circle, using sinus and cosinus functions, it is normally irrelevant, how big the base value is, since the sine wave will repeat itself over and over in steps of 2*Π. But since we can risk overflowing some buffers here with a user holding the control in one direction constantly (and thus making the value for latitude or longitude grow bigger and bigger), we will shrink those values down, until we are in the range of less then 2Π because that is really everything we care about.

To now calculate the real camera position from those three values, we need our old friends sinus and cosines again. Our camera should basically move around the width of a circle in the X-Z plane, according to the value in cameraLat and across a circle in the Y-Z plane according to the value in cameraLon. The radius of the circle will be calculated from the cameraDist value.

    double camX = sinf(cameraLat)*cameraDist;
    double camY = sinf(cameraLon)*cameraDist;
    double camZ = cosf(cameraLat)*cosf(cameraLon)*cameraDist;

The one thing left to do here now, is to replace the old code, to calculate the look-at and the OpenAL listener position with our newly calculated values:

    gluLookAt(camX, 
              camY,
              camZ, 
              0, 0, 0, 
              0, 1, 0);
    alListener3f(AL_POSITION, 
                 camX, 
                 camY,  
                 camZ);

For now, that only means, that our camera will stop following the wired path it followed before and just stand still at an initial position.

To get some movement back into the scene, we do now need to start listening to what our gamepad tells us.

Reading from the Gamepad

Anytime a button is pressed, or an axis is moved on the gamepad, the HIDManager calls the callback function (gamepadAction) we provided earlier, while initializing the HIDManager.

The function is called with a value token, that refers to the new value of the changed control. Our job now is to simply read this new value and set it for one of our controller properties as a float value between -1.0, 0.0 and 1.0.

But there lies a problem: How can we tell, which control element on the gamepad this value belongs to?

We can get a HIDElement reference from the value object, by calling IOHIDValueGetElement(value). The HID api defines two metadata values for a HIDElement which uniquely identify a certain element. Element cookies and a combination of usagePage and usageKey.

I first tried to go with the element cookie, and mapped the value based on the integer value I got back from IOHIDElementGetCookie(element). This worked, as long as I used only one specific gamepad. As soon as I plugged in a different gamepad from a different vendor, the cookie values were changed completely. Therefor, using cookies might only have succeeded, if I had stored a custom mapping for each supported gamepad somewhere and did somehow transform between the custom cookie values to standard cookies.

The good news is, that there is a second, more standardized method, of identifying an element. The combination of usage-page and usage, you get from calling IOHIDElementGetUsagePage(element) and IOHIDElementGetUsage(element) is also unique for each element and defined as standard by the USB consortium. It can be looked up here: HID Usage Tables.

Using those values, reading from the gamepad will work for any standard control elements. Since we want the left stick, to control the longitude and latitude, and the Y-Axis of the right stick, to control the distance, we end up with needing the usages 48, 49 and 53 from the usage page 1.

Now we can write our code. Replace the empty gamepadAction function with the code below:

void gamepadAction(void* inContext, IOReturn inResult, 
                   void* inSender, IOHIDValueRef value) {
    
    IOHIDElementRef element = IOHIDValueGetElement(value);
    
    int usagePage = IOHIDElementGetUsagePage(element);
    int usage = IOHIDElementGetUsage(element);
    if (1 != usagePage)
        return;
    
    long elementValue = IOHIDValueGetIntegerValue(value);
    
    if (48 == usage || 49 == usage || 53 == usage){
        MyGameController* obj = (__bridge MyGameController*) inContext;
        float axisScale = 128;
        
        float axisvalue = ((float)(elementValue-axisScale)/axisScale);
        if (elementValue <= axisScale +1 &&
            elementValue >= axisScale -1)
            axisvalue = 0.0;
        if (48 == usage)
            [obj setLatitude:axisvalue];
        else if (53 == usage)
            [obj setCenterDistance:axisvalue];
        else if (49 == usage)
            [obj setLongitude:axisvalue];
    }
    
}

Conclusion

You have seen, how to read from a USB gamepad, use that values, to manipulate a scene and do that while separating domain specific knowledge with a Controller class.

As always, you can downlad the Xcode project from here: GameDevDiary3.zip and the executable application from here: GameDevDiary3.app.

Next time we will expand on that Controller class, to let the user choose between Gamepad, Keyboard and Mouse controls.

So long…

About these ads

2 thoughts on “Game Developers Diary 3: Getting in Control

    • Well…

      I wouldn’t count on ‘Soon’. This series is pretty old now.

      But I have something new in the making, and I will very likely return to this blog, with a new series, once that thing starts to become more of a thing. ;)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s