Adding a keyboard input handler
Originally, I live-streamed this session, but - once again - I messed up, and accidentally only recorded the first 10 minutes. So instead of posting a link to the video, I'll just describe what I arrived at.
After wrapping up gamepad input handling, I looked at adding keyboard input. So that I can test the game on a Mac, even if the gamepad is being used on the Apple TV by the kids playing some game.
The overall structure
SKScene gives us two methods to override for handling key input:
func keyDown(with event: NSEvent)
func keyUp(with event: NSEvent)
I added a KeyboardInputPublisher
class (analogous to ControllerInputPublisher
and:
- in keyDown and keyUp, register the event
- in the
KeyboardInputPublisher
I parse the events and map them to aGameInputEvent
stream
Then I created a BaseScene
class from which all scenes inherit, and this scene is responsible for combining the input event streams from the keyboard, and the controller, and calling a method for handling the normalized event. Every scene that wants to handle input can override the following function:
func handleInputEvent(_ event: GameInputEvent, from source: GameInputSource)
It looks roughly like this:
class BaseScene : SKScene
{
let controllerPublisher = ControllerPublisher()
let controllerInputPublisher: ControllerInputPublisher
let keyboardInputPublisher = KeyboardInputPublisher()
var cancellables = Set<AnyCancellable>()
required init?(coder aDecoder: NSCoder) {
self.controllerInputPublisher = ControllerInputPublisher(controllerPublisher: controllerPublisher)
super.init(coder: aDecoder)
keyboardInputPublisher.gameInputEventSubject.merge(with: controllerInputPublisher.gameInputEventSubject).sink { pair in
self.handleInputEvent(pair.0, from: pair.1)
}.store(in: &self.cancellables)
}
func handleInputEvent(_ event: GameInputEvent, from source: GameInputSource)
{
}
}
#if os(OSX)
extension BaseScene
{
override func keyDown(with event: NSEvent) {
self.keyboardInputPublisher.sendRawInput(event: event, down: true)
}
override func keyUp(with event: NSEvent) {
self.keyboardInputPublisher.sendRawInput(event: event, down: false)
}
}
#endif
Mapping NSEvent to GameInputEvent
In the KeyboardInputPublisher
I map the events approximately like this:
#if os(OSX)
public func sendRawInput(event: NSEvent, down: Bool )
{
if event.modifierFlags.contains(NSEvent.ModifierFlags.numericPad){
if let theArrow = event.charactersIgnoringModifiers, let keyChar = theArrow.unicodeScalars.first?.value{
switch Int(keyChar){
case NSUpArrowFunctionKey:
self.gameInputEventSubject.send((.up, .keyboard))
break
// ... insert more cases here
default:
break
}
}
} else {
if let characters = event.characters{
for character in characters {
guard let asciiValue8 = character.asciiValue else { continue }
let converted = Int(UInt(asciiValue8))
switch (converted) {
case NSDeleteCharacter:
self.rawNonRepeatInputSubject.send((.back, down, .keyboard))
break
case NSBackspaceCharacter:
self.rawNonRepeatInputSubject.send((.back, down, .keyboard))
break
case NSCarriageReturnCharacter:
self.rawNonRepeatInputSubject.send((.confirm, down, .keyboard))
break
case NSEnterCharacter:
self.rawNonRepeatInputSubject.send((.confirm, down, .keyboard))
break
default:
break
}
}
}
}
}
#endif
There are two points to note here:
- non-arrow events are sent to
rawNonRepeatInputSubject
but arrow events are sent togameInputEventSubject
- non-arrow events can be triggered by different keys
The raw input subject
The ultimate output goes to gameInputEventSubject
. However, the rawNonRepeatInputSubject
acts as a simple "middleware" for events that should not be repeated.
When handling the controller input, we had to add repeat-rate behaviour manually. With the keyboard, it is the opposite. You get repeated events by default, and for consistency with controllers, we have to remove this behaviour for some events:
rawNonRepeatInputSubject.removeDuplicates {
$0.0 == $1.0 && $0.1 == $1.1
}.filter {
$0.1
}.map { ($0.0, $0.2) }.subscribe(gameInputEventSubject).store(in: &self.cancellables)
In other words - remove duplicates, and keep only "key pressed" events (and ignore "key released").
Different inputs for the same output
The first attempt mapped NSBackspaceCharacter
to .back
and NSReturnCharacter
to .confirm
.
This does not work.
The keyboard that came with my iMac sends an NSDeleteCharacter
instead of NSBackspaceCharacter
when I press backspace. I guess I've been mislabelling the backspace character for my whole (Apple using) life?
And same for the Return
character. It actually is registered as the carriage return
character.
I assume different layouts (or keyboards) might trigger these other inputs.
Summary and future considerations
At this point, every scene can act on "normalized" input. E.g. .confirm
instead of button A
or carriage-return character
etc. And if we need to update these mappings, we have a central place to do these updates.
In the future, the input will need to be revisited when adding touch and mouse input:
- for touches, the place that you touch is important. You don't navigate a menu by swiping up or down on an iPad. You just click directly on the relevant option. I imagine,
GameInputEvent.confirm
might be extended with associated coordinates:.confirm(position: CGPoint)
- for the mouse, there is a similar concern as with touches. You need to propagate the position.
But for now, input handling is good enough.
Next on the docket: working on the gameplay. I'll start with rotating game pieces, then spawning more of them, and then experimenting with it until it feels right.
Addendum (edit): after implementing this, I quickly realized that it is also important to avoid retain cycles in closures.