Abstracting key and game controller inputs in Adobe AIR

In general, dealing with keyboard input in ActionScript 3 is fairly easy: you simply listen for KeyboardEvents, and react accordingly. This works very well when the user is interacting with a graphical user interface – you simply turn your event listeners on and off, and wait for something to happen.

When developing games, however, this approach doesn’t work as well. Dealing with events forces the developer to add a layer of management on top of it: since you can only know when a key gets pressed down and then released, you can’t check whether a key is currently pressed inside your game loop. You need to write some code of your own to keep track of its state. Something like this:

// Properties for current input state
private var isLeftPressed:Boolean;
private var isRightPressed:Boolean;

private function gameStart():void {
	// Create events
	stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
	stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
}

private function onKeyDown(KeyboardEvent):void {
	if (e.keyCode == Keyboard.LEFT) isLeftPressed = true;
	if (e.keyCode == Keyboard.RIGHT) isRightPressed = true;
}

private function onKeyUp(KeyboardEvent):void {
	if (e.keyCode == Keyboard.LEFT) isLeftPressed = false;
	if (e.keyCode == Keyboard.RIGHT) isRightPressed = false;
}

private function gameLoop():void {
	// Check for input state
	if (isLeftpressed) {
		// Move the player to the left...
	} else if (isRightPressed) {
		// Move the player to the right...
	}

	// Rest of game loop
}

A secondary problem, and one that is admittedly less important for most people, is that you have to hard-code which keys you’re listening to (via their keyCodes). This is the way it is supposed to be, but some times, especially in games, you need to be able to configure which keys are being used for what, having keys bound to a more abstract identifier.

While working on a previous project at Firstborn I’ve ran into a situation where I really needed keyboard input to be highly configurable. This particular application I was writing would run under a few different configurations of machines put together especially for it, so I wanted to make porting between each form factor as easy as simply having a separate configuration XML. The result was what I called a KeyBinder class that allowed you to create actions (simple String ids) and bind them to different keyCodes. The KeyBinder class was configurable through a XML file, and at any time, you checked for action states (whether they were activated or not) or used the basic events for key press and release.

It just so happens that this is exactly the kind of thing you normally need in a game with configurable bindings. And, in some sort of coincidence, Adobe announced the addition of a new GameInput API to AIR 3.7 right after I was done with the KeyBinder class. Since I was messing around with Adobe AIR and OUYA game development, I decided to combine the two things, and come up with a key binding class that also supported game input.

What I came up with is a KeyActionBinder class. It allows you to control application/game key input (from the keyboard, or a game controller) by abstracting the input into actions.

The problem

Take, for example, the piece of code which you could use in your game loop. Suppose you wrote a class to abstract all keyboard input, which is what most sane people would do. In that case, you may have additional keys for some input, and the code could look something like this:

private function gameLoop():void {
	// Check for input state
	if (myKeyboard.isLeftpressed || myKeyboard.isKeyAPressed) {
		// Move the player to the left...
	} else if (myKeyboard.isRightPressed || myKeyboard.isKeyDPressed) {
		// Move the player to the right...
	}

	// Rest of game loop
}

Changing the binding of any action – by adding, removing, or changing a key – would mean going back to your game loop and changing the condition accordingly. Again, in most cases this is not a problem, but it is a hard-coded solution (and it makes user-configurable bindings, like the ones you normally see on a PC game, impossible).

This problem gets increasingly more complicated when you throw the new game controller input into the mix. While the GameInput class is a great for what it does, it requires a good amount of maintenance for it to be useful. Like the keyboard, it requires listening to events; however, here you need them not only for the actual input that comes from the controller, but for the addition and removal of controller devices themselves (and every new device need input events added to it). Add to this is a vast amount of different control types available (see my spreadsheet with the ones I’ve found out about so far), and it becomes a huge task in its own. KeyActionBinder works by doing all that maintenance itself (with constants built-in for typical button controls), at the expense/benefit (depending on how you look at it) of adding a layer of abstraction of its own via actions.

Abstracting with actions

Long story short, this is how KeyActionBinder works (keyboard input first):

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create keyboard bindings
	binder.addKeyboardActionBinding("move-left", Keyboard.LEFT);
	binder.addKeyboardActionBinding("move-right", Keyboard.RIGHT);
}

private function gameLoop():void {
	// Check for input state
	if (binder.isActionActivated("move-left")) {
		// Move the player to the left...
	} else if (binder.isActionActivated("move-right")) {
		// Move the player to the right...
	}

	// Rest of game loop
}

In the above code, "move-left" and "move-right" are the actions used by the code. These are arbitrary String ids used by the developer; they could be anything, as long as they match the verification that’s being done in the game loop (for this reason most people would have some game-specific constants I assume).

Adding additional bindings

Now, suppose you want to use additional keys to move left and right. This is where it starts getting interesting – you just add more bindings during the setup phase of the KeyActionBinder instance.

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create keyboard bindings
	binder.addKeyboardActionBinding("move-left", Keyboard.LEFT);
	binder.addKeyboardActionBinding("move-left", Keyboard.A);
	binder.addKeyboardActionBinding("move-right", Keyboard.RIGHT);
	binder.addKeyboardActionBinding("move-right", Keyboard.D);
}

private function gameLoop():void {
	// Check for input state
	if (binder.isActionActivated("move-left")) {
		// Move the player to the left...
	} else if (binder.isActionActivated("move-right")) {
		// Move the player to the right...
	}

	// Rest of game loop
}

The code would still work the same – both Keyboard.LEFT and Keyboard.A would activate the "move-left" action. As a bonus, both keys would simply stack their efforts to activate the action: the action wouldn’t be “activated twice”, or deactivated by mistake in case the two keys were pressed at the same time and then one of them was released.

In other words, the game loop has no knowledge of the actual key input state; it only has knowledge of the action activation state (as it should). Everything else is encapsulated inside KeyActionBinder.

Dealing with game controllers

While interesting for keyboard events, this solution starts shining when you add game controller support. In the above case, to add simple controller support (using the directional pad of most controllers), you do:

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addKeyboardActionBinding("move-left", Keyboard.LEFT);
	binder.addKeyboardActionBinding("move-left", Keyboard.A);
	binder.addGamepadActionBinding("move-left", GamepadControls.DPAD_LEFT);
	binder.addKeyboardActionBinding("move-right", Keyboard.RIGHT);
	binder.addKeyboardActionBinding("move-right", Keyboard.D);
	binder.addGamepadActionBinding("move-right", GamepadControls.DPAD_RIGHT);
}

private function gameLoop():void {
	// Check for input state
	if (binder.isActionActivated("move-left")) {
		// Move the player to the left...
	} else if (binder.isActionActivated("move-right")) {
		// Move the player to the right...
	}

	// Rest of game loop
}

The above code will allow any attached game controller have its directional pad control the player.

Individual game controllers for individual players

KeyActionBinder also allows you to bind actions to individual controllers – for a two-player game, for example. The code below will only accept game controller input from the first controller (player 1) by passing one additional parameter to addGamepadActionBinding() with the controller index (0, in this case):

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addKeyboardActionBinding("move-left", Keyboard.LEFT);
	binder.addKeyboardActionBinding("move-left", Keyboard.A);
	binder.addGamepadActionBinding("move-left", GamepadControls.DPAD_LEFT, 0);
	binder.addKeyboardActionBinding("move-right", Keyboard.RIGHT);
	binder.addKeyboardActionBinding("move-right", Keyboard.D);
	binder.addGamepadActionBinding("move-right", GamepadControls.DPAD_RIGHT, 0);
}

private function gameLoop():void {
	// Check for input state
	if (binder.isActionActivated("move-left")) {
		// Move the player to the left...
	} else if (binder.isActionActivated("move-right")) {
		// Move the player to the right...
	}

	// Rest of game loop
}

For two-player support, different actions would be used ("move-left-player-1" and "move-left-player-2", for example), each bound to its own game controller.

Handling analog input

Controlling action activation in a Boolean fashion is not everything a game normally needs. Most game controllers have analog inputs, in the form of analog sticks or pressure-sensitive triggers. These usually give you a value indicating the horizontal/vertical direction of the stick, or how much a button is actually pressed, respectively.

On KeyActionBinder, this is controlled in a similar fashion to normal actions, but using what is called a sensitive action. The code to use an analog stick looks like this:

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addGamepadSensitiveActionBinding("axis-x", GamepadControls.STICK_LEFT_X, NaN, -1, 1);
	binder.addGamepadSensitiveActionBinding("axis-y", GamepadControls.STICK_LEFT_Y, NaN, -1, 1);
}

private function gameLoop():void {
	// Check for input state
	var speedX:Number = binder.getActionValue("axis-x"); // Value will be between -1 and 1
	var speedY:Number = binder.getActionValue("axis-y");

	// Move the player...

	// Rest of game loop
}

In the code above, NaN on the addGamepadSensitiveActionBinding() call means that input can come from any game controller/player, and -1 and 1 denote the value you want to come out of that sensitive action. KeyActionBinder automatically reads the built-in minimum and maximum values possible for that control, and return you a value that is mapped to the range you want (otherwise, 0 and 1 is used as default as the minimum and maximum values).

Pressure-sensitive triggers are used in the same fashion. To use the shoulder buttons available in most modern game controllers, for example, the code looks like this:

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function gameStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addGamepadSensitiveActionBinding("run-speed", GamepadControls.L2_SENSITIVE);
}

private function gameLoop():void {
	// Check for input state
	var runSpeed:Number = binder.getActionValue("run-speed"); // Value will be between 0 and 1

	// Rest of game loop
}

That’s it for standard game implementation!

Stopping and resuming

Some times, game input should start being ignored. For example, if a dialog pops up or an animation is playing. In cases like this, one would normally pause processing of game input, and then resume later. With KeyActionBinder, this is accomplished by calling stop() and start():

private function pauseGame():void {
	// Stop interpreting input
	binder.stop();

	// Rest of pause code...
}

private function resumeGame():void {
	// Resume input
	binder.start();

	// Rest of resume code...
}

In some cases, such as menu dialogs, it would make sense to pause the game’s KeyActionBinder instance, and have a new instance created and fired to handle the new input methods. Once completed, the new binder can be stopped and disposed of, and the game’s own instance can just resume handling input.

The stop() method should always be called when you don’t need a KeyActionBinder instance anymore, otherwise it’ll be listening to events indefinitely.

Events

Certain interfaces, such as UI, dialogs, and menus, work better when handling user input via standard events, rather than reading the device control’s current state during a game loop. For this, KeyActionBinder also allows the use standard press/release events. Only that instead of standard events, it uses a Signals approach for faster speed and lower memory allocation. This is how it is used:

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function menuStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addKeyboardActionBinding("move-left", Keyboard.LEFT);
	binder.addGamepadActionBinding("move-left", GamepadControls.DPAD_LEFT);
	binder.addKeyboardActionBinding("move-right", Keyboard.RIGHT);
	binder.addGamepadActionBinding("move-right", GamepadControls.DPAD_RIGHT);

	// Add callbacks to the event signals
	binder.onActionActivated.add(onActionActivated);
	binder.onActionDeactivated.add(onActionReleased);
}

private function onActionActivated(__action:String):void {
	trace("The user activated the " + __action + " action by pressing a key or button.");
	if (__action == "move-left") myMenu.moveLeft();
	if (__action == "move-right") myMenu.moveRight();
}

private function onActionDeactivated(__action:String):void {
	trace("The user deactivated the " + __action + " action by releasing a key or button.");
	// No code needed since the menu was already moved on press
}

The same goes for sensitive controls. Rather than reading the position of an analog input on every loop, this is how it can be done with an event:

// KeyActionBinder controls the input state
private var binder:KeyActionBinder;

private function menuStart():void {
	// Create binder instance
	binder = new KeyActionBinder(stage);

	// Create input bindings
	binder.addGamepadSensitiveActionBinding("run-speed", GamepadControls.L2_SENSITIVE);

	// Add callbacks to the event signals
	binder.onSensitiveActionChanged.add(onSensitiveActionChanged);
}

private function onSensitiveActionChanged(__action:String, __value:Number):void {
	trace("The user activated the " + __action + " action's value. The new value is " + __value);
	playerRunSpeed = __value;
}

In the above code, the __value parameter received by onSensitiveActionChanged will be in the range of the minValue/maxValue parameters passed to addGamepadSensitiveActionBinding, in the same way it works when reading the values directly in the game loop. The events merely tell your code that a change was registered.

That’s about it. Again, you can find KeyActionBinder on GitHub. I’m still finishing writing the AsDoc-like comments for the class, but it works well. I will probably do some small changes in the future as I work on performance (there’s a bit code here and there that I know can be faster), add more control constants, and add one or two additional features, and refactor it a bit, so it’s still a work in progress. However, it’s solid enough that I’m using it in a couple of projects.

. . .
Notes:

1. If you don’t need any fancy-pants input control configuration or multi-player input, simpler approaches will obviously also work for reading the controller status. This class by Terry Cavanagh seems to be popular.

2. While the GameInput API works as intended on Android/OUYA, it has some performance drawbacks on the desktop version of AIR. Currently, if you use the GameInput API in any way, an internal Timer event (used to check for new devices attached) will take around 38ms of processing once every second. This means that if you have a game running at, say, 60fps, it will most likely skip a frame or two every second. This is a desktop version problem only; this problem is not present on Android, as it uses proper internal events for device detection rather than check for them every second. Adobe is aware of the problem and seems to be working on it. In the meantime, the old Native Extensions that allow game controller input in Adobe AIR still work. The ones I know of are the Joystick AIR ANE (Windows only) and the Gaslight Games’ AIR OUYA Controller (OUYA/Android only, though, so this is kind of moot for this issue).

3. If your GameInput controls suddenly stop working on OUYA or Android, this is likely due to a bug on Adobe AIR’s implementation. See this post for reference and a workaround.

2 responses

    • Hi Jeff,

      Yes, it’s free to use on any application, commercial or not. Mentioning the source is not necessary.

      (You’re right, I have to make the license more clear)

Comments are closed.