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.