Game Dev Diary 4: Extending the Control

Last time we started, adding control elements to the scene. We defined a controller class, that could take gamepad input and manipulate the camera position accordingly.

Today, we want to extend that, by adding keyboard and mouse control. For this simple scene, we want the user to be able, to use all three control schemes simultaneously.

On the Keyboard, the user should be able, to control the latitude and longitude via WASD or the arrow keys, and the center-distance via 1 and 2 or 9 and 0 respectively.

With the mouse, latitude and longitude should be controlled via the movement of the mouse, while using the mouse-wheel to control the center-distance.

A Control Container

In order to be able to receive control input from different sources, we need to restructure the MyGameController class.

It needs to be an interface (or protocol, as it is called in Objective-C) so that the implementation of the control listening mechanism becomes interchangeable.

Beside the old implementation of that protocol for gamepad usage, we will create new implementations for keyboard and mouse input.

To put them all together and to be able, to use different controllers simultaneously, we also need an implementation of the controller protocol, that manages an array of different controller implementations and returns the result of all the controller’s data, when it itself is asked for data.

Extracting the protocol

To refactor existing code in Xcode and extract a protocol from a former concrete class with the least trouble possible, I first renamed the class to what I want the concrete implementation to be called later. In our case, that name should be MyGamePadGameController. To do that in Xcode, you can use the refactoring utility, by right-clicking on the name of your class in the class definition. Using that tool will rename the implementation and header file, as well as the usages of the old name.

After the class has been renamed, we can create a new protocol and fill it with the functions, we need to control the scene.

Note, that a protocol does not have an implementation file, since it only defines methods, other concrete classes need to conform to but doesn’t provide any implementations itself.

@protocol MyGameController

-(void) setupInput;

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

@end

And implement it’s usage at the newly renamed MyGamePadGameController class. Since we basically wrote the protocol from the methods and properties, defined in the old concrete MyGameController class, the MyGamePadGameController classes header file will get pretty empty with that.

#import "MyGameController.h"

@interface MyGamePadGameController : NSObject<MyGameController>

@end

Completing this little refactoring, we need to rewrite the MyOpenGLView, so that the concrete gamepad implementation of the controller is created in the initialization method, but the generic protocol is used throughout.

To do that, redefine the controller property in the header file, to the following:

    NSObject<MyGameController>* controller;

This will order the class to work with only the protocol definition throughout.

Since the initialization in awakeFromNib should stay the same, you may need to correct your imports and are done after that.

The Cocoa Event Architecture

The Cocoa UI framework is build around an Event Engine, that dispatches Events of all kinds to and from your application, thereby allowing your application to communicate with the rest of the system, in a loosely coupled way. This engine, while dispatching all kinds of events, also dispatches keyboard and mouse events, and is therefor our best bet, to implement a control schema for those input methods.

For better understanding of Cocoa’s Event Engine, I highly recommend, you read apple’s Event Handling Guide. It’s not that big and actually quite understandable.

What we need to know, to integrate that framework with our own controller classes, boils down to the fact, that there are Responder classes, which can respond to a certain event. Responder objects are chained in a defined way, one after the other, at execution time. When an event is emitted by the system, the first responder in the chain, either consumes it, or passes it down the chain, until the last responder is passed.

Some events are also disabled by default, to avoid noise and need to be activated by setting a specific flag. One of those is the Mouse-Move event, which we need, to write the Mouse-Controller.

With that in mind, to implement our Keyboard- and Mouse-Controllers, we need to:

  • Let our Controllers extend from some Responder type
  • Implement our Controllers in the responder chain in some way
  • Activate the dispensing of MouseMoved events (only for the mouse controller obviously)

Keyboard Controls

Let’s start with the keyboard controller. To make our scene controllable via keyboard, we need to define a new class, that implements the MyGameController protocol, we defined earlier. I called it MyKeyboardGameController.

Also, since keyboard events get delivered by the system through the Cocoa’s event system, we need to make that class a Responder object, in order to be able to listen to events.

#import "MyGameController.h"

@interface MyKeyboardGameController : NSResponder<MyGameController>

- (void) handleEvent:(NSEvent *)theEvent withFlag:(BOOL) keyDown;

@end

The implementation of MyKeyboardGameController is quite simple actually. All we need to do is, to watch out for the various keyDown and keyUp events, for the keys we are interested in and set or reset the values for latitude and longitude accordingly. It is important, that we follow the whole keyDown – keyup protocol here, since we want the camera to move, as long as a user presses a button and not longer. Therefore don’t forget to set your values back to zero, on a keyUp event.

#import "MyKeyboardGameController.h"

@implementation MyKeyboardGameController

@synthesize centerDistance, latitude, longitude;

- (void)setupInput{}


- (void)keyDown:(NSEvent *)theEvent{
    [self handleEvent:theEvent withFlag:YES];
}

- (void)keyUp:(NSEvent *)theEvent {
    [self handleEvent:theEvent withFlag:NO];    
}

- (void) handleEvent:(NSEvent *)theEvent withFlag:(BOOL) keyDown{
    NSString* theStr = [theEvent charactersIgnoringModifiers];
    if ([theStr length] != 1){
        if (keyDown)
            [super keyDown:theEvent];
        else
            [super keyUp:theEvent];
        return;
    }
    
    unichar theChar = [theStr characterAtIndex:0];
    
    if (NSUpArrowFunctionKey == theChar || 'w' == theChar){
        [self setLongitude:keyDown?1:0];
    } else if (NSDownArrowFunctionKey == theChar || 's' == theChar){
        [self setLongitude:keyDown?-1:0];
    } else if (NSLeftArrowFunctionKey == theChar || 'a' == theChar){
        [self setLatitude:keyDown?-1:0];
    } else if (NSRightArrowFunctionKey == theChar || 'd' == theChar){
        [self setLatitude:keyDown?1:0];
    } else if ('1' == theChar || '9' == theChar){
        [self setCenterDistance:keyDown?-2:0];
    } else if ('2' == theChar || '0' == theChar){
        [self setCenterDistance:keyDown?2:0];
    } else {
        if (keyDown)
            [super keyDown:theEvent];
        else
            [super keyUp:theEvent];
        return;
    }
}

@end

As you can see, the source code is pretty straightforward as well. We synthesize the properties, defined in the protocol with simple default getters and setters. Next we implement the two functions for listening to key events, defined by the NSResponder superclass. Since we want to deal with those events on one single place, we simply hand-on forward the event to our own handleEvent:withFlag: function.

In the handleEvent:withFlag: function, we first filter out only the events, that represent a single key press, since we don’t care for any modified key presses, and hand everything back to the superclass, that is not a single key press.

Next we extract the character, identifying the key in particular and handle it for the characters, that interest us. Everything else also gets send back to the superclass, so that another responder (later in the chain) can maybe handle the key press.

For every target function we want to control via those button presses, we define another if clause, defining which key we want to listen to and setting the value of the target function according to whether the key was down or up.

The values themselves (1 and 2 for center distance on the example) are based on how fast you want your controls to behave. Play around with them, until you found the setting you like.

Make it live

To implement this control schema into our main view, we need to change the controller implementation, set in the view’s awakeFromNib function, to be our newly created MyKeyboardGameController and register that controller with Cocoas main event loop. Otherwise, we won’t get any key presses.

In the awakeFromNib function of your view, exchange the line that sets the controller implementation with the following:

controller = [[MyKeyboardGameController alloc] init];
[self setNextResponder: (MyKeyboardGameController*)controller];

Mouse Controls

The controller class for the mouse starts quite similar to the keyboard controller: We create a new NSResponder class, that implements the MyGameController protocol.

#import "MyGameController.h"

@interface MyMouseGameController : NSResponder<MyGameController>{
    float _centerDistance, _latitude, _longitude;
}

@end

The first difference is in the implementation of the parameters, defined from the MyGameController protocol. Since we do not have a reliable equivalent to the keyUp event when dealing with mouse events, we need to reset the values every time, they get read. Therefore we cannot us the @synthesize shortcut and need to implement those getters by hand. That is also the reason, why we defined our own variables for those values in the header earlier.

#import "MyMouseGameController.h"

@implementation MyMouseGameController

- (void)setupInput{}

- (float)latitude{
    float x = _latitude;
    _latitude = 0;
    return x;
}

- (void)setLatitude:(float)latitude{
    _latitude = latitude;
}

- (float)longitude{
    float y = _longitude;
    _longitude = 0;
    return y;
}

- (void)setLongitude:(float)longitude{
    _longitude = longitude;
}
@end

Since we will use the scroll wheel for controlling the center distance and since that would result in very sudden stops, when we simply reset the value to zero as with the other values; we will implement a soft roll out for that value, so that it will reset to zero over the course of several read operations.

- (float)centerDistance {
    float cd = _centerDistance;
    if (_centerDistance < .2 && _centerDistance > -.2)
        _centerDistance = 0;
    else
        _centerDistance = _centerDistance/2;
    return cd;
}

- (void)setCenterDistance:(float)centerDistance{
    _centerDistance = centerDistance;
}

Now, that we defined the basic behavior for our controller values, we can start handling the mouse events, we are interested in. To remember: We want to control latitude and longitude with the X and Y axis of the mouse movement, while controlling the center distance via the scroll wheel.

Cocoa provides two event types for our use here: a mouseMoved event, and a scrollWheel event.

To react to the mouseMoved event, we need to implement the event listener function with the same name. The event will present us a value for the delta, the mouse moved since the last event in X and Y. The only thing we have to do here is to set the latitude and longitude values appropriately.

- (void)mouseMoved:(NSEvent *)theEvent{
    //latitude value is divided by 2, for smother movement
    [self setLatitude: ([theEvent deltaX]/2)];
    [self setLongitude: [theEvent deltaY]];
}

The scroll wheel will present us with the same deltaX and deltaY values, for the horizontal and vertical scroll wheel accordingly. So basically we would only need to update the centerDistance with the deltaY value of the event, like for the mouseMoved event. But since we want to smoothen the movement a bit, we do some extra calculation before setting the centerDistance value.

- (void)scrollWheel:(NSEvent *)theEvent{
    float dY = [theEvent deltaY];
    if (dY < .2 && dY > -.2)
        return;
    if (dY > 5)
        dY = 5;
    else if (dY < -5)
        dY = -5;
    [self setCenterDistance:[self centerDistance]+(dY/2)];
}

As you can see, we defined a dead zone. Every movement of the scroll wheel, smaller than .2 will be ignored. We also defined a maximum delta value, since we will set every value back to 5 (or -5 respectively) that is higher.

At last, we don’t set the centerDistance as a static value, but transform the value with the given value. By doing that, together with the reader method that gradually let’s the value decline to zero, we can define a pretty smooth movement for our wheel. If you want, play around with the default setting of the value (as we do with latitude and longitude) and see the difference.

Now we have a functioning mouse controller. As with the keyboard controller, we can activate it, by setting it in the awakeFromNib function of our view instead of the keyboard controller.

controller = [[MyMouseGameController alloc] init];
[self setNextResponder: (MyMouseGameController*)controller];

Creating a Controller List

As stated at the beginning, we want the player, to be able to use all the different control schemas in conjunction with each other.

In order to do that, we would have to listen to the inputs from all the three controller implementations in our game view class. We already abstracted what we need to know from the game controller in the view into a protocol. So following that, it would be great, if we would still only care about one controller in the view and have the game controller classes care about where the input comes from.

To follow that idea, we need to hide the calls from the different solid game controller implementations inside a container game controller, that mediates between it’s children. Hence we create the MyListGameController.

The MyListGameController definition extends the MyGameController protocol by a single function to add a new controller and contains an array to keep a hold of all the added controllers.

#import "MyGameController.h"

@interface MyListGameController : NSResponder<MyGameController>{
    NSMutableArray* controllers;
}
- (void) addController:(NSObject<MyGameController>*) controller;
@end

The implementation needs to do the standard implementation stuff, as well, as be able to add a new controller to the list. When that happens, we also want to append that controller to Cocoa’s nextResponder chain, to guarantee, that all the child game controller can receive events if needed.

#import "MyListGameController.h"

@implementation MyListGameController

@dynamic centerDistance, latitude, longitude;

- (id)init {
    self = [super init];
    if (self) {
        controllers = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void) setupInput{}

- (void) addController:(NSObject<MyGameController> *)controller {
    if (![controllers containsObject:controller]){
        [controllers addObject:controller];
    }
    NSResponder* lastResponder = self;
    for (NSObject<MyGameController>*  c in controllers) {
        if ([c isKindOfClass:[NSResponder class]]){
            NSResponder* r = (NSResponder*)c;
            [lastResponder setNextResponder:r];
            lastResponder = r;
        }
    }
}

@end

Whenever a value is read from the list controller, it will calculate the median between the values of all it’s children for that given value. Doing so, every single controller can manipulate the value, but no two controller can add up and create a value that is bigger than both controller could have achieved on their own.

- (float)centerDistance{
    int s = 0;
    float total = 0;
    for (NSObject<MyGameController>* c in controllers) {
        s++;
        total += [c centerDistance];
    }
    return 0 == s?0:total/s;
}

- (float)latitude{
    int s = 0;
    float total = 0;
    for (NSObject<MyGameController>* c in controllers) {
        s++;
        total += [c latitude];
    }
    return 0 == s?0:total/s;
}

- (float)longitude{
    int s = 0;
    float total = 0;
    for (NSObject<MyGameController>* c in controllers) {
        s++;
        total += [c longitude];
    }
    return 0 == s?0:total/s;
}

Putting it all together

To implement the list controller into our main view, we need to do a little bit more work, that we did before, to enable a single controller implementation. In the awakeFromNib function of our main view, replace the two lines, you used to initialize the keyboard or mouse controller with the code below and adjust the imports accordingly.

    controller = [[MyListGameController alloc] init];
    [self setNextResponder: (MyListGameController*)controller];
    
    MyGamePadGameController* gamepad = [[MyGamePadGameController alloc] init];
    [gamepad setupInput];
    MyKeyboardGameController* keyb = [[MyKeyboardGameController alloc] init];
    [keyb setupInput];
    MyMouseGameController* mouse = [[MyMouseGameController alloc] init];
    [mouse setupInput];
    
    [(MyListGameController*)controller addController: gamepad];
    [(MyListGameController*)controller addController: keyb];
    [(MyListGameController*)controller addController: mouse];

As you can see, we need to initialize the view’s main controller as our new list controller type, as we did with the others before and set it as a nextResponder, to not break the event chain.

Since we also need a place to define, which controllers should be operating as children of the list controller, we need to initialize them here as well and add them to the list.

The rest is handled by the list controller itself.

Conclusion

You should by now have a pretty decent demo, to impress your cat (mine wasn’t that impressed, really…) in which you can control the scene by different control schemas.

We have now had a look at most of the very core utilities for a programmer to make games with.

Next time we will look at another more detailed topic and figure out, how 2D drawing works, by adding a simple game menu.

Until then, you can again download the state of this tutorial as a zipped Xcode project from here: GameDevDiary4.zip or the working Mac application from here: GameDevDiary4.app.

Have fun and leave a comment, if you like.

So Long…

About these ads

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