KeyActionBinder updates: time sensitive activations, new constants

When testing my latest game prototype, I realized I needed certain player inputs to be time-sensitive, that is, only be acceptable if they were activated within a certain time limit. This is useful for one specific condition: jumping.

In my game prototype, I had my jump activation checks like so:

function gameLoop():void {
	// Calculate whether the player is touching the ground
	var isPlayerTouchingTheGround:Boolean = ...

	if (isPlayerTouchingTheGround && binder.isActionActivated("jump")) {
		// Execute jumping action
		body.applyImpulse(new Vec2(0, JUMP_FORCE));

		// "Consume" the action so the player has to press the button again to jump again
		binder.consumeAction("jump");
	}
}

This works well, but it has an unexpected behavior: if the player jumps once (the action is activated, and then consumed), and then presses the jump button again while in mid-air, the action will remain activated until the player touches the ground, when it will allow the player to immediately jump again upon contact.

I could fix this situation by constantly consuming the jump activation when a jump cannot be performed, effectively forcing the player to press the button in the exact instant he/she wants to perform a jump.

function gameLoop():void {
	// Calculate whether the player is touching the ground
	var isPlayerTouchingTheGround:Boolean = ...

	if (binder.isActionActivated("jump")) {
		// "Consume" the action so the player has to press the button again to jump again
		binder.consumeAction("jump");

		if (isPlayerTouchingTheGround) {
			// Execute jumping action
			body.applyImpulse(new Vec2(0, JUMP_FORCE));
		}
	}
}

This also works, but it introduces a new problem: if the player presses the jump button immediately before his/her character is about to hit the ground *thinking it’s time to perform a jump( the action may be ignored, consumed, and the player will hit the ground with a thud, as if the jump action was ignored. This may sound like a small problem, but in practice, it isn’t: it’s very typical that when jumping repeatedly, players will press the jump button just before contact with the ground.

The solution I reached is to make activations time-sensitive – there is, the current time is stored with them every time they are activated. That way, one easily allow for a more lenient activation control, effectively allowing players to control the game in a way that feels natural, rather than what is mathematically correct.

For this, I have updated KeyActionBinder to support the use of a time parameter on isActionActivated. With this change, the updated jump check now looks like this in my game prototype:

function gameLoop():void {
	// Calculate whether the player is touching the ground
	var isPlayerTouchingTheGround:Boolean = ...

	// Allows for 0.3s of leniency on the time check
	if (isPlayerTouchingTheGround && binder.isActionActivated("jump", 0.3)) {
		// Execute jumping action
		body.applyImpulse(new Vec2(0, JUMP_FORCE));

		// "Consume" the action so the player has to press the button again to jump again
		binder.consumeAction("jump");
	}
}

And it feels much better to play.

Lastly, given the recent changes to the GameInput API controls, and the new OSX controls, I have made some profound changes to KeyActionBinder’s sister class, GamepadControls. This class contains a list of controls (as constants) that are normally supported by Adobe AIR’s GameInput on several platforms. I was trying to keep a consistent naming structure for its constants (e.g., STICK_LEFT_X for the horizontal position of the left stick on any platform), but it’s clear now that this isn’t possible given the differences in control ids between platforms. As such, I’ve prefaced all constants with platform names (e.g., ANDROID_STICK_LEFT_X and WINDOWS_STICK_LEFT_X) for consistency’s sake. This results in some changes to how code sets up actions using KeyActionBinder; in my case, my initialization code works something like this now:

// Decide platform, somehow
var platform:String = ...

// Key bindings - game initialization
switch (platform) {
	case "android":
	case "ouya":
		// Android and OUYA: use keys (injected from dpad) and gamepad
		binder.addKeyboardActionBinding(ACTION_LEFT, Keyboard.LEFT); // dpad left
		binder.addKeyboardActionBinding(ACTION_RIGHT, Keyboard.RIGHT); // dpad right
		binder.addGamepadActionBinding(ACTION_JUMP, GamepadControls.ANDROID_BUTTON_ACTION_DOWN);
		break;
	case "windows":
		// Windows PC: use keyboard and XBox 360 controller
		binder.addKeyboardActionBinding(ACTION_LEFT, Keyboard.LEFT);
		binder.addKeyboardActionBinding(ACTION_RIGHT, Keyboard.RIGHT);
		binder.addKeyboardActionBinding(ACTION_JUMP, Keyboard.SPACE);

		binder.addGamepadActionBinding(ACTION_LEFT, GamepadControls.WINDOWS_DPAD_LEFT);
		binder.addGamepadActionBinding(ACTION_RIGHT, GamepadControls.WINDOWS_DPAD_RIGHT);
		binder.addGamepadActionBinding(ACTION_JUMP, GamepadControls.WINDOWS_BUTTON_ACTION_A);
		break;
	case "osx":
		// OSX Mac: use keyboard and XBox 360 controller
		binder.addKeyboardActionBinding(ACTION_LEFT, Keyboard.LEFT);
		binder.addKeyboardActionBinding(ACTION_RIGHT, Keyboard.RIGHT);
		binder.addKeyboardActionBinding(ACTION_JUMP, Keyboard.SPACE);

		binder.addGamepadActionBinding(ACTION_LEFT, GamepadControls.OSX_DPAD_LEFT);
		binder.addGamepadActionBinding(ACTION_RIGHT, GamepadControls.OSX_DPAD_RIGHT);
		binder.addGamepadActionBinding(ACTION_JUMP, GamepadControls.OSX_BUTTON_ACTION_A);
		break;
}

This is fairly straightforward (if somewhat verbose). One of the issues, of course, is how to determine the platform the code is being ran on. This can, for the most part, be inferred by checking Capabilities.manufacturer and Capabilities.os. In my case, however, because I want a safer, more granular control (e.g. differentiating between Android and OUYA, for some other user input features), I am actually passing the platform type to the compiled SWF using a compile-time constant named CONFIG::PLATFORM that states which platform the compiled SWF is targetting. It’s a more reliable way to deal with platform differences that require an actual platform check.