1

At first, Keyboard event handling for games in Cocoa seemed easy: I added -acceptsFirstResponder and -becomeFirstResponder overrides to my custom game map view, then overrode -moveUp:, -moveDown:, -moveLeft: and -moveRight: to handle the arrow keys.

But there's one big difference to most other games: It only ever accepts one keypress at a time. So if I'm holding down the up arrow key to have my character run forward, then quickly press the right arrow key to sidestep and obstacle, my character will stop in its tracks, as if I had released the up arrow key.

This makes sense for text entry, where one might accidentally still be holding down one character while another finger presses the next, but for a game this is annoying.

How can I chord arbitrary key combinations together?

uliwitness
  • 8,532
  • 36
  • 58

1 Answers1

1

The solution is to keep track of which key is down yourself. Override -keyDown and -keyUp to keep track of which keys are being held down. I’m using a C++ unordered_set for that, but an Objective-C NSIndexSet would work just as well:

@interface ICGMapView : NSView
{
    std::unordered_set  pressedKeys;
}

@end

and in the implementation:

-(void) keyDown:(NSEvent *)theEvent
{
    NSString    *   pressedKeyString = theEvent.charactersIgnoringModifiers;
    unichar         pressedKey = (pressedKeyString.length > 0) ? [pressedKeyString characterAtIndex: 0] : 0;
    if( pressedKey )
        pressedKeys.insert( pressedKey );
}


-(void) keyUp:(NSEvent *)theEvent
{
    NSString    *   pressedKeyString = theEvent.charactersIgnoringModifiers;
    unichar         pressedKey = (pressedKeyString.length > 0) ? [pressedKeyString characterAtIndex: 0] : 0;
    if( pressedKey )
    {
        auto foundKey = pressedKeys.find( pressedKey );
        if( foundKey != pressedKeys.end() )
            pressedKeys.erase(foundKey);
    }
}

Then add an NSTimer to your class that periodically checks whether there are any keys pressed, and if they are, reacts to them:

-(void) dispatchPressedKeys: (NSTimer*)sender
{
    BOOL    shiftKeyDown = pressedKeys.find(ICGShiftFunctionKey) != pressedKeys.end();
    for( unichar pressedKey : pressedKeys )
    {
        switch( pressedKey )
        {
            case 'w':
                [self moveUp: self fast: shiftKeyDown];
                break;
            ...
        }
    }
}

Since your timer is polling at an interval here, and you can’t make that interval too fast because it’s the rate at which key repeats will be sent, it is theoretically possible that you would lose keypresses whose duration is shorter than your timer interval. To avoid that, you could store a struct in an array instead of just the keypress in a set. This struct would remember when the key was originally pressed down, and when the last key event was sent out.

That way, when the user begins holding down a key, you’d immediately trigger processing of this key once, and make note of when that happened. From then on, your -dispatchPressedKeys: method would check whether it’s been long enough since the last time it processed that particular key, and would send key repeats for each key that is due. As a bonus, when a key is released, you could also notify yourself of that.

uliwitness
  • 8,532
  • 36
  • 58