ActionScript 3 - Motion Tutorial by Chris DeLeon

Difficulty: Beg/Int (Assumes Game-Core Familiarity)
Program: MXMLC (Free ActionScript 3 Command-Line Compilation)
Estimated Completion Time: 60-90 minutes

ActionScript 3 - Game Motion Tutorial

Written by Chris DeLeon
HobbyGameDev.com

My previous ActionScript 3 - Flash Game Core Tutorial covered the basics of how to do the following within your game:

In this tutorial, we'll introduce a few types of smooth motion that will come in handy for generic 2D videogame programming. First we'll dive right into how they look in a rather crude fashion, then we'll smooth them out and improve them as we go.


Final Result Preview - ActionScript 3 Game Motion

Let's take a look at the final result we will be working towards:

Click within the game area, then use the arrow keys to move the keyboard controlled HappyBall, or additional mouse clicks to move the mouse controlled HappyBall.

This tutorial assumes familiarity with Tutorial 1: $0 Flash Development: Quick Intro to MXMLC and Tutorial 2: ActionScript 3 - Flash Game Core Tutorial - accordingly, this tutorial will go a bit faster, and information is conveyed here mostly in the form of commented code.


Step 1: Copy and Compile Starter File

If you aren't yet set up to compile with mxmlc, then follow the steps 1-4 from the first tutorial. At this point, make a copy of either GameCore.as or ShortExample.as and rename it to GameMotion.as. Replace the code in that new GameMotion.as file with the following, which will serve as our starting point for this tutorial (note that this assumes the happyball.png and good.mp3 files from the previous tutorial's GameCore example are in the same folder):

package {
  import flash.display.Sprite;
  import flash.text.TextField;
  import flash.display.Bitmap;
  import flash.events.MouseEvent;
  import flash.events.KeyboardEvent;
  import flash.media.Sound;
  import flash.utils.Timer; 
  import flash.events.TimerEvent;
  // reminder: the imports determine which built-in functionality we can use
  
  // next line is new. It lets us specify the SWF dimensions and color
  [SWF(width="500", height="375", backgroundColor="#FFFFFF")]
  public class GameMotion extends Sprite
  {  
    public var ourExampleText:TextField = new TextField();

    [Embed(source="/happyball.png")]
    public var ballImage:Class;
    
    // three uses (distinct simultaneous positions) of the happyball image:
    public var bouncyBMP:Bitmap = new ballImage() as Bitmap;
    public var mouseBMP:Bitmap = new ballImage() as Bitmap;
    public var keyboardBMP:Bitmap = new ballImage() as Bitmap;
    
    // we'll use these two variables to track x and y speed, respectively
    // by adding them to the bouncy ball's position each frame, it moves smoothly
    // we can reverse direction by multiplying these times negative 1 at any time
    public var bouncySpeed_X:Number = 0.0;
    public var bouncySpeed_Y:Number = 0.0;

    // we'll use these to keep track of where the user last clicked
    // then we'll move the mouse chasing ball toward this point
    public var mouseTarget_X:Number = 0.0;
    public var mouseTarget_Y:Number = 0.0;

    // these two will keep track of how fast the keyboard's happyball is moving
    // we'll decelerate the ball by multiplying these by a percent each frame
    public var keyboardSpeed_X:Number = 0.0;
    public var keyboardSpeed_Y:Number = 0.0;

    [Embed(source="/good.mp3")]
    public var clickSound:Class;
    public var clickSND:Sound = new clickSound() as Sound;
    
    public var constantAction:Timer;

    public function GameMotion()
    {      
      // start positions for each bitmap we're using
      // we also add each bitmap as a child to the application Sprite
      bouncyBMP.x = 50;
      bouncyBMP.y = 50;
      addChild(bouncyBMP);

      mouseBMP.x = 80;
      mouseBMP.y = 50;
      addChild(mouseBMP);

      keyboardBMP.x = 110;
      keyboardBMP.y = 50;
      addChild(keyboardBMP);

      ourExampleText.text = "Click anywhere";
      addChild(ourExampleText);
      ourExampleText.height = 16;
      
      stage.addEventListener(MouseEvent.CLICK, clickRespond);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyRespond);
      
      // draw rectangle around the edge
      graphics.lineStyle(2.0,0x000000); // line style set to 2 thickness, black
      graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight); // draw border

      // give the bouncy ball an initial x speed and y speed
      bouncySpeed_X = 3.0;
      bouncySpeed_Y = 5.0;
      
      // set mouse target initially to middle of the game area
      mouseTarget_X = stage.stageWidth*0.5;
      mouseTarget_Y = stage.stageHeight*0.5;
      
      // Note: 1000 milliseconds = 1.0 second
      constantAction = new Timer(1000/30); // 30 per 1000 milliseconds = 30 fps
      
      // call our function called "constantUpdate" every constantAction tick
      constantAction.addEventListener(TimerEvent.TIMER, constantUpdate);
      
      // kick off the timer. (Don't forget to start it after making it!)
      constantAction.start();
    }
        
    public function constantUpdate(evt:TimerEvent):void {
      if(bouncyBMP.x > stage.stageWidth) { // too far to the right?
        bouncySpeed_X = -bouncySpeed_X; // flip horizontal movement direction
      }
      if(bouncyBMP.x < 0) { // too far to the left?
        bouncySpeed_X = -bouncySpeed_X; // flip horizontal movement direction
      }

      if(bouncyBMP.y > stage.stageHeight) { // below the bottom?
        bouncySpeed_Y = -bouncySpeed_Y; // flip vertical movement direction
      }
      if(bouncyBMP.y < 0) { // above the top?
        bouncySpeed_Y = -bouncySpeed_Y; // flip vertical movement direction
      }

      // adjust bouncy ball's current position according to its velocity
      bouncyBMP.x += bouncySpeed_X;
      bouncyBMP.y += bouncySpeed_Y;

      // keyboard happyball loses 10% of its speed every frame
      keyboardSpeed_X *= 0.9;
      keyboardSpeed_Y *= 0.9;

      // adjust keyboard ball's current position according to its velocity
      keyboardBMP.x += keyboardSpeed_X;
      keyboardBMP.y += keyboardSpeed_Y;
      
      var chaseSpeed:Number = 6.0;
      
      if(mouseBMP.x < mouseTarget_X) { // left of the mouse target?
        mouseBMP.x += chaseSpeed; // move right
      }
      if(mouseBMP.x > mouseTarget_X) { // right of the mouse target?
        mouseBMP.x -= chaseSpeed; // move left
      }
      if(mouseBMP.y < mouseTarget_Y) { // above the mouse target?
        mouseBMP.y += chaseSpeed; // move down
      }
      if(mouseBMP.y > mouseTarget_Y) { // below the mouse target?
        mouseBMP.y -= chaseSpeed; // move up
      }
    }
    
    public function clickRespond(e:MouseEvent):void
    {
      // mouseX and mouseY always reflect the current mouse position
      // storing current copies of them to mouseTarget_X/Y here upon clicking
      // lets us track the mouse controlled happyball toward the latest click
      mouseTarget_X = mouseX;
      mouseTarget_Y = mouseY;
      
      // window gained focus; update instructions to mention arrows
      ourExampleText.text = "Click, use arrows";

      clickSND.play(); // beep!
    }

    public function keyRespond(event:KeyboardEvent):void
    {
      var keyMoveSpeed:Number = 4.5;
      
      switch(event.keyCode) {
        case 32: // pressing spacebar?
          // randomize bouncy ball velocity
          bouncySpeed_X = Math.random()*20.0-10; // between -10 to +10
          bouncySpeed_Y = Math.random()*20.0-10; // between -10 to +10
          clickSND.play();
          break;
        case 37: // pressing left arrow?
          keyboardSpeed_X = -keyMoveSpeed; // set horizontal speed to go left
          break;
        case 38: // pressing up arrow?
          keyboardSpeed_Y = -keyMoveSpeed; // set vertical speed to go up
          break;
        case 39: // pressing right arrow?
          keyboardSpeed_X = keyMoveSpeed; // set horizontal speed to go right
          break;
        case 40: // pressing down arrow?
          keyboardSpeed_Y = keyMoveSpeed; // set vertical speed to go down
          break;
      }
      
      // report keycode still. handy to see how the keys work
      ourExampleText.text = "Key code: " + event.keyCode;
    }
  }
}

Make sure that it compiles before we move forward, using whichever one of the following compilation lines fits your OS:

Mac or Linux compilation line:

./bin/mxmlc ./projects/GameMotion.as -static-link-runtime-shared-libraries

Windows PC compilation line:

.\bin\mxmlc projects\GameMotion.as -static-link-runtime-shared-libraries

This should create a GameMotion.swf file in your projects folder that displays 3 HappyBall graphics when opened with a browser or Flash Player, like so:

There are a few things that can be done to interact with this project:

Try out the example SWF above before moving forward. Is that what yours is doing?


Step 2: Organize the Code

We're going to make some changes to the above code, and after we do those changes, the program will behave exactly the same way as it did before the changes. If you're new to programming, that sounds a bit nutty; if you've done lots of programming and are just here to learn AS3 videogame graphics/sounds stuff, you're probably no stranger to refactoring.

In this case, we're going to reorganize the code into new functions that capture the grouping of each ball movement type. We could go further and split these out into different classes in new files, but for a project this small and simple to do that would only make the code more awkward to work with and write about. We'll rewrite code to the extent that it makes it easier to read and follow changes to, but no further.

By splitting out the functionality into new functions, the main sequence of operations in the code become more descriptive, and largely self-comment by the function names given. As improvements are made to functionality in the steps that follow, we'll be able to isolate which function the changes are being made to, rather than needing to reconsider all of the code at once.

package {
  import flash.display.Sprite;
  import flash.text.TextField;
  import flash.display.Bitmap;
  import flash.events.MouseEvent;
  import flash.events.KeyboardEvent;
  import flash.media.Sound;
  import flash.utils.Timer;
  import flash.events.TimerEvent;
  
  [SWF(width="500", height="375", backgroundColor="#FFFFFF")]
  public class GameMotion extends Sprite
  {  
    public var ourExampleText:TextField = new TextField();

    [Embed(source="/happyball.png")]
    public var ballImage:Class;
    
    public var bouncyBMP:Bitmap = new ballImage() as Bitmap;
    public var mouseBMP:Bitmap = new ballImage() as Bitmap;
    public var keyboardBMP:Bitmap = new ballImage() as Bitmap;
    
    public var bouncySpeed_X:Number = 0.0;
    public var bouncySpeed_Y:Number = 0.0;

    public var mouseTarget_X:Number = 0.0;
    public var mouseTarget_Y:Number = 0.0;

    public var keyboardSpeed_X:Number = 0.0;
    public var keyboardSpeed_Y:Number = 0.0;

    [Embed(source="/good.mp3")]
    public var clickSound:Class;
    public var clickSND:Sound = new clickSound() as Sound;
    
    public var constantAction:Timer;
    
    public function init_happyballs():void
    {
      bouncyBMP.x = 50;
      bouncyBMP.y = 50;
      addChild(bouncyBMP);

      mouseBMP.x = 80;
      mouseBMP.y = 50;
      addChild(mouseBMP);

      keyboardBMP.x = 110;
      keyboardBMP.y = 50;
      addChild(keyboardBMP);

      bouncySpeed_X = 3.0;
      bouncySpeed_Y = 5.0;
      
      mouseTarget_X = stage.stageWidth*0.5;
      mouseTarget_Y = stage.stageHeight*0.5;
    }

    public function GameMotion()
    {
      init_happyballs();

      ourExampleText.text = "Click anywhere";
      addChild(ourExampleText);
      ourExampleText.height = 16;
      
      stage.addEventListener(MouseEvent.CLICK, clickRespond);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyRespond);
      
      graphics.lineStyle(2.0,0x000000);
      graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight);
      
      constantAction = new Timer(1000/30);
      constantAction.addEventListener(TimerEvent.TIMER, constantUpdate);
      constantAction.start();
    }
    
    public function update_bouncyHappy():void
    {
      if(bouncyBMP.x > stage.stageWidth) {
        bouncySpeed_X = -bouncySpeed_X;
      }
      if(bouncyBMP.x < 0) {
        bouncySpeed_X = -bouncySpeed_X;
      }

      if(bouncyBMP.y > stage.stageHeight) {
        bouncySpeed_Y = -bouncySpeed_Y;
      }
      if(bouncyBMP.y < 0) {
        bouncySpeed_Y = -bouncySpeed_Y;
      }

      bouncyBMP.x += bouncySpeed_X;
      bouncyBMP.y += bouncySpeed_Y;
    }

    public function update_keyboardHappy():void
    {
      keyboardSpeed_X *= 0.9;
      keyboardSpeed_Y *= 0.9;

      keyboardBMP.x += keyboardSpeed_X;
      keyboardBMP.y += keyboardSpeed_Y;
    }
    
    public function update_mouseHappy():void
    {
      var chaseSpeed:Number = 6.0;
      
      if(mouseBMP.x < mouseTarget_X) { 
        mouseBMP.x += chaseSpeed;
      }
      if(mouseBMP.x > mouseTarget_X) {
        mouseBMP.x -= chaseSpeed;
      }
      if(mouseBMP.y < mouseTarget_Y) {
        mouseBMP.y += chaseSpeed;
      }
      if(mouseBMP.y > mouseTarget_Y) {
        mouseBMP.y -= chaseSpeed;
      }
    }
    
    public function constantUpdate(evt:TimerEvent):void
    {
      update_bouncyHappy();
      update_keyboardHappy();
      update_mouseHappy();
    }
    
    public function clickRespond(e:MouseEvent):void
    {
      mouseTarget_X = mouseX;
      mouseTarget_Y = mouseY;
      
      ourExampleText.text = "Click, use arrows";

      clickSND.play();
    }
    
    public function randomize_bouncySpeed():void {
      bouncySpeed_X = Math.random()*20.0-10;
      bouncySpeed_Y = Math.random()*20.0-10;
    }

    public function keyRespond(event:KeyboardEvent):void
    {
      var keyMoveSpeed:Number = 4.5;
      
      switch(event.keyCode) {
        case 32:
          randomize_bouncySpeed();
          clickSND.play();
          break;
        case 37:
          keyboardSpeed_X = -keyMoveSpeed;
          break;
        case 38:
          keyboardSpeed_Y = -keyMoveSpeed;
          break;
        case 39:
          keyboardSpeed_X = keyMoveSpeed;
          break;
        case 40:
          keyboardSpeed_Y = keyMoveSpeed;
          break;
      }
      
      ourExampleText.text = "Key code: " + event.keyCode;
    }
  }
}

We could take this compartmentalizing and refactoring much further. There are software engineers that, without prodding from a designer concerned with additional features or a business person concerned with deadlines, will refactor and organize endlessly as a matter of obsessive craft. For someone specializing in engineering, that's fine, maybe even a good sign that they're in the right line of work. However, since we're flying solo here we have to play generalist a bit, meaning the designer and business/producer concerns inside need to limit the refactoring urge to the extent that it's useful for speeding up or simplifying development, and not as a worthwhile end in itself.

Recompile, then reload (or reopen) the newly compiled SWF, and it should still act exactly the same as it did from the start code given by the first step.

The code is now better broken into managable chunks. Now we can take a closer look at improving the currently rough implementation issues. Currently each type of control has one major problem that needs to be addressed:

Image of sample program revealing various errors in how movement is handled

In the next steps, we'll patch each one of those up.

Please take a moment to look for those issues in the movement and behavior of the first example SWF. If you don't see what we're working on fixing in the following steps, it won't be very clear why we're doing additional work on this code that's already taking mouse/keyboard/no input respectively.


Step 3: Fixing Edge Bounce

This issue is the easiest one to fix.

Recall the following diagram from the previous tutorial, showing the pixel coordinates for a 12x12 image like the HappyBall image we're using:

Diagram of corners on HappyBall

To be clear, those coordinates are in no way embedded in the image - by convention, (0,0) is always the top left of a bitmap image, and (width-1,height-1) is always the bottom right of a bitmap image. The "minus one" is because (0,0) is the coordinate for an actual pixel - if (width,height) was also a pixel, the photo would have 1 row and 1 column too many.

Because we are checking the bitmap's (x,y) coordinate against the edges - but our image is larger than 1 pixel tall and wide - in our current code the top left corner (0,0) of the image is bouncing around, ignoring the size of the image. That's why the right and bottom of the HappyBall go outside the SWF frame before it reflects direction. To fix this, we'll need it to bounce some distance left of the right side. How far? The image's width from the right side - since the left coordinate (x) plus the image's width (x+11) is the right edge coordinate.

Modify the code to read as follows (new code is noted in bold):

...skipping the code before this function...

    public function update_bouncyHappy():void
    {
      if(bouncyBMP.x > stage.stageWidth-bouncyBMP.width) {
        bouncySpeed_X = -bouncySpeed_X;
      }
      if(bouncyBMP.x < 0) {
        bouncySpeed_X = -bouncySpeed_X;
      }

      if(bouncyBMP.y > stage.stageHeight-bouncyBMP.height) {
        bouncySpeed_Y = -bouncySpeed_Y;
      }
      if(bouncyBMP.y < 0) {
        bouncySpeed_Y = -bouncySpeed_Y;
      }

      bouncyBMP.x += bouncySpeed_X;
      bouncyBMP.y += bouncySpeed_Y;
    }

...skipping the code after this function...

Re-compile with mxmlc again (remember that "-static-link-runtime-shared-libraries" argument!), and you should find upon refreshing the SWF that the bouncing ball now reflects off the right and bottom edges just as naturally as it does from the top and left sides.


Step 4: Fixing Chase Toward Mouse Click

The HappyBall chasing the mouse is currently showing a weird behavior: diagonal until it is directly vertical or horizontal with the last clicked position, then following that perfect horizontal or vertical line until it arrives.

That current, rough behavior is a consequence of x and y speed for the tracking toward mouse position being determined independently, and in particular that they are each either off, on, or negative at a fixed magnitude. In other words, if the target we're moving toward is up and right of the point, the point will move both right (at "chaseSpeed") and up (at "chaseSpeed"), the same speed on both axes yielding movement along a 45 degree angle. Once either axis is aligned with the target, movement on that axis stops, leaving only vertical or horizontal movement.

The chase would look much better, and in any case much more natural, if it went directly to the target in a straight, unbroken line. We want the chase HappyBall to follow the hypotenuse of a right triangle in which the current point and the goal point form the tips of the legs.

As I mention in Math for Videogame Making, or as you may remember from school, trigonometry is handy when angles are involved. Or, if you were more into geometry than trigonometry, you may notice that vector geometry can also get us what we need for this type of problem, without getting angles involved.

Here are both ways to rewrite update_mouseHappy(). Feel free to use either.

Trigonometry solution:

    
    // adding this function above update_mouseHappy, for Pythagorean theorem
    public function distance_to(p1_x:Number,p1_y:Number,
                                p2_x:Number,p2_y:Number):Number {
      var dx:Number = p1_x-p2_x;
      var dy:Number = p1_y-p2_y;
      
      return Math.sqrt(dx*dx+dy*dy);
    }

    // updating/replacing this function
    public function update_mouseHappy():void
    {
      var chaseSpeed:Number = 6.0;

      // if we're less than 1 step from the target, just go to it
      if( distance_to( mouseTarget_X, mouseTarget_Y,
                       mouseBMP.x, mouseBMP.y) < chaseSpeed ) {
        mouseBMP.x = mouseTarget_X;
        mouseBMP.y = mouseTarget_Y;
        return; // exits from this function prematurely
      }
      // without the above check, HappyBall would wiggle near the target
      // due to overshooting it, from one side to the other, every frame
      
      var angleTo:Number;
      
      // because tan(angle) = y/x, 
      // angle = arctan(y/x)
      // atan2(y,x) is common programming lingo for arctan(y/x)
      angleTo = Math.atan2(mouseTarget_Y-mouseBMP.y,
                           mouseTarget_X-mouseBMP.x);
      
      // One way to think about cosine is as a function that returns what
      // percentage of a total magnitude applies to the x axis for a given angle
      mouseBMP.x += Math.cos(angleTo)*chaseSpeed;

      // Likewise, sine can be thought of as the percentage of a total magnitude
      // that applies to the y axis for a given angle
      mouseBMP.y += Math.sin(angleTo)*chaseSpeed;
    }

Normalized Vector solution:

    
    public function update_mouseHappy():void
    {
      var chaseSpeed:Number = 6.0;
      
      // we're doing the distance calculation inside this function
      // since the delta x, delta y, and distance are also needed
      // to compute the normalized vector
      var dx:Number = mouseTarget_X-mouseBMP.x;
      var dy:Number = mouseTarget_Y-mouseBMP.y;
      var distanceTo:Number = Math.sqrt(dx*dx+dy*dy);

      // see the trig version for notes on the following check
      if( distanceTo < chaseSpeed ) {
        mouseBMP.x = mouseTarget_X;
        mouseBMP.y = mouseTarget_Y;
        return;
      }
      
      // in the normalized vector solution, the above check doubles as
      // a safety measure to avoid dividing by 0 in the code that follows
      // if the distance came out to 0, which would have crashed the game
      
      // by dividing the x delta and y delta by the total magnitude
      // we normalize the x and y into a unit vector, which, like
      // in the trig version, can be thought of as percentages of the
      // total magnitude to apply to the x and y axes respectively
      mouseBMP.x += (dx/distanceTo)*chaseSpeed;
      mouseBMP.y += (dy/distanceTo)*chaseSpeed;
    }

The normalized version is less roundabout, but having the angle in the middle step of the trig approach can make it a bit easier to think through, particularly if you're less familiar with vector geometry.

As an aside: trigonometry (especially atan2) and distance checks (sqrt in particular) used to be computationally intensive enough for processors in the 70's-90's that a number of optimizations, approximations, and ways of precomputing them have been worked out over the years. On a modern processor, such optimizations are unlikely to make a noticable difference unless you're doing tons of these calculations every cycle (3D rendering/physics, mutually attracted particle effects, crowd AI, etc.). I mention it here because it's worth knowing (1.) that such optimizations exist, and are only an internet search away, for cases that are so intensive that optimizations will still make a difference (and 2.) those optimizations are probably not worth worrying about unless you need to perform many such calculations on every logic cycle.

Two down, one to go. Just one more movement fix before we revisit how the entire code looks end-to-end, and before I'll provide an updated SWF showing the fixes to all 3 movement types. Next up: keyboard!


Step 5: Fixing Keyboard Arrows

Currently, pressing the arrow keys causes the keyboard controlled HappyBall to move in short bursts. We want the keyboard to act more like a videogame controller, and less like a typing instrument - we're not so much interested in the moment that the keystroke registers, so much as we would like to check during game logic whether the key is currently being pressed.

ActionScript 3 gives us a good way to do just that. New code is in bold this time:

    ...skipping to keyboardSpeed_X definition...
    
    public var keyboardHold_U:Boolean = false;
    public var keyboardHold_D:Boolean = false;
    public var keyboardHold_L:Boolean = false;
    public var keyboardHold_R:Boolean = false;
    public var keyboardSpeed_X:Number = 0.0;
    public var keyboardSpeed_Y:Number = 0.0;

    ...skipping over some code that we don't change for this...
    
    public function GameMotion()
    {
      init_happyballs();

      ourExampleText.text = "Click anywhere";
      addChild(ourExampleText);
      ourExampleText.height = 16;
      
      stage.addEventListener(MouseEvent.CLICK, clickRespond);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyRespond);
      stage.addEventListener(KeyboardEvent.KEY_UP, keyLetGo);
      
      graphics.lineStyle(2.0,0x000000);
      graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight);
      
      constantAction = new Timer(1000/30);
      constantAction.addEventListener(TimerEvent.TIMER, constantUpdate);
      constantAction.start();
    }
    
    ...skipping to update_keyboardHappy...
    
    public function update_keyboardHappy():void
    {
      var keyMoveSpeed:Number = 4.5;
      
      if(keyboardHold_L) { // holding left, set horizontal speed to left
        keyboardSpeed_X = -keyMoveSpeed;
      } else if(keyboardHold_R) { // holding right, set horizontal speed right
        keyboardSpeed_X = keyMoveSpeed;
      } else {
        keyboardSpeed_X *= 0.9; // lose speed
      }

      if(keyboardHold_U) { // holding up, set vert speed to up
        keyboardSpeed_Y = -keyMoveSpeed;
      } else if(keyboardHold_D) { // holding down, set vert speed down
        keyboardSpeed_Y = keyMoveSpeed;
      } else {
        keyboardSpeed_Y *= 0.9; // lose speed
      }
      
      keyboardBMP.x += keyboardSpeed_X;
      keyboardBMP.y += keyboardSpeed_Y;

      keyboardSpeed_Y *= 0.9;
    }
    
    ...skipping down to keyRespond, after randomize_bouncySpeed...
    
    public function keyRespond(event:KeyboardEvent):void
    {
      switch(event.keyCode) {
        case 32:
          randomize_bouncySpeed();
          clickSND.play();
          break;
        case 37:
          keyboardHold_L = true;
          break;
        case 38:
          keyboardHold_U = true;
          break;
        case 39:
          keyboardHold_R = true;
          break;
        case 40:
          keyboardHold_D = true;
          break;
      }
      
      ourExampleText.text = "Key pressed: " + event.keyCode;
    }

    // adding this new function to clear key states when released
    public function keyLetGo(event:KeyboardEvent):void
    {
      switch(event.keyCode) {
        case 37:
          keyboardHold_L = false;
          break;
        case 38:
          keyboardHold_U = false;
          break;
        case 39:
          keyboardHold_R = false;
          break;
        case 40:
          keyboardHold_D = false;
          break;
      }
      
      ourExampleText.text = "Key released: " + event.keyCode;
    }

Conceptually, the change to the code reflects that now we're keeping track of 4 new boolean (on/off or true/false) variables, reflecting whether each of the arrow keys is held. If one of those keys goes down, we set its corresponding variable to true - and when one of those keys goes up, we set its corresponding variable to false. Then, within the same function (update_keyboardHappy) that handles movement each frame for keyboard input, horizontal and vertical speed are set based on the state of those keys.

As a reminder, there is nothing magical about the update_keyboardHappy() function that causes it to be called every frame. It is being called from the constantUpdate() function, which we set up to be called every time the constantAction timer fires 30 times per second. That set up occurred because it was part of the GameMotion() constructor function, the name of which matched our GameMotion game class and filename; there is something magical about the function with a name matching the class/program - it gets called automatically at start up - but everything that's happening after that is only taking place because we explicitly set it up to happen.

Here's the updated SWF so far. The bouncing HappyBall now bounces off its right and bottom edges just as firmly as it does off its left and top edges. The HappyBall chasing the mouse clicks now moves directly to it, rather than doing a 45-degree slide toward an axis first. Lastly, notice our latest change: holding down arrow keys results in smooth, consistent movement for the keyboard controlled HappyBall (remember to click in the window first, or the SWF can't detect key presses!):



Step 6: Polish

Although the three types of movement illustrated are now much nicer than when they started, there are still a few smaller details that we ought to fix:

Here's all the source code, accounting for those new issues in commented sections, including the fixes from the previous steps (I use the normalized vector approach here for the mouse chaser, but there's no harm if you prefer the trig solution):

package {
  import flash.display.Sprite;
  import flash.text.TextField;
  import flash.display.Bitmap;
  import flash.events.MouseEvent;
  import flash.events.KeyboardEvent;
  import flash.media.Sound;
  import flash.utils.Timer;
  import flash.events.TimerEvent;
  
  [SWF(width="500", height="375", backgroundColor="#FFFFFF")]
  public class GameMotion extends Sprite
  {  
    public var ourExampleText:TextField = new TextField();

    [Embed(source="/happyball.png")]
    public var ballImage:Class;
    
    public var bouncyBMP:Bitmap = new ballImage() as Bitmap;
    public var mouseBMP:Bitmap = new ballImage() as Bitmap;
    public var keyboardBMP:Bitmap = new ballImage() as Bitmap;
    
    public var bouncySpeed_X:Number = 0.0;
    public var bouncySpeed_Y:Number = 0.0;
    public var bouncyGravity:Number = 0.6; // to make it a side-view bounce

    public var mouseTarget_X:Number = 0.0;
    public var mouseTarget_Y:Number = 0.0;

    public var keyboardHold_U:Boolean = false;
    public var keyboardHold_D:Boolean = false;
    public var keyboardHold_L:Boolean = false;
    public var keyboardHold_R:Boolean = false;
    public var keyboardSpeed_X:Number = 0.0;
    public var keyboardSpeed_Y:Number = 0.0;

    [Embed(source="/good.mp3")]
    public var clickSound:Class;
    public var clickSND:Sound = new clickSound() as Sound;
    
    public var constantAction:Timer;
    
    public function init_happyballs():void
    {
      bouncyBMP.x = 50;
      bouncyBMP.y = 50;
      addChild(bouncyBMP);

      mouseBMP.x = 80;
      mouseBMP.y = 50;
      addChild(mouseBMP);

      keyboardBMP.x = 110;
      keyboardBMP.y = 50;
      addChild(keyboardBMP);

      bouncySpeed_X = 3.0;
      bouncySpeed_Y = 5.0;
      
      mouseTarget_X = stage.stageWidth*0.5;
      mouseTarget_Y = stage.stageHeight*0.5;
    }

    public function GameMotion()
    {
      init_happyballs();

      ourExampleText.text = "Click anywhere";
      addChild(ourExampleText);
      ourExampleText.height = 16;
      
      stage.addEventListener(MouseEvent.CLICK, clickRespond);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyRespond);
      stage.addEventListener(KeyboardEvent.KEY_UP, keyLetGo);
      
      graphics.lineStyle(2.0,0x000000);
      graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight);
      
      constantAction = new Timer(1000/30);
      constantAction.addEventListener(TimerEvent.TIMER, constantUpdate);
      constantAction.start();
    }
    
    public function update_bouncyHappy():void
    {
      if(bouncyBMP.x > stage.stageWidth-bouncyBMP.width) {
        bouncySpeed_X = -bouncySpeed_X;
      }
      if(bouncyBMP.x < 0) {
        bouncySpeed_X = -bouncySpeed_X;
      }
      
      if(bouncyBMP.y > stage.stageHeight-bouncyBMP.height) {
        if(bouncySpeed_Y > 0.0) { // moving downward?
          bouncySpeed_Y = -bouncySpeed_Y;
        } else { // it's underground. give it a new random position
          bouncyBMP.x = Math.random()*(stage.stageWidth-bouncyBMP.width);
          bouncyBMP.y = Math.random()*(stage.stageHeight-bouncyBMP.height);
        }
      }
      if(bouncyBMP.y < 0) {
        bouncySpeed_Y = -bouncySpeed_Y;
      }

      // change to the speed that changes the position
      // that's acceleration!
      bouncySpeed_Y += bouncyGravity;

      bouncyBMP.x += bouncySpeed_X;
      bouncyBMP.y += bouncySpeed_Y;
    }

    public function update_keyboardHappy():void
    {
      var keyMoveSpeed:Number = 4.5;
      
      if(keyboardHold_L) {
        keyboardSpeed_X = -keyMoveSpeed;
      } else if(keyboardHold_R) {
        keyboardSpeed_X = keyMoveSpeed;
      } else {
        keyboardSpeed_X *= 0.9;
      }

      if(keyboardHold_U) {
        keyboardSpeed_Y = -keyMoveSpeed;
      } else if(keyboardHold_D) {
        keyboardSpeed_Y = keyMoveSpeed;
      } else {
        keyboardSpeed_Y *= 0.9;
      }
      
      // if our total speed is greater than our max speed, slow it down
      // this happens if we hold both a horizontal and a vertical direction
      if( keyboardSpeed_X*keyboardSpeed_X + 
          keyboardSpeed_Y*keyboardSpeed_Y > keyMoveSpeed*keyMoveSpeed ) {
        var length:Number = Math.sqrt(keyboardSpeed_X*keyboardSpeed_X + 
                                      keyboardSpeed_Y*keyboardSpeed_Y);

        keyboardSpeed_X = (keyboardSpeed_X/length)*keyMoveSpeed;
        keyboardSpeed_Y = (keyboardSpeed_Y/length)*keyMoveSpeed;
      }
      
      keyboardBMP.x += keyboardSpeed_X;
      keyboardBMP.y += keyboardSpeed_Y;
      
      keyboardSpeed_Y *= 0.9;

      // keep the keyboard controlled HappyBall in the play area
      if(keyboardBMP.x < 0) {
        keyboardBMP.x = 0;
      }
      if(keyboardBMP.x >= stage.stageWidth-keyboardBMP.width) {
        keyboardBMP.x = stage.stageWidth-keyboardBMP.width;
      }
      if(keyboardBMP.y < 0) {
        keyboardBMP.y = 0;
      }
      if(keyboardBMP.y >= stage.stageHeight-keyboardBMP.height) {
        keyboardBMP.y = stage.stageHeight-keyboardBMP.height;
      }
    }
    
    public function update_mouseHappy():void
    {
      var chaseSpeed:Number = 6.0;
      
      var dx:Number = mouseTarget_X-mouseBMP.x;
      var dy:Number = mouseTarget_Y-mouseBMP.y;
      var distanceTo:Number = Math.sqrt(dx*dx+dy*dy);

      if( distanceTo < chaseSpeed ) {
        mouseBMP.x = mouseTarget_X;
        mouseBMP.y = mouseTarget_Y;
        return;
      }
      
      mouseBMP.x += (dx/distanceTo)*chaseSpeed;
      mouseBMP.y += (dy/distanceTo)*chaseSpeed;
    }
    
    public function constantUpdate(evt:TimerEvent):void
    {
      update_bouncyHappy();
      update_keyboardHappy();
      update_mouseHappy();
    }
    
    public function clickRespond(e:MouseEvent):void
    {
      if( mouseX < 0 || mouseX >= stage.stageWidth ||
          mouseY < 0 || mouseY >= stage.stageHeight ) {
        return; // ignore this click; it happened outside the game area
      }
    
      // offset by half width and half height, to center target
      mouseTarget_X = mouseX - mouseBMP.width*0.5;
      mouseTarget_Y = mouseY - mouseBMP.height*0.5;
      
      ourExampleText.text = "Click, use arrows";

      clickSND.play();
    }
    
    public function randomize_bouncySpeed():void {
      bouncySpeed_X = Math.random()*20.0-10;
      bouncySpeed_Y = Math.random()*20.0-10;
    }

    public function keyRespond(event:KeyboardEvent):void
    {
      switch(event.keyCode) {
        case 32:
          randomize_bouncySpeed();
          clickSND.play();
          break;
        case 37:
          keyboardHold_L = true;
          break;
        case 38:
          keyboardHold_U = true;
          break;
        case 39:
          keyboardHold_R = true;
          break;
        case 40:
          keyboardHold_D = true;
          break;
      }
      
      ourExampleText.text = "Key pressed: " + event.keyCode;
    }

    public function keyLetGo(event:KeyboardEvent):void
    {
      switch(event.keyCode) {
        case 37:
          keyboardHold_L = false;
          break;
        case 38:
          keyboardHold_U = false;
          break;
        case 39:
          keyboardHold_R = false;
          break;
        case 40:
          keyboardHold_D = false;
          break;
      }
      
      ourExampleText.text = "Key released: " + event.keyCode;
    }

  }
}

Booya!


Step 7: Extension Challenge

Exercises to try, starting from the above code, that will turn this into a simple 2 player game:

  1. Add detection for when HappyBalls overlap. Play a sound when this happens, and/or have their motion affected by the collision.
  2. If the bouncing HappyBall touches the mouse controlled HappyBall, change the corner text to "Keyboard wins!", but if the mouse controlled HappyBall gets close enough to the keyboard controlled HappyBall, change that text to "Mouse wins!" (Remove the key press and other updates to that text, so that the win/loss text stands out.)
  3. Create different icons for each character, instead of using the same tiny yellow smiley for each. If you're feeling lazy or don't like making art, recolor that face to make a red one and a green one. The goal: adjust the code to use 3 different bitmaps.
  4. Add another text object that shows the current score, Keyboard vs Mouse. Freeze gameplay after each round ends until Spacebar is pressed (indicate the need to press Spacebar!), then reset everything except the score for a new match.

If you run into questions that you're having trouble working through, let me know, and I'll be happy to help clarify, provide examples, or point you in the right direction.


What We've Done

By following these past 3 tutorials, you have:

  1. Downloaded and set up the free compiler
  2. Loaded and used art/audio/input/timers
  3. Developed smooth, 2D game movements
  4. (Optionally) Created a simple 2 player game

My friend and former coworker Nate Yun recommends the free ActionScript 3 IDE FlashDevelop, which can be handy for dealing with larger, multi-file projects - I used it for my experimental gameplay projects, and it stands up quite well to Adobe's pricey Flex/Flash Builder IDE.

If you're interested in learning about libraries written by other videogame developers that can help handle animations and other common issues in a way conducive to videogame development (build upon what others have spent time solving!), see my recent post about the Flash Game Dojo.



Written by Chris DeLeon

Part of his resources for
HobbyGameDev.com

If you liked this, consider checking out my other text resources at HobbyGameDev.com.
All the information there is free. I don't run ads on the site, I'm just into helping people.