8bitrocket.com
2Sep/071

Flash CS3: Actionscript 3 (AS3) Game Primer #2: Asynchronous key detection for arcade games.

In Adobe's attempt to "improve" every aspect of Actionscript, they deleted some features that arcade game programmers have relied on for years. One of the most fundamental aspects in most arcade games is accurately timed user input detection. Sadly, this has been made much more difficult (but not impossible) in AS3. Before AS3, one only needed to use the key.isDown(keycode) global method to detect a key press. The beauty of this method was that is didn't pause before detecting the next key press. This was a subtle, but very import feature of most action games. To replicate the "arcade" experience, the keyboard needs to allow the "down" state to fire repeatedly without interruption. In Adobe's defense, the key.isDown(keycode) method was not good for detecting text input as the repeating nature of the key presses was terrible for that purpose. Also, Adobe has explained, there are some security reasons why this method was eliminated. It has been replaced with a new keyboard listener that is much better for text input, but terrible for arcade game input. The new listener pauses after the initial key down event on a key, and waits a few milliseconds to fire off a repeat of the key. While this is wonderful for text input, it makes a game like Asteroids play very strange. When a user holds down the left or right "rotate" button in Asteroids, the result should be a smooth repeating rotation. The rotation should not increment once, then pause, then start incrementing again. This is impossible with a plain vanilla implementation of the new key listener model.. On top of that, the synchronous nature of the new event model makes game loop timing almost impossible. So, even if they had added in some sort of key repeat setting in the key listener, game loops would still be difficult to create. All is not lost though, in this lesson will examine a efficient method of using this new key listener and still detect non-pausing, repeatable key presses.

Why asynchronous key detection is needed for an action game loop.

Arcade games require very exact timing. The basic code for an AS2 arcade game loop might look like this:[cc lang="javascript" width="550"]private function gameLoop():void {
getKeys();
moveObjectsInMemory()
checkCollisions();
render();

}[/cc]

In a simple shooter style game, the above would function like this:

1. Check for some user input
2. Update positions of player and enemy objects in memory only. Don't physically move them on the screen. For example, variables such as nextx and nexty can be used to hold the next location in memory.
3. Use math collision detection methods, circle - circle, square - square, tile based, pixel level, or other methods using the new locations stored in memory (not the current physical locations of objects).
For instance, if the user tries to go in a direction or to a location he cannot go, the nextx and nexty would be changed back to the original x and y before the objects positions are updated on the screen. This is a simple way to prevent the player from moving onto a tile that is not passable without physically moving him first, then checking his new location against rules and collisions, and the moving him back again. There is no need to display those actions on the screen to the user, and it is much cleaner to do it before it is visually represented on the screen.
4. Copy the nextx and nexty locations to the x and y properties of the onscreen objects and put them on the screen.
5. Repeat.

All four of these methods must be timed to fire off asynchronously. This is where the new key listener model becomes a problem. If the program is constantly listening for key listen events, the player can update his/her position during the other phases of the game loop, and not just during the "getKeys" method. One possible solution would be the repeatedly add and delete the key listener so keys are only checked during the getKeys phase. Even if this idea worked (I tried it with no success), it wouldn't solve the next problem - detecting repeating keys.

Here is a basic example of an Asteroids style ship rotation using just the new listener model:

Hold down the left or right arrow key to rotate the ship. You will notice a little hiccup after the first key response before the smooth rotation begins.

[cc lang="javascript" width="550"]
/**
* ...
* @author Jeff Fulton
* @version 0.1
*/

package {
import flash.display.MovieClip;
import flash.events.*;

public class Mainloop1 extends MovieClip {

static const RIGHT = 39;
static const LEFT = 37;

public function Mainloop1() {

addEventListener(Event.ENTER_FRAME, run);
stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownListener);
ship_mc.nextrotation=ship_mc.rotation;
}

function run(e:Event):void {
//moveObjectsInMemory();
render();
}

function keyDownListener(e:KeyboardEvent):void {
trace("e.keyCode=" + e.keyCode);
if (e.keyCode==LEFT) {
trace("left");
ship_mc.nextrotation=ship_mc.rotation-5;
}else if (e.keyCode==RIGHT) {
trace("right");
ship_mc.nextrotation=ship_mc.rotation+5;
}

}

function moveObjectsInMemory() {
//not needed because key listener does memory update
}

function render() {
ship_mc.rotation=ship_mc.nextrotation;
}
}

}
[/cc]

In this simple example, the "run" method is the main game loop. Normally, we would have three methods for it to call to just update rotations:

getKeys();
moveObjectsInMemory();
render();

We have eliminated the getKeys() and moveObjectsInMenory methods because there is nothing for them to do. If we had enemy ships and asteroids on the screen, then the moveObjectsInMemory() function would still be needed, but the keyKeys() would still be not be needed because the keyDownListener supposedly works in its place. Obviously, the ship rotation is NOT ideal, so let's try another approach.

Hold down the left or right arrow key to rotate the ship. This version rotates much smoother than the previous example.

[cc lang="javascript" width="550"]
/**
* ...
* @author Jeff Fulton
* @version 0.1
*/

package {
import flash.display.MovieClip;
import flash.events.*;

public class Mainloop2 extends MovieClip {

static const RIGHT = 39;
static const LEFT = 37;
private var keyPressed:Boolean;
private var keyNum:int;
private var shipIncrement:int=0;
public function Mainloop2() {

addEventListener(Event.ENTER_FRAME, run);
stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownListener);
stage.addEventListener(KeyboardEvent.KEY_UP,keyUpListener);
ship_mc.nextrotation=ship_mc.rotation;
}

function run(e:Event):void {
getKeys();
moveObjectsInMemory();
render();
}

function keyDownListener(e:KeyboardEvent):void {
keyPressed=true;
keyNum=e.keyCode;
}

function keyUpListener(e:KeyboardEvent):void {
keyPressed=false;
keyNum=undefined;
}

function getKeys() {

if (keyPressed) {
switch (keyNum) {
case LEFT:
//trace("left");
shipIncrement=-5;
break;
case RIGHT:
//trace("right");
shipIncrement=5;
break;
}
}else{
shipIncrement=0;
}
}

function moveObjectsInMemory() {
ship_mc.nextrotation=ship_mc.rotation+shipIncrement;
}

function render() {
ship_mc.rotation=ship_mc.nextrotation;
}
}

}

[/cc]

The above example has re-implemented the game loop properly and the key detection is much better. Also, there is no collision detection in this quick example, but if added, the the above example would perform much better than the first example because the entire game loop runs asynchronously.

Here is what i did.

1. I created 3 new variables:

private var keyPressed:Boolean;
private var keyNum:int;
private var shipIncrement:int=0;

If the keyDownListener is fired off, the keyPressed var is set to true until the keyUpListener is fired off. The keyDownListener sets the keyNum var to be the keyCode of the key pressed.

2. The game loop contains all three asynchronous methods:

getKeys();
moveObjectsInMemory();
render();

The getKeys() function now does a switch on the key pressed and changes the shipIncrement:int var to the appropriate new value.
The moveObjectsInMemory() function now uses the shipIncrement value to rotate the ship left or right in memory. If we needed the collision detection, we would do it next before we call the render() function.

 

Detecting Multiple Key Presses

That's all find and dandy, Jeff, you say, but the ship in Asteroids does much more than just rotate. How do would the ship be able to rotate AND thrust (or fire, or all three) at the same time with code like this. One answer would be to change the keyDownListener and keyUpListener to detect individual key presses, and set boolean values for each. So, we would eliminate the keyPressed and keyNum variables and add in boolean checks for all of the separate keys that need to be detected. Another possible solution would be to create an array of pressed keys and loop through it on each interval. I think they are both valid solutions, and we'll explore the former here and leave the array version up to you. I haven't tried it yet, but I can imagine there might be some trouble with responding to multiple of the same key press. You would need some boolean variables set to true or false so a key isn't fired off twice (at first thought), so I don't think it would be too much better than this next version. If you have a great way to do this, please email me. This method is the first one I thought of, and it works, so I'm using it for the foreseeable future.

Hold down the left or right arrow key to rotate the ship. Press up to thrust the ship in the direction it is facing. With boolean variables controlling all key presses individually you will have no trouble doing both at the same time.

[cc lang="javascript" width="550"]
/**
* ...
* @author Jeff Fulton
* @version 0.1
*/

package {
import flash.display.MovieClip;
import flash.events.*;

public class Mainloop3 extends MovieClip {

static const RIGHT = 37;
static const LEFT = 39;
static const UP = 38;
private var keyPressedRight:Boolean;
private var keyPressedLeft:Boolean;
private var keyPressedUp:Boolean;
private var shipIncrement:int=0;
public function Mainloop3() {

addEventListener(Event.ENTER_FRAME, run);
stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownListener);
stage.addEventListener(KeyboardEvent.KEY_UP,keyUpListener);
ship_mc.nextrotation=ship_mc.rotation;
ship_mc.dx=0;
ship_mc.dy=0;
ship_mc.speed=.2;
ship_mc.nextx=ship_mc.x;
ship_mc.nexty=ship_mc.y;
}

function run(e:Event):void {
getKeys();
moveObjectsInMemory();
render();
}

function keyDownListener(e:KeyboardEvent):void {
switch (e.keyCode) {
case LEFT:
keyPressedRight=true;
break;
case RIGHT:
keyPressedLeft=true;
break;
case UP:
keyPressedUp=true;
break;

}

}

function keyUpListener(e:KeyboardEvent):void {
switch (e.keyCode) {
case LEFT:
keyPressedRight=false;
break;
case RIGHT:
keyPressedLeft=false;
break;
case UP:
keyPressedUp=false;
break;

}
}

function getKeys() {
shipIncrement=0;
if (keyPressedRight) {
shipIncrement=5;
}else if(keyPressedLeft) {
shipIncrement=-5;
}
if (keyPressedUp) {
ship_mc.dx=ship_mc.dx+ship_mc.speed*(Math.cos(2.0*Math.PI*(ship_mc.rotation-90)/360.0))
ship_mc.dy=ship_mc.dy+ship_mc.speed*(Math.sin(2.0*Math.PI*(ship_mc.rotation-90)/360.0))
}
}

function moveObjectsInMemory() {
ship_mc.nextrotation=ship_mc.rotation+shipIncrement;
ship_mc.nextx=ship_mc.x+ship_mc.dx;
ship_mc.nexty=ship_mc.y+ship_mc.dy;
if (ship_mc.nextx < 0) ship_mc.nextx=300; if (ship_mc.nextx > 300) ship_mc.nextx=0;
if (ship_mc.nexty < 0) ship_mc.nexty=300; if (ship_mc.nexty > 300) ship_mc.nexty=0;
}

function render() {
ship_mc.rotation=ship_mc.nextrotation;
ship_mc.x=ship_mc.nextx;
ship_mc.y=ship_mc.nexty;
}
}

}
[/cc]

In this version, we have created boolean variables to hold our three different key presses:

private var keyPressedRight:Boolean;
private var keyPressedLeft:Boolean;
private var keyPressedUp:Boolean;

The keyDownListener and keyUpListener switch on the event keyCode value and then set the boolean variables to true or false respectively. This enables the getKeys() function to work with both a left/right movement and a thrust movement at the same time. You can add booleans for each of your needed key presses and then be able to detect any number of keys, in asynchronous mode to keep your game loop and player movement timed perfectly.

There you have it. Nothing brilliant, nothing complicated, just some down home good 'ol Booleans and a plain old game loop and you have an AS3 version of AS2 keyDown functionality.

 

If you enjoyed this post, please consider leaving a comment or subscribing to the RSS feed to have future articles delivered to your feed reader.
Comments (1) Trackbacks (0)

Leave a comment

No trackbacks yet.

This site is protected by Comment SPAM Wiper.