What is the most effective way to test for combined keyboard arrow direction in ActionScript 3.0? - actionscript-3

I need to monitor the direction a user is indicating using the four directional arrow keys on a keyboard in ActionScript 3.0 and I want to know the most efficient and effective way to do this.
I've got several ideas of how to do it, and I'm not sure which would be best. I've found that when tracking Keyboard.KEY_DOWN events, the event repeats as long as the key is down, so the event function is repeated as well. This broke the method I had originally chosen to use, and the methods I've been able to think of require a lot of comparison operators.
The best way I've been able to think of would be to use bitwise operators on a uint variable. Here's what I'm thinking
var _direction:uint = 0x0; // The Current Direction
That's the current direction variable. In the Keyboard.KEY_DOWN event handler I'll have it check what key is down, and use a bitwise AND operation to see if it's already toggled on, and if it's not, I'll add it in using basic addition. So, up would be 0x1 and down would be 0x2 and both up and down would be 0x3, for example. It would look something like this:
private function keyDownHandler(e:KeyboardEvent):void
{
switch(e.keyCode)
{
case Keyboard.UP:
if(!(_direction & 0x1)) _direction += 0x1;
break;
case Keyboard.DOWN:
if(!(_direction & 0x2)) _direction += 0x2;
break;
// And So On...
}
}
The keyUpHandler wouldn't need the if operation since it only triggers once when the key goes up, instead of repeating. I'll be able to test the current direction by using a switch statement labeled with numbers from 0 to 15 for the sixteen possible combinations. That should work, but it doesn't seem terribly elegant to me, given all of the if statements in the repeating keyDown event handler, and the huge switch.
private function checkDirection():void
{
switch(_direction)
{
case 0:
// Center
break;
case 1:
// Up
break;
case 2:
// Down
break;
case 3:
// Up and Down
break;
case 4:
// Left
break;
// And So On...
}
}
Is there a better way to do this?

You can keep track of whether each key is down or not by listening for all KEY_DOWN and KEY_UP events, and storing each key state in an array. I wrote a class a while ago to do just that (included at the end of my answer).
Then you are no longer tied to the event model to know if a certain key is down or not; you can periodically check every frame (or every timer interval). So you could have a function like:
function enterFrameCallback(e:Event):void
{
var speed:Number = 1.0; // net pixels per frame movement
thing.x += (
-(int)Input.isKeyDown(Keyboard.LEFT)
+(int)Input.isKeyDown(Keyboard.RIGHT)
) * speed;
thing.y += (
-(int)Input.isKeyDown(Keyboard.UP)
+(int)Input.isKeyDown(Keyboard.DOWN)
) * speed;
}
which would take into account all possible combinations of arrow key presses. If you want the net displacement to be constant (e.g. when going right and down at same time, the object moves X pixels diagonally, as opposed to X pixels in both horizontal and vertical directions), the code becomes:
function enterFrameCallback(e:Event):void
{
var speed:Number = 1.0; // net pixels per frame movement
var displacement:Point = new Point();
displacement.x = (
-(int)Input.isKeyDown(Keyboard.LEFT)
+(int)Input.isKeyDown(Keyboard.RIGHT)
);
displacement.y = (
-(int)Input.isKeyDown(Keyboard.UP)
+(int)Input.isKeyDown(Keyboard.DOWN)
);
displacement.normalize(speed);
thing.x += displacement.x;
thing.y += displacement.y;
}
Here is the Input class I wrote (don't forget to call init from the document class). Note that it also keeps track of mouse stuff; you can delete that if you don't need it:
/*******************************************************************************
* DESCRIPTION: Defines a simple input class that allows the programmer to
* determine at any instant whether a specific key is down or not,
* or if the mouse button is down or not (and where the cursor
* is respective to a certain DisplayObject).
* USAGE: Call init once before using any other methods, and pass a reference to
* the stage. Use the public methods commented below to query input states.
*******************************************************************************/
package
{
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.display.Stage;
import flash.geom.Point;
import flash.display.DisplayObject;
public class Input
{
private static var keyState:Array = new Array();
private static var _mouseDown:Boolean = false;
private static var mouseLoc:Point = new Point();
private static var mouseDownLoc:Point = new Point();
// Call before any other functions in this class:
public static function init(stage:Stage):void
{
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false, 10);
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp, false, 10);
stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown, false, 10);
stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp, false, 10);
stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove, false, 10);
}
// Call to query whether a certain keyboard key is down.
// For a non-printable key: Input.isKeyDown(Keyboard.KEY)
// For a letter (case insensitive): Input.isKeyDown('A')
public static function isKeyDown(key:*):Boolean
{
if (typeof key == "string") {
key = key.toUpperCase().charCodeAt(0);
}
return keyState[key];
}
// Property that is true if the mouse is down, false otherwise:
public static function get mouseDown():Boolean
{
return _mouseDown;
}
// Gets the current coordinates of the mouse with respect to a certain DisplayObject.
// Leaving out the DisplayObject paramter will return the mouse location with respect
// to the stage (global coordinates):
public static function getMouseLoc(respectiveTo:DisplayObject = null):Point
{
if (respectiveTo == null) {
return mouseLoc.clone();
}
return respectiveTo.globalToLocal(mouseLoc);
}
// Gets the coordinates where the mouse was when it was last down or up, with respect
// to a certain DisplayObject. Leaving out the DisplayObject paramter will return the
// location with respect to the stage (global coordinates):
public static function getMouseDownLoc(respectiveTo:DisplayObject = null):Point
{
if (respectiveTo == null) {
return mouseDownLoc.clone();
}
return respectiveTo.globalToLocal(mouseDownLoc);
}
// Resets the state of the keyboard and mouse:
public static function reset():void
{
for (var i:String in keyState) {
keyState[i] = false;
}
_mouseDown = false;
mouseLoc = new Point();
mouseDownLoc = new Point();
}
///// PRIVATE METHODS BEWLOW /////
private static function onMouseDown(e:MouseEvent):void
{
_mouseDown = true;
mouseDownLoc = new Point(e.stageX, e.stageY);
}
private static function onMouseUp(e:MouseEvent):void
{
_mouseDown = false;
mouseDownLoc = new Point(e.stageX, e.stageY);
}
private static function onMouseMove(e:MouseEvent):void
{
mouseLoc = new Point(e.stageX, e.stageY);
}
private static function onKeyDown(e:KeyboardEvent):void
{
keyState[e.keyCode] = true;
}
private static function onKeyUp(e:KeyboardEvent):void
{
keyState[e.keyCode] = false;
}
}
}

Related

AS3 Update Variable Reports Null.. in a pattern

I'm making a space game in AS3 - I have an enemy class named EnemyShip.
I've already registered the Event.ENTER_FRAME and it works correctly - the problem is that my variable, rowXY of type Array, reports both null and a non-null value.. in a pattern.
How can I keep this from happening (or is there an easier way to animate the ships to move in Flash Professional?).
The pattern is as follows:
EnemyShip:
package
{
import flash.display.Sprite;
import flash.events.Event;
public class EnemyShip extends Sprite
{
internal var id:int;
internal var rowOrder:int;
internal var rowXY:Array;
private var dirUp:Boolean = false;
public function EnemyShip()
{
// add event listeners
addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
addEventListener(Event.ENTER_FRAME, update);
}
private function update(e:Event):void
{
moveUpAndDown();
trace(rowXY);
function moveUpAndDown():void
{
if (dirUp)
y -= C.ENEMY_SPEED;
else
y += C.ENEMY_SPEED;
}
}
private function onAddedToStage(e:Event):void
{
// get row XY
if (rowOrder == 1)
rowXY = C.ENEMY_ROW_1;
if (rowOrder == 2)
rowXY = C.ENEMY_ROW_2;
if (rowOrder == 3)
rowXY = C.ENEMY_ROW_3;
// set XY position
x = rowXY[0];
y = rowXY[1];
removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}
internal function destroy():void
{
rowOrder = null;
rowXY = null;
// remove event listeners
removeEventListener(Event.ENTER_FRAME, update);
// remove from display list
parent.removeChild(this);
}
}
}
C.as (Constants):
package
{
// C stands for constants
public class C
{
// ship constants
public static const FIRE_DELAY:int = 500; // milliseconds
// laser constants
public static const LASER_SPEED:int = 30;
public static const POINTS_KILL:int = 10;
// mcScore constants
public static const SCORE_LOC_X:Number = 345;
public static const SCORE_LOC_Y:Number = -120;
// enemy ship constants
public static const ENEMY_ROW_1:Array = [485, -45];
public static const ENEMY_ROW_2:Array = [485, 25];
public static const ENEMY_ROW_3:Array = [485, 95];
public static const ENEMY_SPEED:int = 5; // 5 pixels
public static const ENEMY_Y_MIN:int = -10;
public static const ENEMY_Y_MAX:int = 10;
}
}
If you're trying to animate using Flash Professional, I would take advantage of their built in Motion Tween feature. You could also animate the ship in your code by taking advantage of the Tween class.
I reproduced the behavior that you've got using this for loop ( you can get the same result by instantiating 3 objects and insert 3 others directly to the stage ) :
for(var i:int = 1; i < 7; i++){
var enemy:EnemyShip = new EnemyShip();
enemy.rowOrder = i;
addChild(enemy);
}
here we can see very clear that where i is 4, 5 or 6, rowOrder will be null which also will fire some #1009 errors for all the instances which has the rowOrder greater that 3 from this line :
x = rowXY[0];
So to avoid that, you can, for example, restrict the value of rowOrder to be between 1 and 3, like this, for example :
enemy.rowOrder = 1 + int(Math.random()*3);
you can also set that value inside the EnemyShip class itself.
...
Hope that can help.
The Solution
It was to my knowledge that the event Event.ENTER_FRAME was only passed to objects on the stage, but this is not true.
The problem was in my EnemyShip class' constructor method - the ENTER_FRAME event was being listened to by both on- and off-stage instances - the off-stage instances had not been assigned a rowXY (this happens in the event ADDED_TO_STAGE).
Old Code:
New Code:
So what's the lesson to be learned here?
Event.ENTER_FRAME happens no matter if the object is displayed or not.
In the screenshot you posted you have this code:
if(rowOrder > 3)
rowOrder = 0;
But when you check rowOrder in onAddedToStage() you don't have a case for 0 -- only 1, 2, or 3. So rowXY never gets set when rowOrder is 0.
Either change that code to set rowOrder to 1 instead of 0, or change your other code to be zero indexed.

How to add keyboard interaction in AS3

I got 16 different buttons in this application, each button playing different kind of sound. Currently i only able to use mouse click to start playing sound, and click again to stop, but now i wish to add shortcut key to access those track buttons, like pressing "q" for track 1, "w" for track 2. Any hints or tips like what am i suppose to do to achieve this? Thanks a lot.
private var _ambientTracks:Array = [ambient1, ambient2, ambient3, ambient4];
private var _effectTracks:Array = [effect1, effect2, effect3, effect4];
private var _melodyTracks:Array = [melody1, melody2, melody3, melody4];
private var _beatTracks:Array = [beat1, beat2, beat3, beat4];
//handle click on track invoke start / stop functions
private function onTrackClicked(event:MouseEvent):void {
var track:Sprite = event.currentTarget as Sprite;
var trackName:String = track.name;
if (trackName in _playingTracks) {
stopTrack(track);
delete _playingTracks[trackName];
} else {
startTrack(track);
_playingTracks[trackName] = trackName;
}
}
//starts track animation and dispatch event for TrackMixer
private function startTrack(track:Sprite):void {
Actuate.tween(track, 0.6, {alpha: 0.3}).reflect().repeat();
dispatchEvent(new ObjectEvent(START_TRACK, track.name, true));
}
//stop track animation and dispatch event for TrackMixer
private function stopTrack(track:Sprite):void {
Actuate.stop(track, "alpha");
track.alpha = 1;
dispatchEvent(new ObjectEvent(STOP_TRACK, track.name, true));
}
Here is a basic concept similar to what you are trying to achieve you need to listen for a key press and use the specific key code handle what key is being pressed, if you have lots of key commands I suggest using a switch case.
import flash.display.MovieClip;
import flash.events.KeyboardEvent;
var mc:MovieClip = new MovieClip();
mc.graphics.beginFill(0x000000);
mc.graphics.drawCircle(30,30,30);
mc.graphics.endFill();
stage.addEventListener(KeyboardEvent.KEY_UP,keyUp);
function keyUp(e:KeyboardEvent):void{
if(e.keyCode == 81){//q
addChild(mc);
}
if(e.keyCode == 87){//w
if(mc.stage){
removeChild(mc);
}
}
}
It would still use the similar concepts, however you need to someway to "toggle" the same key to play and stop. A boolean variable would fit nicely to track the state of the key press.
import flash.display.MovieClip;
import flash.events.KeyboardEvent;
//Code to create a simple black circle xpos: 30 ypos: 30 radius: 30
var mc: MovieClip = new MovieClip();
mc.graphics.beginFill(0x000000);
mc.graphics.drawCircle(30, 30, 30);
mc.graphics.endFill();
//Adding event listener to stage for a key up event
stage.addEventListener(KeyboardEvent.KEY_UP, keyUp);
/*
Creating a boolean value to track "on" or "off" for one key.
Default value for boolean is false.
*/
var bool: Boolean;
function keyUp(e: KeyboardEvent): void {
if (e.keyCode == 81) {
if (!bool) {
addChild(mc);
bool = true;
} else {
if (mc.stage) {
removeChild(mc);
bool = false;
}
}
}
}
if key == q and bool == false add mc to stage and set bool to true.
The next time this method is called, bool will be true in which case the else statement
will be fired off (because bool is not false). The stage will be checked to see if mc exists to avoid errors, mc will be removed from stage, and bool will be set back to false in order to meet the condition of the if statement (bool is false).

Drag and Drop allow only one object per target

I'm sure this is simple but I have the drag and drop code that works wonderfully...however, it allows me to drop muliple objects per target. What I want is to allow only one object per target and I've looked and can't seem to find how to test to see if an object had already been dropt. Here's the code I have:
package com {
import flash.display.MovieClip;
import flash.events.MouseEvent;
import flash.geom.Point;
import com.greensock.*;
public class DraggableObject extends MovieClip
{
protected var origPos:Point;
public function DraggableObject()
{
origPos = new Point(x, y);
buttonMode = true; // changes cursor to hand
addEventListener(MouseEvent.MOUSE_DOWN, down);
}
protected function down (event:MouseEvent): void
{
parent.addChild(this); // adds object to the top of the display list to keep the object on top
startDrag(); // built in Flash method
stage.addEventListener(MouseEvent.MOUSE_UP, stageUp);
var upPosX = x + 5;
var upPosY = y - 5;
TweenMax.to(this, .2, {dropShadowFilter:{color:0x666666, alpha:1, blurX:12, blurY:12, distance:12}});
TweenLite.to(this, .2, {x: upPosX, y: upPosY});
mouseEnabled = false;
}
protected function stageUp (event:MouseEvent): void
{
stage.removeEventListener(MouseEvent.MOUSE_UP, stageUp); // good coding
stopDrag(); // built in Flash method
TweenMax.to(this, .5, {dropShadowFilter:{color:0x666666, alpha:0, blurX:12, blurY:12, distance:12}});
if (dropTarget.parent.name == "root1"){
returnToOrigPos();
}
else if(dropTarget)// dropTarget is a built in property this means if dropTarget != null
{
trace("dropt on "+dropTarget.parent.name);
x = dropTarget.parent.x;
y = dropTarget.parent.y;
//buttonMode = false; //only use these to keep the user's selection...can't change if these are active.
//removeEventListener(MouseEvent.MOUSE_DOWN, down);
}
mouseEnabled = true;
}
protected function returnToOrigPos(): void
{
x = origPos.x;
y = origPos.y;
}
}
I was thinking that hitTestObject or dropTarget would work but can't figure out how to use them.
Thanks for any help!
Mike
Add a public property (eg: _hasDroppedObject) to whatever kind of object you're dropping objects into and set it to true when an object is dropped into it:
dropTarget.hasDroppedObject = true;
Then change your "if (dropTarget.parent.name == "root1")" line to:
if (dropTarget.parent.name == "root1" || dropTarget.hasDroppedObject) {
(This assumes you have access to the drop-receiving Class).

HitTest for objects not yet on Stage

I need to add a MovieClip to stage, the limitation being that it should only be added to an empty area on the stage. The stage itself either contains complex shapes or is manipulable by the user i.e. he can drag/move objects to change the empty area. The hitTest and hitTestObject methods need DisplayObject already available on the stage. What is the right way to go - the only solution I can imagine is having added my object on the stage and then repeatedly doing hit tests?
[Imagine it to something like adding sprites in a video game - they must spawn in empty regions; if they pop out from inside of each other, then it'll look really odd.]
Well, when you create a new class, just turn it off with a variable and set the visibility to false, then loop until there is no hitTest.
A silly example:
public class someClass extends Sprite
{
private var objectsOnStage:Array;
public function someClass(objectsArray:Array) {
objectsOnStage = objectsArray;
visible = false;
addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event){
removeEventListener(Event.ADDED_TO_STAGE, init);
addEventListener(Event.ENTER_FRAME, SEARCH);
}
private function SEARCH(e:Event) {
var doesHit:Boolean = false;
x = Math.round(Math.random() * (550 - 0)) + 0;
y = Math.round(Math.random() * (400 - 0)) + 0;
for (var i:int = 0; i < objectsOnStage; i++) {
if (doesHit) break;
if (this.hitTestObject(objectsOnStage[i])) {
doesHit = true;
}
}
if (doesHit) return;
placedInit();
}
private function placedInit() {
visible = true;
removeEventListener(Event.ENTER_FRAME, SEARCH);
//now init the stuff you want.
}
}
You just check if bounding boxes of both clips overlaps. Like this:
import flash.geom.Rectangle;
import flash.display.MovieClip;
// create simple movie clips that has a rectangle shape inside
var sym1 : MovieClip = new Sym1();
var sym2 : MovieClip = new Sym2();
// get a rectanle of both clipt
var boundingBox1 : Rectangle = sym1.getBounds(this);
var boundingBox2 : Rectangle = sym2.getBounds(this);
// check if bounding boxes of both movie clips overlaps
// so it works like hitTestObject() method
trace( boundingBox1.intersects( boundingBox2) )
I know this post is super old, but in case it helps anybody --
If you need to do a hit test on a movieclip that isn't on the stage. A workaround is to rasterize it to a bitmap first.
var bitmapData:BitmapData = new BitmapData(mc.width, mc.height, true, 0x0000000);
bitmapData.draw(mc);
if (bitmapData.getPixel32(x, y) > 0) {
// Hit true.
}

Actionscript: Am I deleting this class instance correctly?

Okay by deleting correctly I mean am I actually getting rid of the instance or is it just not being drawn anymore? I should mention that I'm trying to delete the instance from within its own class, that is it deletes itself. It 'works' in that the square it draws no longer appears on the screen but again I'm not sure if it's really gone or just not being drawn. Anyway here's the class:
package
{
import flash.display.*;
import flash.events.*;
public class OBJECT_bullet_1 extends Sprite
{
public var X:int = 0; public var Y:int = 0;
public var Y_SPEED:int = 5;
public var DEPTH:int = 9;
public var CONTAINER:Sprite = new Sprite();
public function CREATE(CONTAINER:Sprite,X:int,Y:int):void
{
this.CONTAINER = CONTAINER;
CONTAINER.stage.addEventListener(Event.ENTER_FRAME,STEP);
this.X = X; this.Y = Y;
DRAW();
}
public function STEP(event:Event):void
{
this.graphics.clear();
Y -= Y_SPEED;
if (Y < 20) {Y = 300; CONTAINER.removeChild(this); CONTAINER.stage.removeEventListener(Event.ENTER_FRAME,STEP); CONTAINER.(delete this); CONTAINER = null; return;}
DRAW();
}
public function DRAW():void
{
this.graphics.beginFill(0xCCCC00,1);
this.graphics.drawRect(X - 2,Y - 2,4,4);
this.graphics.endFill();
CONTAINER.addChild(this);
}
}
}
The part I'm concerned about is in the STEP function when it checks to see if Y < 20. You'll notice that it does several things afterwords. Am I deleting it correctly? If so is there anything I am doing to delete it that I don't need to?
Yes to both questions. To ensure an object is deleted, all you have to do is remove all references to it. The child reference and event callback are the only ones the above code is aware of, and you have taken care to remove them both. Nullifying your own container reference is unnecessary, as is whatever you think CONTAINER.(delete this) does.
There are some other significant problems with your supplied code. I made some improvements and heavily commented all changes to explain why I made them.
// You should avoid using the default package. Using the default package
// can make it difficult later on if you start having naming conflicts.
package com.stackoverflow.example {
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Point;
import flash.utils.getTimer;
// Class names are spelled in CamelCase by convention. Also, note
// that "Object" has a special meaning in AS3 so you should avoid
// using it to refer to anything else. I used here "Entity" instead.
public class EntityBullet1 extends Sprite {
// ALLCAPS when used are reserved for static const names.
// A good use of static consts is to store "magic numbers".
public static const DEFAULT_COLOR:uint = 0xCCCC00;
public static const DEFAULT_SPEED_X:Number = 0;
public static const DEFAULT_SPEED_Y:Number = -100;
public static const DEFAULT_SIZE:Number = 4;
// I'm calculating the time between frames for smoother movement.
public var lastTime:int;
public var color:uint = DEFAULT_COLOR;
public var size:int = DEFAULT_SIZE;
// Instead of separate x and y vars, you can use the Point class.
public var pos:Point;
public var speed:Point;
// Instead of a "create" method do all creation inside the constructor!
public function EntityBullet1(x:Number = 0, y:Number = 0) {
pos = new Point(x, y);
speed = new Point(DEFAULT_SPEED_X, DEFAULT_SPEED_Y);
// You don't need the parent container to access the ENTER_FRAME
// event. Every DisplayObject has its own. Much simpler.
addEventListener(Event.ENTER_FRAME, firstStep);
}
public function draw():void {
// Keep all drawing inside the draw function. Previously,
// clear() was being called inside the step method.
graphics.clear();
graphics.beginFill(color);
graphics.drawRect(pos.x - size/2, pos.y - size/2, size, size);
graphics.endFill();
}
// On the first frame, the field "lastTime" is still uninitialized.
// This method initializes it to the current time and hands off
// future events to the proper step() method.
public function firstStep(event:Event):void {
removeEventListener(Event.ENTER_FRAME, firstStep);
addEventListener(Event.ENTER_FRAME, step);
lastTime = getTimer();
step(event);
}
public function step(event:Event):void {
// To move at a fixed rate regardless of how fast the framerate is,
// you need to calculate the time delta.
var cur:int = getTimer();
var delta:Number = (cur - lastTime) / 1000.0;
lastTime = cur;
// Position equals velocity times time.
pos.x += speed.x * delta;
pos.y += speed.y * delta;
draw();
// Note that all DisplayObjects already have references to their
// parent containers called "parent"!
if (pos.y < 20) {
if (parent != null) parent.removeChild(this);
removeEventListener(Event.ENTER_FRAME, step);
}
}
}
}