8bitrocket.com
30Oct/070

Actionscript 3: Tutorial – BitmapData rotation with a matrix

The bitmapData class has been a revelation for advanced Flash game programmers. With it, one can replicate the sprite sheet magic that 8-bit game wizards produced on machines such as the Atari 800, C=64 and Nintendo NES. I have covered the use of sprite sheets and BitmapData in a previous tutorial: Flash CS3: Actionscript 3 (AS3) Game Primer #1: Tile Maps, XML, and bitmapData. In this lesson, we will explore a technique for manipulating the Bitmap Data object through the use of some standard matrix operations.

I have been building a new game called Pumpkin Man. It is an AS3 tile-based, Pacman variant. I have been putting special emphasis on the game display engine to ensure future that AS3 games I create can make use of it. I have created an engine that uses one display object (a sprite) to display the entire game. This sprite contains one Bitmap object, and one BitmapData object (inside the Bitmap). The tiles, the background, the power ups and and all game characters are redrawn each frame into this one Bitmap Data object. This eliminates the need for the rendering engine to constantly update the math necessary to move vectors, and the movie has just one redraw region. The result (so far) is a fast, slick display engine. The problem is that many of the easy things you can do with MovieClips, such as rotation around the center point, scaling, etc are not as easy to accomplish because the game characters are not drawn within their own display objects. What follows ia an example of how I solved one such problem: Rotation around a center point.

Rotating a game character around the mid point.
When Pumpkin Man, the main character in the game dies, I wanted him to spin around his mid point and quickly scale down. It gives the slight impression of being flushed down the toilet. I could have easily done this with standard display object manipulations. Here are the steps I would have taken if the game character was inside a Sprite:

1. Make sure the Bitmap holder for the character was centered at -1/2 width for x and -1/2 height for the y value. This x and y position is relative to the local coordinates of the Sprite holder for the Bitmap. This puts the Bitmap holder's center point at the origin of the Sprite.
2. Rotate the Sprite and scale in an EnterFrame or timer operation to create the illusion of rotation and scaling down.

But, in my case, I only had the actual BitmapData to use, and no individual Sprite and Bitmap holders to manipulate. I didn't create a rotated sprite sheet version of each rotation frame - which would have made this easier, but also eats up valuable bandwidth and ram. I had one frame on BitmapData to use. The solution is to use the Matrix class to transform the BitmapData on each frame update. The Matrix.rotate() method takes an angle in radians as its one parameter. Since a BitmapData object's origin point is set to 0,0 always, and can't be changed though a property update method, you must also use a Matrix.translate() function call (actually) two of them to create the illusion of rotation around the character's center point.

First we need to create a generic main document class to hold our code.

 

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

package {

import flash.geom.*;
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.events.*;

public class Main extends MovieClip {

public function Main() {
trace("main");
}

} // end class

} // end package
[/cc]

This is the basic structure for the class. It is going to be the document class for our .fla, so you can set "Main" as the Document class in the document properties window
Imports:
1. import flash.geom.*; - to do the matrix transformations and create Point and Rectangle instances.
2. import flash.display.BitmapData; - for the BitmapData object that we are going to use to display our character.
3. import flash.display.MovieClip; - MovieClip (or Sprite) because this is the Document class of a MovieClip;
4. import flash.events.*; - For our EnterFrame event.

I have placed a 32x32 Bitmap in the library that will be out character to rotate and scale. Its linkage name is set to "character". Our next step will be to create the necessary variables to store and manipulate this character on the screen. The following code updates the previous code and ads some new variables and functions:

 

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

package {

import flash.geom.*;
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.events.*;

public class Main extends MovieClip {
private var screenBD:BitmapData;
private var backgroundBD:BitmapData;
private var screenB:Bitmap;
private var charObj:Object=new Object();

public function Main() {
createScreen();
createCharacter();
renderCharacter();
}

private function createScreen():void {
backgroundBD = new BitmapData(300,300,false,0x000000);

screenBD=new BitmapData(300,300,false,0x000000);
screenB=new Bitmap(screenBD);
addChild(screenB);
}

private function createCharacter():void {
charObj.sourceBD=new character(32,32);
charObj.displayBD=new BitmapData(32,32,true,0xffffffff);
charObj.displayBD.copyPixels(charObj.sourceBD,new Rectangle(0,0,32,32),new Point(0,0));
charObj.x=50;
charObj.y=50;
charObj.rect=new Rectangle(0,0,32,32)
charObj.point=new Point(charObj.x,charObj.y);
}

private function renderCharacter() {
screenBD.copyPixels(charObj.displayBD,charObj.rect, charObj.point);
}

} // end class

} // end package
[/cc]

We now have declared these variables:
1. private var screenBD:BitmapData; - This will be the bitmap data holder for our screen. This is a container for all of the objects we will add to the screen. Since it is a BitmapData object, we must either draw() or copyPixels() from displayObjects such as BitmapData to update this screen.
2. private var backgroundBD:BitmapData; - This will be used as an "eraser" between frame draws. We need to ensure there is no ghosting of pixels or trails left behind as we rotate and scale.
3. private var screenB:Bitmap; -This is the container to be added to the main movie. It will contain the screenBD.
4. private var charObj:Object=new Object(); - This is a generic object that will hold an instance of our character class. If this was a full blown game, we would make this a real class, but in this instance, we will make use of the simple generic Object class. If you have ever programmed in C, this is much like a struct. It will hold structured data about our object, but will not have and accessor or other methods (because it can't).

Next we have our main function. This currently just calls more functions to set up the screen, character, and finally render the character.

The createScreen() method simply creates a 300 x 300 Bitmapdata object with transparency set to false, and black as it's fill color. We also call the addChild() of our main MovieClip to display it on the screen. It also created the backgroundBD that will be used as an eraser. This is because once we start drawing on the current screenBD, we will be destroying all of the current black pixels and replacing them with colored pixels. These back pixels will never come back unless we redraw them. The backgroundBD is used for that purpose.

The createCharacter() is a little more involved.
1. charObj.sourceBD=new character(32,32); - This creates a sourceBD (Bitmapdata) instance from the character in the library. We must pass the size of the source image into the the constructor for a library object with flash.display.BitmapData set as the Base Class. This makes it a simple task to pull items from the library and create instances of them, especially BitmapData objects.
2. charObj.displayBD.copyPixels(charObj.sourceBD,new Rectangle(0,0,32,32),new Point(0,0)); - We copy all of the pixels from the sourceBD to the displayBD. We do this because the matrix operations we will do are "destructive", meaning that they change the actual BitmapData of the object and cannot be easily reversed. We will do these operations on the "cloned" copy called displayBD and leave the sourceBD intact. We need to use this method because if we just assigned one instance to the other with the "=" operator we would have two instances that are pointers to or references of the exact same data. Matrix operation changes would then be destructive to both and not just the displayBD. Believe me, I learned this the hard way.
3. Next we set and x and y property of the object both to 50. This is simply the upper left-hand corner point where the character will be drawn into the screenBD.
4. The rect is an instance of the Rectangle class and it holds the clip area of the BitmapData to copy to the screenBD. Since we want the entire character to be copied to the screenBD, we set the start point at 0,0, and the end point at 32,32.
5. The point variable constructs an actual Point class instance from the x and y values created above.

Then renderCharacter() method is currently deceptively simple:
screenBD.copyPixels(charObj.displayBD,charObj.rect, charObj.point); It is pretty powerful for one line. Basically this tells the screenBD to copy all of the pixels from the charObj's displayBD instance to itself. It uses the rect as the clip area to copy and the point as the location on the screen to place the pixels it copies.

Next we add in code to loop and rotate the character.

 

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

package {

import flash.geom.*;
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.events.*;

public class Main extends MovieClip {
private var screenBD:BitmapData;
private var backgroundBD:BitmapData;
private var screenB:Bitmap;
private var charObj:Object=new Object();

public function Main() {
createScreen();
createCharacter();
addEventListener(Event.ENTER_FRAME, run);
}

private function run(e:Event):void {
//trace ("running");
updateCharacter();
renderBackground();
renderCharacter();
}

private function createScreen():void {
backgroundBD = new BitmapData(300,300,false,0x000000);
screenBD=new BitmapData(300,300,false,0x000000);
screenB=new Bitmap(screenBD);
addChild(screenB);
}

private function createCharacter():void {
charObj.sourceBD=new character(32,32);
charObj.displayBD=new BitmapData(32,32,true,0xffffffff);
charObj.displayBD.copyPixels(charObj.sourceBD,new Rectangle(0,0,32,32),new Point(0,0));
charObj.x=50;
charObj.y=50;
charObj.rect=new Rectangle(0,0,32,32)
charObj.point=new Point(charObj.x,charObj.y);
charObj.rotation=0;
}

private function updateCharacter():void {
charObj.rotation+=18;
if (charObj.rotation <361) {
var degrees:int=charObj.rotation;
var angle_in_radians = Math.PI * 2 * (charObj.rotation / 360);
var rotationMatrix:Matrix = new Matrix();
rotationMatrix.translate(-16,-16);
rotationMatrix.rotate(angle_in_radians);
rotationMatrix.translate(16,16);
var matrixImage:BitmapData = new BitmapData(32, 32, true, 0x00000000);
matrixImage.draw(charObj.sourceBD, rotationMatrix);
charObj.displayBD=matrixImage;

}
}

private function renderBackground():void{
//trace("render background");
screenBD.copyPixels(backgroundBD,new Rectangle(0,0,300,300), new Point(0,0));
}

private function renderCharacter() {
//trace("render charcter");
screenBD.copyPixels(charObj.displayBD,charObj.rect,charObj.point);
}

} // end class

} // end package
[/cc]

We have added in a lot of code here. First I will explain all of it, then you will see a demo of it in action and be able to download a .fla with a working example.

In the Main() method, we added this line: addEventListener(Event.ENTER_FRAME, run); We now have an event that will fire off each frame and will call our run() function;

The run(e:event) function repeatedly calls these methods: updateCharacter(); renderBackground(); renderCharacter(); We already have discussed the unchanged renderCharacter() function. The other two will discussed in detail.

The updateCharacter() method is used to update the BitmapData for the character before the renderCharacter() method is called. What we want to do in this example is rotate the character 18 degrees on each frame and have the rotation stop after on revolution.
charObj.rotation+=18; This simply adds 18 to the number we are using to hold the rotational degree for the character.
if (charObj.rotation <361) { - This starts out iteration and will not continue once the degree value has reached 360;
var degrees:int=charObj.rotation; Here we create an int to hold the current rotation value. I do this just in case there is a data type problem with the generic object. By creating an int, I am sure the data held in the variable will be the correct type);
var angle_in_radians = Math.PI * 2 * (charObj.rotation / 360); - We need to translate degrees into radians because the Matrix class uses radians in angle calculations.
var rotationMatrix:Matrix = new Matrix(); - We simply create a new Matrix for the rotation.
rotationMatrix.translate(-16,-16); - Here we use the translate method of the Matrix class to move the matrix to -16, -16. This is equivalent to setting it's origin point to -16, -16. We are using 16 here because the width and height of out character is 32.
rotationMatrix.rotate(angle_in_radians); - We call the rotate() method of the matrix and pass in the radians of our degree angle. This will cause the rotation to be calculated on the current translated position.
rotationMatrix.translate(16,16); - We move the matrix back to 0,0 be adding 16 to the x and the y (tx and ty in the matrix). This works because all Matrix operations are additive. We could never just move it to a position without knowing its current position. Since it started at 0,0, was moved to -16,16, we need t0 just add 16 to the tx and 16 to the ty of the matrix to put it back at origin 0,0.
var matrixImage:BitmapData = new BitmapData(32, 32, true, 0x00000000); - We create a new Bitmapdata object to hold the result of our matrix operation on the charObj.sourceBD operation.
matrixImage.draw(charObj.sourceBD, rotationMatrix, null, null, null, true);- We draw into our clean, new matrixImage the sourceBD with the matrix applied. We set the final option of Pixel Smoothing to true so our rotated bitmap looks as close to a rotated vector as possible.
charObj.displayBD=matrixImage; - Finally, we have our displayBD reference the new matrixImage.

We have one last function called renderBackgroud(): Its basic job is to wipe clean the screenBD by copying the background to it on every frame before the character is drawn:
screenBD.copyPixels(backgroundBD,new Rectangle(0,0,300,300), new Point(0,0));

bd_rotate.zip - Example .fla

   
This site is protected by Comment SPAM Wiper.