Box2D for Flash and AS3: Bitmaps and Boxes

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:

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[]:

Make sure you initialize it near the top of getStarted(), as well:


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:

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:

(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:

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:

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:

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:

Comment out the circle drawing code, and replace it with code to move the correct image from the array:

(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:

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:

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():

Don't forget to import flash.events.MouseEvent. Now create the handler function (I've added a trace to check it works):

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():

...and then in onTick(), we can't cast wheelImage to a Bitmap anymore, we must define it as a 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:

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:

  1. A b2Vec which defines the direction and strength of the impulse (remember b2Vecs are like arrows; the longer the arrow, the stronger the push).
  2. 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:

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:

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:

(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():

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:

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:

Then, we have to populate these arrays:

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:

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.

Try now:

Problem solved! Let's make the crates a little bigger, too:

That's better.


Step 11: The Amazing Balancing Crate

I noticed something weird. Try balancing a big crate on a small wheel, like so:

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:

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:

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():

(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():

...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:

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():

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():

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:

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.

Tags:

Comments

Related Articles