8bitrocket.com
14Apr/100

Tutorial: Exploring the AS3 Bitmap Class Lesson 2 – Rotation with and without a Matrix

Tutorial: Exploring the AS3 Bitmap Class Lesson 2 - Rotation and with and without a Matrix.

In this second lesson we will explore a couple different methods for rotating the Bitmap object from Lesson 1. We will first try simply using Bitmap.rotation property. Next we will do the same operation using a Matrix and finally we will add the dynamic tile-sheet animation to the Matrix rotation. Lesson 2 will be comprised on 3 separate parts (Parts 4,5,and 6 in this series).

We will be using the dynamic tile sheet we created in Lesson 1, so is you have not familiarized yourself with that lesson and find yourself getting lost it would be a good idea to go back and take a look.

Part 4: Rotating a Bitmap object directly

The Bitmap class (just like all display objects) has it's own rotation property. You would think that this would make rotating the contents of the object a trivial matter. In a sense, it is. You just simply set the rotation property to a new value

The problem you will see is that we cannot rotate the BitmapData object inside the Bitmap from any point other than the top-left corner. We will be adding a single line of code to the final code from Part 3 (inside the runGame) function:

[cc lang="javascript" width="550"]
//normal rotation will not work as it rotates on the top left corner
bitmapToDisplay.rotation += 10;

[/cc]

This code will simply rotate the Bitmap by 10 degrees on each frame tick. We will also be moving the bitmapToDisplay to a new position (84x,84y) so the object won't clip off the viewable screen during the rotation. We turn "smoothing" on to help the bitmap rotation not look jagged as the pixel locations are recalculated. This will be added to the init() method.

[cc lang="javascript" width="550"]bitmapToDisplay.smoothing = true;[/cc]

Here is the complete code for the "Part4" class. The new line of code is added to the end of the code in the Part3 runGame function.

[cc lang="javascript" width="550"]

package {
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Point;
import flash.geom.Rectangle;

/**
* ...
* @author Jeff Fulton
*/
public class Part4 extends Sprite {

private var bitmapToDisplay:Bitmap;
private var bitmapDataToDisplay:BitmapData;

private var bitmapScrollRect:Rectangle;
private var animationDelay:int = 5;
private var animationCount:int = 0;

public function Part4():void {
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}

private function init(e:Event = null):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
bitmapDataToDisplay = new BitmapData(64, 32, true, 0x00000000);

bitmapToDisplay = new Bitmap(bitmapDataToDisplay);
bitmapToDisplay.x = 84;
bitmapToDisplay.y = 84;
addChild(bitmapToDisplay);

createBitmapData();
//makes it look better when rotates
bitmapToDisplay.smoothing = true;

bitmapScrollRect=new Rectangle(0, 0, 32, 32);
bitmapToDisplay.scrollRect = bitmapScrollRect;
addEventListener(Event.ENTER_FRAME, runGame,false,0,true);
}

private function createBitmapData():void {
//draw vertical line with setPixel32
for (var ctr:int = 4; ctr <= 27; ctr++) {
bitmapDataToDisplay.setPixel32(15, ctr, 0xff0066ff);
bitmapDataToDisplay.setPixel32(16, ctr, 0xff0066ff);
bitmapDataToDisplay.setPixel32(ctr, 26, 0xff0066ff);
bitmapDataToDisplay.setPixel32(ctr, 27, 0xff0066ff);
}

//copy ship to next tile
bitmapDataToDisplay.copyPixels(bitmapDataToDisplay, _
new Rectangle(0, 0, 32, 32), new Point(32, 0));

//add red thruster under the ship for this frame
for (ctr= 29; ctr <= 31; ctr++) {
bitmapDataToDisplay.setPixel32(47, ctr, 0xffff0000);
bitmapDataToDisplay.setPixel32(48, ctr, 0xffff0000);
}

}

private function runGame(e:Event):void {

animationCount++;
if (animationCount == animationDelay) {
animationCount = 0;
if (bitmapScrollRect.x == 0) {
bitmapScrollRect.x = 32;
}else {
bitmapScrollRect.x = 0;
}
bitmapToDisplay.scrollRect = bitmapScrollRect;
}

//normal rotation will not work as it rotates on the top left corner
bitmapToDisplay.rotation += 10;
}

}

}

[/cc]

Notice the long line under the //copy ship to next tile comment. This line has been broken with an "_" and should be reattached if you are going to cut and paste the code.

Here is the working swf file:

As you can see, if you want to simulate a space ship wildly spinning out of control this might be a good start but it isn't good for a rotation around the internal center point of the Bitmap. I am sure there are a few good applications where simulating the rotation from the top left point might work, but not for an Asteroids style ship rotation.

Part 5: Rotating the Bitmap on the Fly with a Matrix

In part 4 we rotated our Bitmap by updating it's standard rotation property. What we saw was a Bitmap that rotated around the top-left corner. Now, my normal method to get around this is to place the Bitmap inside a Sprite object and set its local coordinates to be -.5*width for x and -.5*height for y. When the Sprite is rotated, the Bitmap will rotate around it's center point. The one problem with this technique is that the actual top-left edge of the viewable Bitmaps now not at 0,0. This means you must offset some math calculations when using these objects (especially in tile-based games). Luckily, this is not the only way to accomplish this task. We can also use a combination of translation and rotation with Matrix object to simulate the same thing.

We will use the standard Matrix class, not the fl.motion.MatrixTransform class as the fl libraries are not part of the Flex SDK.

First let's look at the three new class level variables we will need for this operation:

[cc lang="javascript" width="550"]
private var bitmapRotation:int = 0;
private var bitmapRotationMatrix:Matrix;
private var startLocation:Point = new Point(84, 84);

[/cc]

The bitmapRotation variable will hold the current angle of rotation for the ship. It will range from 0-359 and will be used in the Matrix calculation to position the ship correctly on the screen. The bitmapRotationMatrix is a class level Matrix object instance that will be used to calculate and apply the center point rotation for the ship. We use a class level variable to avoid the overhead of creating a new Matrix object on each frame tick. The startLocation variable will be used as the upper left-hand corner location for the ship. We need to keep this in a separate variable from the Bitmap x and y coordinates as those will change based on the Matrix translation and rotation.

We will be instantiating this our Matrix in the init() function the Part5 class file.

[cc lang="javascript" width="550"]bitmapRotationMatrix= new Matrix();[/cc]

In our runGame function we will be commenting out the code that that does the animation with the scrollRect between the two dynamically created tile sheet sprite frames. We will add in the code that does the Matrix rotation of our ship. We will add the animation back in part 6, but for now we want to just demonstrate the rotation with out any other animation.

[cc lang="javascript" width="550"]
bitmapRotation += 1;
if (bitmapRotation > 359){
bitmapRotation = 0;
}

var angleInRadians:Number = Math.PI * 2 * (bitmapRotation / 360);

bitmapRotationMatrix.identity(); //resets the matrix

bitmapRotationMatrix.translate(-16,-16);
bitmapRotationMatrix.rotate(angleInRadians);
bitmapRotationMatrix.translate(startLocation.x+16, startLocation.y+16);

bitmapToDisplay.transform.matrix = bitmapRotationMatrix;

[/cc]

Let's step through this code:

1. First we update our bitmapRotation variable by 1 and check to see if it is greater than 359. If it is we set it back to 0. Now, this is not explicitly needed for this example as rotation will work without it. I use this because calculations with angles and especially look-up tables with vector calculations for angled movement are much easier to use with 0-359 as the angle of rotation.

2. We create a local Number variable called angleInRadians from the bitmapRotation value because the Matrix operation expects the angle to be represented in his manner.

3. We call the identity() method of the Matrix class. This resets the Matrix and allows us to feed new values into it on each frame tick. For some fun and weird effects, remove it and what what happens.

4. Now we get to the actual Matrix calculations. These function a little like a programming language all to itself.

First we call the translate() function and pass it -.5*with, -.5* height for the object. I just stuck 16 in here because it is a faster calculation. but you can calculate this on the fly with the width and height properties of the bitmapToDisplay object. This forces the rotation operation to occur at the center of the object rather than the upper left-hand corner.

Next we pass the newly calculated angleInRadians value into the rotate() method of the Matrix class. This will rotate the bitmapToDisplay object on the current x,y point, which is the center because of the translate() call above.

Finally, we must move the bitmapToDisplayObject back to its original position. We do this by using the startLocation Point instance's x and y coordinates (84,84) and adding the .5*width and .5*height to the respective startLocation Point values. Again, I applied the actual value (16) for speed of operation, but these very well could be calculated with the properties of the bitmapToDisplay object.

Here is the complete code for part 5:

[cc lang="javascript" width="550"]
package {
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.geom.Transform

/**
* ...
* @author Jeff Fulton
*/
public class Part5 extends Sprite {

private var bitmapToDisplay:Bitmap;
private var bitmapDataToDisplay:BitmapData;

private var bitmapScrollRect:Rectangle;
private var animationDelay:int = 5;
private var animationCount:int = 0;

private var bitmapRotation:int = 0;
private var bitmapRotationMatrix:Matrix;
private var startLocation:Point = new Point(84, 84);

public function Part5():void {
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}

private function init(e:Event = null):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
bitmapDataToDisplay = new BitmapData(64, 32, true, 0x00000000);

bitmapToDisplay = new Bitmap(bitmapDataToDisplay);
bitmapToDisplay.x = startLocation.x;
bitmapToDisplay.y = startLocation.y;
addChild(bitmapToDisplay);

createBitmapData();

bitmapRotationMatrix= new Matrix();
bitmapScrollRect=new Rectangle(0, 0, 32, 32);
bitmapToDisplay.scrollRect = bitmapScrollRect;

bitmapToDisplay.smoothing = true; //makes it look better when rotates

addEventListener(Event.ENTER_FRAME, runGame,false,0,true);
}

private function createBitmapData():void {
//draw vertical line with setPixel32
for (var ctr:int = 4; ctr <= 27; ctr++) {
bitmapDataToDisplay.setPixel32(15, ctr, 0xff0066ff);
bitmapDataToDisplay.setPixel32(16, ctr, 0xff0066ff);
bitmapDataToDisplay.setPixel32(ctr, 26, 0xff0066ff);
bitmapDataToDisplay.setPixel32(ctr, 27, 0xff0066ff);
}

//copy ship to next tile
bitmapDataToDisplay.copyPixels(bitmapDataToDisplay, _
new Rectangle(0, 0, 32, 32), new Point(32, 0));

//add red thruster under the ship for this frame
for (ctr= 29; ctr <= 31; ctr++) {
bitmapDataToDisplay.setPixel32(47, ctr, 0xffff0000);
bitmapDataToDisplay.setPixel32(48, ctr, 0xffff0000);
}

}

private function runGame(e:Event):void {

/*
animationCount++;
if (animationCount == animationDelay) {
animationCount = 0;
if (bitmapScrollRect.x == 0) {
bitmapScrollRect.x = 32;
}else {
bitmapScrollRect.x = 0;
}
bitmapToDisplay.scrollRect = bitmapScrollRect;
}
*/

//rotate with a matrix
bitmapRotation += 1;
if (bitmapRotation > 359){
bitmapRotation = 0;
}

var angleInRadians:Number = Math.PI * 2 * (bitmapRotation / 360);

bitmapRotationMatrix.identity(); //resets the matrix

bitmapRotationMatrix.translate(-16,-16);
bitmapRotationMatrix.rotate(angleInRadians);
bitmapRotationMatrix.translate(startLocation.x+16, startLocation.y+16);

bitmapToDisplay.transform.matrix = bitmapRotationMatrix;

}

}

}

[/cc]

Notice the long line under the //copy ship to next tile comment. This line has been broken with an "_" and should be reattached if you are going to cut and paste the code.

Here is the working swf file:

Part 6: Adding the dynamic tile sheet sprite animation back into the mix

For this section we will simply be adding the animation with the scrollRect of the bitmapToDisplay back into the code. It is currently commented out in the runGame() function from part 5:

[cc lang="javascript" width="550"]

animationCount++;
if (animationCount == animationDelay) {
animationCount = 0;
if (bitmapScrollRect.x == 0) {
bitmapScrollRect.x = 32;
}else {
bitmapScrollRect.x = 0;
}
bitmapToDisplay.scrollRect = bitmapScrollRect;
}
}

[/cc]

For a refresher, here is the explanation from lesson 1 part 3.

When then animationCount == animationDelay we will change the x value of the bitmapScrollRect. The value is either 0 (the start x of the first tile) or 32 (the start x of the second tile). It just jumps back and forth between these two values every 5 frames. This isn't the most sophisticated animation technique, but it works fine for this example.

Notice that if there is a change to the bitmapScrollRect, we have to re-apply it to the bitmapToDisplay.scrollRect. The scrollRect property of a display object cannot be changed by reference, it must be re-applied as needed.

All of the above code will be inside a simple runGame function that we will create. This function is called on each frame by a simple EnterFrame event.

Simpily uncomment those lines from the Part5.as class file and you will have the animation back in to the code.

Here is the working swf file:

As you can see in the above example, we can simulate animation by changing the x value of the scrollRect property of a display object and at the same time we can rotate the Bitmap object using the Matrix operations.

In the next lesson we will cover scale transformations on our bitmapToDisplay object.

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 (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.

This site is protected by Comment SPAM Wiper.