Handling keypresses in Cocoa games
At first blush, Keyboard event handling for games in Cocoa seems easy: You add -acceptsFirstResponder and -becomeFirstResponder overrides to your custom game map view, then override -moveUp:, -moveDown:, -moveLeft: and -moveRight: to handle the arrow keys.
However, if you play a game like that, you'll notice one big difference to most other games: It only ever accepts one keypress at a time. So if you're holding down the up arrow key to have your character run forward, then quickly press the right arrow key to sidestep and obstacle, your character will stop in its tracks, as if you had released the up arrow key.
This makes sense for text entry, where you might accidentally still be holding down one key while another finger presses the next, but for a game this is annoying. You want to be able to chord arbitrary key combinations together.
I found a clever solution for game keyboard handling on the CocoaDev Wiki, but it's a bit old and incomplete, so I thought I'd provide an updated technique:
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<unichar> 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); } }
Of course, you'll also want to react to modifier keys, and like most games, you will want to treat them not as modifiers in a shortcut, but as regular keys, so people can press Command to fire, or so. That's basically the same, just that you override -flagsChanged: and that there are no standard character constants for the modifier keys. So lets just define our own:
// We need key codes under which to save the modifiers in our "keys pressed" // table. We must pick characters that are unlikely to be on any real keyboard. // So we pick the Unicode glyphs that correspond to the symbols on these keys. enum { ICGShiftFunctionKey = 0x21E7, // -> NSShiftKeyMask ICGAlphaShiftFunctionKey = 0x21EA, // -> NSAlphaShiftKeyMask ICGAlternateFunctionKey = 0x2325, // -> NSAlternateKeyMask ICGControlFunctionKey = 0x2303, // -> NSControlKeyMask ICGCommandFunctionKey = 0x2318 // -> NSCommandKeyMask }; -(void) flagsChanged: (NSEvent *)theEvent { if( theEvent.modifierFlags & NSShiftKeyMask ) { pressedKeys.insert( ICGShiftFunctionKey ); } else { auto foundKey = pressedKeys.find( ICGShiftFunctionKey ); if( foundKey != pressedKeys.end() ) pressedKeys.erase(foundKey); } if( theEvent.modifierFlags & NSAlphaShiftKeyMask ) { pressedKeys.insert( ICGAlphaShiftFunctionKey ); } else { auto foundKey = pressedKeys.find( ICGAlphaShiftFunctionKey ); if( foundKey != pressedKeys.end() ) pressedKeys.erase(foundKey); } if( theEvent.modifierFlags & NSControlKeyMask ) { pressedKeys.insert( ICGControlFunctionKey ); } else { auto foundKey = pressedKeys.find( ICGControlFunctionKey ); if( foundKey != pressedKeys.end() ) pressedKeys.erase(foundKey); } if( theEvent.modifierFlags & NSCommandKeyMask ) { pressedKeys.insert( ICGCommandFunctionKey ); } else { auto foundKey = pressedKeys.find( ICGCommandFunctionKey ); if( foundKey != pressedKeys.end() ) pressedKeys.erase(foundKey); } if( theEvent.modifierFlags & NSAlternateKeyMask ) { pressedKeys.insert( ICGAlternateFunctionKey ); } else { auto foundKey = pressedKeys.find( ICGAlternateFunctionKey ); if( foundKey != pressedKeys.end() ) pressedKeys.erase(foundKey); } }
An alternative would be to just enlarge the numeric type used to store keys in your unordered_set. Instead of two-byte unichar values, you'd pick uint32_t, and then define the constants as values that are out of range for an actual unichar, like 0xffff1234. If you're using NSIndexSet, you're lucky, it uses NSInteger, which is already larger.
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.
You could even create key event objects of some sort to hand into your engine.