In the first part of this series, we covered the very basics of Box2D, by adding some simple circular objects and rendering them using graphics
calls. In this part, not only will we use actual images to represent our objects, we'll learn how to use an entirely different shape, too!
I recommend reading this series from the start, but if you want to start here, then grab this zip file -- it contains the source code we wrote in the first part. (The Download button above contains the source code from the end of this part.)
Step 1: Put This PNG in Your Project
We'll start by replacing the plain circles that we've been using with something else. Right-click this PNG to download it to your hard drive:
(There'll be plenty of time to draw your own designs later on, but for now, just grab this one.)
Add it to your project files.
In Flash Pro, this means importing it to your Library, and giving it a Class Name (pick SimpleWheelBitmapData
). In FlashDevelop, this means putting it in your library folder, and embedding it into your Main
class using code like so:
[Embed(source='../lib/SimpleWheel.png')] public var SimpleWheel:Class;
Do whatever's relevant for your workflow. I'm creating a Pure AS3 Project in FlashDevelop, remember, so I'll be writing in that context.
Step 2: Create an Array to Hold the Wheel Images
For each wheel that we create in the Box2D world, we'll need to create an instance of SimpleWheel
and put it in the display list. To access all these wheels, we can use another array -- create one, and call it wheelImages[]
:
public var wheelImages:Array;
Make sure you initialize it near the top of getStarted()
, as well:
private function getStarted():void { var gravity:b2Vec2 = new b2Vec2(0, 10); world = new b2World(gravity, true); wheelArray = new Array(); wheelImages = new Array();
Step 3: Initialize the Wheel Images
The most sensible place to initialize the images of the wheels is in createWheel()
, since we have all the arguments we need.
Add these lines to the end of that function:
wheelArray.push(wheelBody); var wheelImage:Bitmap = new SimpleWheel(); wheelImage.width = radius * 2; wheelImage.height = radius * 2; wheelImage.x = startX; wheelImage.y = startY; this.addChild(wheelImage); wheelImages.push(wheelImage); }
Watch out -- you need to import flash.display.Bitmap
as well, since the PNG we embedded is a Bitmap
.
If you're using Flash Pro, then you don't have a SimpleWheel
class, so instead of typing new SimpleWheel()
, you must do this:
wheelArray.push(wheelBody); var wheelImage:Bitmap = new Bitmap(new SimpleWheelBitmapData()); wheelImage.width = radius * 2; wheelImage.height = radius * 2; wheelImage.x = startX; wheelImage.y = startY; this.addChild(wheelImage); wheelImages.push(wheelImage); }
(I'm going to continue using new SimpleWheel()
throughout this tutorial, for simplicity. Just remember you have to change it if you're using Flash Pro.)
You can try this out already! The wheels won't move, but they should be on the screen (after all, they're in the display list). Here's mine (click to play):
Oh. Um. Where are they?
Step 3: Notions of Notations
The issue here is that scaleFactor
again. Remember: Box2D takes measurements in meters, while Flash takes measurements in pixels. The radius
parameter is in meters, while the startX
and startY
parameters are in pixels; we're not applying a scale factor to either, you'll notice, so we're not seeing the results we expect. (In this case, the image is being made so tall and so wide that all you can see is its top-left corner -- which is full of transparent pixels.)
We could decide to standardize on one or the other measurement systems, e.g. to specify all measurements in meters from now on, so that we know that we always have to multiply by the scale factor for any Flash functions that require pixels... but, I'll be honest, I'm going to find this a pain. Instead, I suggest we use a form of Hungarian notation (the good kind) to stop us getting confused.
It's a simple idea: we just give all our variables that are measured in meters names beginning with m
, and all our variables that are measured in pixels names beginning with px
(you can pick different prefixes if you use m
and px
to mean different things in your code). Check it out:
private function createWheel(mRadius:Number, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var wheelBodyDef:b2BodyDef = new b2BodyDef(); wheelBodyDef.type = b2Body.b2_dynamicBody; wheelBodyDef.position.Set(pxStartX / scaleFactor, pxStartY / scaleFactor); var wheelBody:b2Body = world.CreateBody(wheelBodyDef); var circleShape:b2CircleShape = new b2CircleShape(mRadius); var wheelFixtureDef:b2FixtureDef = new b2FixtureDef(); wheelFixtureDef.shape = circleShape; wheelFixtureDef.restitution = (Math.random() * 0.5) + 0.5; wheelFixtureDef.friction = (Math.random() * 1.0); wheelFixtureDef.density = Math.random() * 20; var wheelFixture:b2Fixture = wheelBody.CreateFixture(wheelFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); wheelBody.SetLinearVelocity(startingVelocity); wheelArray.push(wheelBody); var wheelImage:Bitmap = new SimpleWheel(); wheelImage.width = mRadius * 2; wheelImage.height = mRadius * 2; wheelImage.x = pxStartX; wheelImage.y = pxStartY; this.addChild(wheelImage); wheelImages.push(wheelImage); }
See? Now we can instantly tell that wheelImage.width = mRadius * 2
is wrong, because wheelImage.width
is a Flash DisplayObject
property, which means it expects pixels, but mRadius
is obviously in meters. To fix this, we need to apply the scale factor -- but it's hard to remember, without looking it up, whether we need to divide or multiply, right?
The answer is multiply, but we can make this easier by renaming the scale factor from scaleFactor
to mToPx
("meters to pixels"). Do a "find/replace" in your code so you don't miss any.
So, now we know that we can multiply any value in meters by mToPx
to get the equivalent value in pixels. It's not too hard to remember that you can divide by this to do the opposite, but to make it even easier, let's create a variable pxToM
that does the inverse -- i.e., when you multiply a pixel measurement by pxToM
, you get the equivalent measurement in meters.
We can do this by just creating a new variable whose value is one divided by mToPx
, because anyValue * (1 / mToPx) == anyValue / mToPx
. So:
public var mToPx:Number = 20; public var pxToM:Number = 1 / mToPx;
Now just do another "find/replace" to change "/ mToPx
" to "* pxToM
" everywhere in your code. Er, but make sure you rewrite the line we just added, that says public var pxToM:Number = 1 / mToPx
!
Test your SWF to make sure it's still running as you expect. Assuming it is, that's great! It means all you have to remember is that you never divide by a scale factor. Only multiply. Flash is faster at multiplying, anyway.
Okay, now we can actually apply the scale factor to the createWheel()
code we had trouble with:
var wheelImage:Bitmap = new SimpleWheel(); wheelImage.width = mRadius * 2 * mToPx; wheelImage.height = mRadius * 2 * mToPx; wheelImage.x = pxStartX; wheelImage.y = pxStartY; this.addChild(wheelImage); wheelImages.push(wheelImage); }
Try it out:
Cool! Now we can see the PNGs, even though they don't move yet. All that work we did to change the notation may have seemed unnecessary, but believe me, if you didn't do it now, you'd wish you had later on.
Step 4: Make the Wheels Move
As you'll recall from the first part of this series, the actual movement of the wheels is handled in the onTick()
function. Here's what the key code looks like now:
for each (var wheelBody:b2Body in wheelArray) { graphics.drawCircle( wheelBody.GetPosition().x * mToPx, wheelBody.GetPosition().y * mToPx, (wheelBody.GetFixtureList().GetShape() as b2CircleShape).GetRadius() * mToPx ); }
Comment out the circle drawing code, and replace it with code to move the correct image from the array:
var wheelImage:Bitmap; for each (var wheelBody:b2Body in wheelArray) { /* graphics.drawCircle( wheelBody.GetPosition().x * mToPx, wheelBody.GetPosition().y * mToPx, (wheelBody.GetFixtureList().GetShape() as b2CircleShape).GetRadius() * mToPx ); */ wheelImage = wheelImages[wheelArray.indexOf(wheelBody)]; wheelImage.x = wheelBody.GetPosition().x * mToPx; wheelImage.y = wheelBody.GetPosition().y * mToPx; }
(Since each image is added to wheelImages[]
when its equivalent body is added to wheelArray[]
, then we know that, whichever position the body is in wheelArray[]
, the image must be in the same position in wheelImages[]
.)
Try it out:
It's good, but why are the wheels all half underground?
Step 5: That Sinking Feeling
We know that the circle-drawing code from before works fine, so let's un-comment it to see if it gives us any clues:
var wheelImage:Bitmap; for each (var wheelBody:b2Body in wheelArray) { graphics.drawCircle( wheelBody.GetPosition().x * mToPx, wheelBody.GetPosition().y * mToPx, (wheelBody.GetFixtureList().GetShape() as b2CircleShape).GetRadius() * mToPx ); wheelImage = wheelImages[wheelArray.indexOf(wheelBody)]; wheelImage.x = wheelBody.GetPosition().x * mToPx; wheelImage.y = wheelBody.GetPosition().y * mToPx; }
Does it help?
Yes. Clearly, the PNG images are offset from the actual objects' positions.
This is because Box2D's coordinates are given from the center of the circle shapes. When we draw them with graphics.drawCircle()
we have no problems, because that function draws circles from their centers; however, when we set the x
and y
properties of a DisplayObject
, Flash positions it based on its registration point -- and, by default, our SimpleWheel
's registration point is at the top-left corner of the PNG.
The solution is simple, though: we just have to move the image to compensate for this offset. The top-left corner of the image is at the centre of the object, so we need to move it up and left so that the centers align:
var wheelImage:Bitmap; for each (var wheelBody:b2Body in wheelArray) { graphics.drawCircle( wheelBody.GetPosition().x * mToPx, wheelBody.GetPosition().y * mToPx, (wheelBody.GetFixtureList().GetShape() as b2CircleShape).GetRadius() * mToPx ); wheelImage = wheelImages[wheelArray.indexOf(wheelBody)]; wheelImage.x = (wheelBody.GetPosition().x * mToPx) - (wheelImage.width * 0.5); wheelImage.y = (wheelBody.GetPosition().y * mToPx) - (wheelImage.height * 0.5); }
Offsetting it by half its width and height should do the trick. Try it:
Sorted. Comment out the graphics drawing code again and try a higher scale factor (let's go with mToPx = 40
).
Step 6: Detect a Click
Now that we're putting objects on the display list, we can interact with them using the mouse. A really simple way to demonstrate this is to give the wheels a little "push" when they're clicked.
Before we get into the physics code for that, let's make sure we can detect which box was clicked. Add a MouseEvent.CLICK
listener to each wheel image, by inserting this code at the end of createWheel()
:
wheelImage.addEventListener(MouseEvent.CLICK, onClickWheelImage);
Don't forget to import flash.events.MouseEvent
. Now create the handler function (I've added a trace to check it works):
private function onClickWheelImage(event:MouseEvent):void { trace("A wheel was clicked"); }
Try it out. Doesn't work? Oh, that's because Bitmap
doesn't descend from InteractiveObject
(which detects mouse events). That's frustrating. It's easy to fix, though; we just wrap each Bitmap
in a Sprite
, inside createWheel()
:
var wheelImage:Bitmap = new SimpleWheel(); wheelImage.width = mRadius * 2 * mToPx; wheelImage.height = mRadius * 2 * mToPx; //wheelImage.x = pxStartX; <-- delete this //wheelImage.y = pxStartY; <-- delete this //this.addChild(wheelImage);<-- delete this var wheelSprite:Sprite = new Sprite(); wheelSprite.addChild(wheelImage); wheelSprite.x = pxStartX; wheelSprite.y = pxStartY; wheelImages.push(wheelSprite); this.addChild(wheelSprite); wheelSprite.addEventListener(MouseEvent.CLICK, onClickWheelImage);
...and then in onTick(), we can't cast wheelImage
to a Bitmap
anymore, we must define it as a Sprite
:
private function onTick(a_event:TimerEvent):void { //graphics.clear(); graphics.lineStyle(3, 0xff0000); world.Step(0.025, 10, 10); //var wheelImage:Bitmap; <-- delete this var wheelImage:Sprite;
Now the trace works.
Step 7: Push It
We can detect that a wheel was clicked, but which wheel?
Well, we know which image was clicked, because its Sprite
will be the value of event.target
, so we can use the same trick we used in onTick()
to retrieve the wheel body, based on the body's and the Sprite
's positions in the arrays:
private function onClickWheelImage(event:MouseEvent):void { var spriteThatWasClicked:Sprite = event.target as Sprite; var relatedBody:b2Body = wheelArray[wheelImages.indexOf(spriteThatWasClicked)]; }
To give the wheel a nudge, we can use an impulse. I'm sure we'll go into the precise definition of impulse later on in the series, but for now, just think of it as a push, applied in a certain direction and at a certain point on the wheel. (Wikipedia has more info, if you want it.)
We apply an impulse using the b2Body.ApplyImpulse()
method, funnily enough. This takes two arguments:
- A
b2Vec
which defines the direction and strength of the impulse (rememberb2Vecs
are like arrows; the longer the arrow, the stronger the push). - Another
b2Vec
which defines the point on the body where the impulse should be applied.
So, to apply an fairly strong impulse directly to the right, applied to the center of the wheel that was clicked, add this code:
private function onClickWheelImage(event:MouseEvent):void { var spriteThatWasClicked:Sprite = event.target as Sprite; var relatedBody:b2Body = wheelArray[wheelImages.indexOf(spriteThatWasClicked)]; relatedBody.ApplyImpulse( new b2Vec2(10, 0), relatedBody.GetPosition() ); }
Try it out:
Neat, right?
Step 8: Push It Away
We can make this a little more interesting by making the direction of the impulse depend on where the wheel is clicked -- so, click the bottom edge of the tyre and the wheel is pushed upwards; click the top-right and it's pushed down and left; and so on.
I recommend having a go at this yourself, before reading my solution! Remember everything we've covered so far -- the way the wheel's image is offset from center; the scale factor; everything.
Here is what I did:
private function onClickWheelImage(event:MouseEvent):void { var spriteThatWasClicked:Sprite = event.target as Sprite; var relatedBody:b2Body = wheelArray[wheelImages.indexOf(spriteThatWasClicked)]; var centerX:Number = spriteThatWasClicked.x + (spriteThatWasClicked.width / 2); var centerY:Number = spriteThatWasClicked.y + (spriteThatWasClicked.height / 2); var xDistance:Number = centerX - event.stageX; var yDistance:Number = centerY - event.stageY; var xImpulse:Number = 60 * xDistance * pxToM; //I picked 60 simply because var yImpulse:Number = 60 * yDistance * pxToM; //it "felt right" to me relatedBody.ApplyImpulse( new b2Vec2(xImpulse, yImpulse), relatedBody.GetPosition() ); }
And the result:
It's not perfect. Maybe you did better :)
Step 9: Put the "Box" in "Box2D"
Circles are fine, but let's add another shape: squares. Download this image and add it to your project's library, just like you did with the wheel PNG, only this time, name the class SimpleCrate
(or SimpleCrateBitmapData
if using Flash Pro):
(Start to Crate: 29 Steps.)
Now we have to create box shapes. We actually did this in the last tutorial, to create the walls, ceiling, and floor, but that was a while ago, so let's quickly go over it.
Remember the objects we need to create a body?
- A body definition, which is like a template for creating...
- A body, which has a mass and a position, but doesn’t have...
- A shape, which could be as simple as a circle, that must be connected to a body using...
- A fixture, which is created using...
- A fixture definition which is another template, like the body definition.
The code for that looks like this:
var crateBodyDef:b2BodyDef = new b2BodyDef(); crateBodyDef.position.Set(mStartX, mStartY); var crateBody:b2Body = world.CreateBody(crateBodyDef); var crateShape:b2PolygonShape = new b2PolygonShape(); crateShape.SetAsBox(mWidth / 2, mHeight / 2); var crateFixtureDef:b2FixtureDef = new b2FixtureDef(); crateFixtureDef.shape = crateShape; var crateFixture:b2Fixture = crateBody.CreateFixture(crateFixtureDef);
(Remember that the SetAsBox()
method takes the half-width and half-height, so we have to divide by two.)
Let's wrap that up in a function, like we did with createWheel()
:
private function createCrate(mWidth:Number, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var crateBodyDef:b2BodyDef = new b2BodyDef(); crateBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); //note the scale factor var crateBody:b2Body = world.CreateBody(crateBodyDef); var crateShape:b2PolygonShape = new b2PolygonShape(); crateShape.SetAsBox(mWidth / 2, mWidth / 2); //crates are square so width == height var crateFixtureDef:b2FixtureDef = new b2FixtureDef(); crateFixtureDef.shape = crateShape; var crateFixture:b2Fixture = crateBody.CreateFixture(crateFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); crateBody.SetLinearVelocity(startingVelocity); }
Watch out -- the code is slightly different from that above. Since we used pixel measurements to choose the positions of the wheels, I've used pixel measurements for the position of the crate, too. Also, since crates are square, I've not included a mHeight
parameter. And I've set the velocity, using basically the same code as for the wheels.
Yeah, yeah, we can't actually see them unless we use graphics
calls or add some instances of SimpleCrate
or whatever, I know. But we can at least see their effects, so try adding one now:
private function getStarted():void { var gravity:b2Vec2 = new b2Vec2(0, 10); world = new b2World(gravity, true); wheelArray = new Array(); wheelImages = new Array(); for (var i:int = 0; i < 20; i++) { createWheel( Math.random() * 0.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createCrate( Math.random() * 0.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ) } createBoundaries(); stepTimer = new Timer(0.025 * 1000); stepTimer.addEventListener(TimerEvent.TIMER, onTick); graphics.lineStyle(3, 0xff0000); stepTimer.start(); }
Try it out:
It's working, but something's not quite right. Guess we'll have to make them visible to find out what.
Step 10: See Crates
As before, we'll make an array to hold the crate bodies, and another array to hold the crate graphics:
public var world:b2World; public var wheelArray:Array; public var wheelImages:Array; public var crateArray:Array; public var crateImages:Array; public var stepTimer:Timer; public var mToPx:Number = 50; public var pxToM:Number = 1 / mToPx; /** SNIP **/ private function getStarted():void { var gravity:b2Vec2 = new b2Vec2(0, 10); world = new b2World(gravity, true); wheelArray = new Array(); wheelImages = new Array(); crateArray = new Array(); crateImages = new Array();
Then, we have to populate these arrays:
private function createCrate(mWidth:Number, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var crateBodyDef:b2BodyDef = new b2BodyDef(); crateBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); //note the scale factor var crateBody:b2Body = world.CreateBody(crateBodyDef); var crateShape:b2PolygonShape = new b2PolygonShape(); crateShape.SetAsBox(mWidth / 2, mWidth / 2); //crates are square so width == height var crateFixtureDef:b2FixtureDef = new b2FixtureDef(); crateFixtureDef.shape = crateShape; var crateFixture:b2Fixture = crateBody.CreateFixture(crateFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); crateBody.SetLinearVelocity(startingVelocity); crateArray.push(crateBody); var crateImage:Bitmap = new SimpleCrate(); //or "new Bitmap(new SimpleCrateBitmapData());" in Flash Pro crateImage.width = mWidth * mToPx; crateImage.height = mWidth * mToPx; var crateSprite:Sprite = new Sprite(); crateSprite.addChild(crateImage); crateSprite.x = pxStartX; crateSprite.y = pxStartY; crateImages.push(crateSprite); this.addChild(crateSprite); }
This is essentially the same code as for rendering the wheel, but with changes to the width and height because the crate is square, not circular. The images are in the display list, but we have to make them move every tick, so add this to the onTick()
function:
var crateImage:Sprite; for each (var crateBody:b2Body in crateArray) { crateImage = crateImages[crateArray.indexOf(crateBody)]; crateImage.x = (crateBody.GetPosition().x * mToPx) - (crateImage.width * 0.5); crateImage.y = (crateBody.GetPosition().y * mToPx) - (crateImage.height * 0.5); }
Here's the result:
The crates aren't moving! That explains the weirdness. Know why? It's because I forgot something... have a guess at what, then expand the box below to see if you're right.
private function createCrate(mWidth:Number, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var crateBodyDef:b2BodyDef = new b2BodyDef(); crateBodyDef.type = b2Body.b2_dynamicBody; crateBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var crateBody:b2Body = world.CreateBody(crateBodyDef); var crateShape:b2PolygonShape = new b2PolygonShape(); crateShape.SetAsBox(mWidth / 2, mWidth / 2); var crateFixtureDef:b2FixtureDef = new b2FixtureDef(); crateFixtureDef.shape = crateShape; var crateFixture:b2Fixture = crateBody.CreateFixture(crateFixtureDef);
Try now:
Problem solved! Let's make the crates a little bigger, too:
for (var i:int = 0; i < 20; i++) { createWheel( Math.random() * 0.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createCrate( Math.random() * 1.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ) }
That's better.
Step 11: The Amazing Balancing Crate
I noticed something weird. Try balancing a big crate on a small wheel, like so:
/* for (var i:int = 0; i < 20; i++) { createWheel( Math.random() * 0.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createCrate( Math.random() * 1.5, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ) } */ //create these objects specifically, instead of generating the wheels and crates at random: createWheel(0.5, stage.stageWidth / 2, stage.stageHeight - (0.5 * mToPx), 0, 0); createCrate(3.0, (1.0 * mToPx) + stage.stageWidth / 2, stage.stageHeight - (3.5 * mToPx), 0, 0);
Odd, right? A crate shouldn't balance on a round object like that. If you click the wheel to make it roll away, the crate just falls straight down (or, sometimes, follows the wheel).
It makes it feel as though the mass of the crate is unevenly distributed -- like it contains something really, really heavy, all in its left side, which is letting it balance on the wheel.
In fact, the problem is to do with its mass: we didn't give it any! Box2D does give a body a default mass if none is specified, but there are other mass-related properties (like the moment of inertia) that don't get set correctly in this case, making the body act weird.
Don't worry about what these properties are for now -- just know that, in order to fix them, we need to give each body a proper mass. Well, actually, in order to make sure everything behaves the way we'd expect it to, we should instead set the mass of individual fixtures, and let Box2D figure out the mass of the body itself. And instead of setting the mass directly, we should set the density -- that is, how many kilograms a square meter of the fixture would weigh.
Though all that sounds complicated, it actually just means adding one line:
private function createCrate(mWidth:Number, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var crateBodyDef:b2BodyDef = new b2BodyDef(); crateBodyDef.type = b2Body.b2_dynamicBody; crateBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var crateBody:b2Body = world.CreateBody(crateBodyDef); var crateShape:b2PolygonShape = new b2PolygonShape(); crateShape.SetAsBox(mWidth / 2, mWidth / 2); var crateFixtureDef:b2FixtureDef = new b2FixtureDef(); crateFixtureDef.shape = crateShape; crateFixtureDef.density = 1.0; //bigger number = heavier object var crateFixture:b2Fixture = crateBody.CreateFixture(crateFixtureDef);
We actually already set this for the wheels! Still, it's interesting to see how things behave differently without these basic properties, right?
Test it out now:
Hmm... it's better, but still doesn't feel quite right. Try playing the SWF a few times, and you'll see what I mean.
Step 12: Let Someone Else Do the Hard Work
When something didn't feel quite right in the first part of this tutorial (after adding the wheel graphic), we just went back to our existing graphics
drawing code -- which we knew worked -- and saw what the problem was.
Unfortunately, we don't have any code for drawing rectangles, let alone code that we know already works. Fortunately, Box2D does have such code: it's called DebugDraw
.
Import the class Box2D.Dynamics.b2DebugDraw
, then modify getStarted()
like so:
private function getStarted():void { var gravity:b2Vec2 = new b2Vec2(0, 10); world = new b2World(gravity, true); var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(mToPx); debugDraw.SetFlags(b2DebugDraw.e_shapeBit); world.SetDebugDraw(debugDraw);
This creates an object called debugDraw
which will look at all the bodies in the world
and draw them in the new Sprite we created, called debugSprite
. We pass it b2DebugDraw.e_shapeBit
to tell it that it should draw the shapes (we could tell it only to draw, say, the centers of mass of the bodies instead, if we wanted).
We have to manually tell the World to tell the DebugDraw object to render the bodies, so do this in onTick()
:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData();
(You can get rid of all the graphics
calls now; we won't need them any more.)
Try this out:
Did you see it? Here's a screenshot that shows the problem:
The body is rotating, but the image isn't. And -- of course it isn't! We never told it to!
Step 13: Like a Record, Baby
We can rotate the image of the crate by setting its rotation
property in onTick()
:
for each (var crateBody:b2Body in crateArray) { crateImage = crateImages[crateArray.indexOf(crateBody)]; crateImage.x = (crateBody.GetPosition().x * mToPx) - (crateImage.width * 0.5); crateImage.y = (crateBody.GetPosition().y * mToPx) - (crateImage.height * 0.5); crateImage.rotation = crateBody.GetAngle(); }
...well, okay, that's not true; just as how Box2D uses meters and Flash uses pixels for distance measurements, Box2D and Flash use different units for angles: radians in Box2D's case, degrees in Flash's. Check this article for a full explanation.
To deal with this, we'll create a pair of "angle factor" variables:
public var mToPx:Number = 50; public var pxToM:Number = 1 / mToPx; public var radToDeg:Number = 180 / b2Settings.b2_pi; public var degToRad:Number = b2Settings.b2_pi / 180;
You'll need to import Box2D.Common.b2Settings
-- and, again, check this article if you want to know why we're using Pi and 180.
Now just change the rotation of the image in onTick()
:
for each (var crateBody:b2Body in crateArray) { crateImage = crateImages[crateArray.indexOf(crateBody)]; crateImage.x = (crateBody.GetPosition().x * mToPx) - (crateImage.width * 0.5); crateImage.y = (crateBody.GetPosition().y * mToPx) - (crateImage.height * 0.5); crateImage.rotation = crateBody.GetAngle() * radToDeg; }
Try it out:
...oh.
Step 14: Up and to the Left
Here's the problem: the crate image rotates around a certain point in the image, called the registration point. The crate body rotates around a certain point in the body, called the center of mass. These two points are not the same -- except in the one position where the crate hasn't rotated at all.
This animation shows the difference:
The crate body rotates around its center; the crate image rotates around one of its corners (the one that starts at the top left).
If you're using Flash Pro, there's a really easy way to fix this: just change the registration point of your Crate symbol to the exact center of the square. If you're not, however, you'll need to do the equivalent thing with code. That's still pretty simple, though, because the actual crate Bitmap is inside a Sprite; when we rotate the crate image, we're actually rotating that Sprite, which in turn rotates the Bitmap inside it.
See, at the minute, it's like this:
The gray dashed square represents the Sprite that contains the crate Bitmap. The little grey circle is the point around which the Sprite rotates. If we rotate the Sprite by 90 degrees:
...it does this, as you'd expect from looking at the Sprite. But if all you can see is the crate, it looks like it's rotated like so:
See? Now, suppose we move the crate Bitmap a little bit within the Sprite, so that the centers line up:
...and we rotate the Sprite:
...the crate seems to have rotated around its center:
Perfect! So, to make this work, we just need to move the crate Bitmap, within the Sprite, upwards by half its height and left by half its width. Do this by modifying the code in createCrate()
:
var crateImage:Bitmap = new SimpleCrate(); crateImage.width = mWidth * mToPx; crateImage.height = mWidth * mToPx; var crateSprite:Sprite = new Sprite(); crateSprite.addChild(crateImage); crateImage.x -= crateImage.width / 2; crateImage.y -= crateImage.height / 2; crateSprite.x = pxStartX; crateSprite.y = pxStartY; crateImages.push(crateSprite); this.addChild(crateSprite);
We also need to remove this code from onTick()
which moves the crate Sprite left by half its width and up by half its height:
var crateImage:Sprite; for each (var crateBody:b2Body in crateArray) { crateImage = crateImages[crateArray.indexOf(crateBody)]; //remember this is the Sprite, not the Bitmap crateImage.x = (crateBody.GetPosition().x * mToPx);// - (crateImage.width * 0.5); crateImage.y = (crateBody.GetPosition().y * mToPx);// - (crateImage.height * 0.5); crateImage.rotation = crateBody.GetAngle() * radToDeg; }
Try it out now:
Great! You can't even see the DebugDraw image, which means it's perfectly covered by the images.
Conclusion
You might like to reinstate the code that creates 20 wheels and 20 crates of random sizes and positions, to see how it acts with the changes we made. Here's mine:
Wow, it really makes a difference -- compare it to the SWF from Step 11!
Now you've got Bitmaps to display nicely, try experimenting with your own images instead of sticking to what I've made. In the next part of this series we'll expand on this with different properties, and maybe even build a fun little toy out of it!
Don't forget you can vote on what you want to learn from this tutorial over at our Google Moderator page.
Comments