In the first two parts of this series, we created two simple types of object, rendered using Flash's display list. In this part, we'll see how to extend this to let us create as many different types of object as we want, without our code getting all tangled up!
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 from the end of the the second part. (The Download button above contains the source code from the end of this part.)
Step 1: New Balls Please
Let's take a quick look at where we left off:
Bunch of wheels and crates bouncing around. The wheels don't all have the same properties; each wheel has a random size, restitution, friction, and density. Ditto for the crates.
That's been fine for testing, but now let's make things a little less random. We'll start by replacing the wheels with tennis balls, which all have the same properties.
First, draw a tennis ball (or get a photo of one). Here's mine:

To make yours consistent with mine, make sure that it's called TennisBall.png, that you save it in \lib\
, that it's 35x35px, and that it has a transparent background. (You can change any of these points, if you wish, but you'll have to figure out how to modify your code accordingly.)
Embed the PNG into your Main.as
file, just like we've done before:
[Embed(source='../lib/TennisBall.png')] public var TennisBall:Class;
Now, create two arrays: one for your tennis ball Box2D objects, and one for the corresponding images:
public var tennisBallArray:Array; public var tennisBallImages:Array;
tennisBallArray = new Array(); tennisBallImages = new Array();
(This is all much the same as we did in the second part of this series, so take another look at that if you need a refresher.)
Now we need a function to create a single tennis ball. Here's what we use to create a single wheel:
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 * pxToM, pxStartY * pxToM); 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 * mToPx; wheelImage.height = mRadius * 2 * mToPx; 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); }
We can copy this and modify it to our needs. Since all tennis balls will have the same properties (size, restitution, friction, and density), we don't need the mRadius
argument, and we don't need to use random numbers anywhere. It's hard to guess what values of restitution and so on will be correct, so just have a go - we can always correct them later.
Here's my createTennisBall()
function:
private function createTennisBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var tennisBallBodyDef:b2BodyDef = new b2BodyDef(); tennisBallBodyDef.type = b2Body.b2_dynamicBody; tennisBallBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var tennisBallBody:b2Body = world.CreateBody(tennisBallBodyDef); var circleShape:b2CircleShape = new b2CircleShape(35 * .5 * pxToM); var tennisBallFixtureDef:b2FixtureDef = new b2FixtureDef(); tennisBallFixtureDef.shape = circleShape; tennisBallFixtureDef.restitution = 0.8; tennisBallFixtureDef.friction = 0.75; tennisBallFixtureDef.density = 1.0; var tennisBallFixture:b2Fixture = tennisBallBody.CreateFixture(tennisBallFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); tennisBallBody.SetLinearVelocity(startingVelocity); tennisBallArray.push(tennisBallBody); var tennisBallImage:Bitmap = new TennisBall(); var tennisBallSprite:Sprite = new Sprite(); tennisBallSprite.addChild(tennisBallImage); tennisBallSprite.x = pxStartX; tennisBallSprite.y = pxStartY; tennisBallImages.push(tennisBallSprite); this.addChild(tennisBallSprite); //tennisBallSprite.addEventListener(MouseEvent.CLICK, onClickWheelImage); }
A few things to note:
- The
b2CircleShape()
constructor is expecting to receive the radius of the tennis ball, in meters, and I don't want to squash or enlarge the image I've made. I guess I could look up the radius of an actual tennis ball, enter this value in here, and alter thepxToM
scale factor to make everything else fit, but instead I've just told the constructor to convert the pixel radius of my tennis ball image to meters. - Restituion is the bounciness of the object, on a scale of 0 to 1; tennis balls are pretty bouncy so I've set this quite high.
- Friction is also between 0 and 1; tennis balls are quite rough so I set this fairly high, too.
- Density is not limited to any particular range; tennis balls are mostly air, so I set this to the same value as my crates (which I'm also assuming are empty).
- Unlike in
createWheel()
, there's no need to resize the image (as I mentioned above), so I've deleted those lines. - I've just commented the
onClickWheelImage
event listener out for now, as it's not relevant.
Try to compile the SWF. It should work - if it didn't, you missed something out - but since we aren't actually calling createTennisBall()
from anywhere, nothing will be different.
Step 2: Creating Tennis Balls
Let's just remove the wheels straight away, and replace them with tennis balls. Change this code, in getStarted()
:
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 ) }
...to this:
for (var i:int = 0; i < 20; i++) { createTennisBall( 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 ) }
Note that the first argument has been deleted, since we don't manually set the size any more, but all other arguments remain the same. Try it out:
Hmm... not quite right. Ah, of course - we didn't add any code to render the tennis balls. We'll do that next. Change onTick()
from this:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var wheelImage:Sprite; for each (var wheelBody:b2Body in wheelArray) { 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); } 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); crateImage.rotation = crateBody.GetAngle() * radToDeg; } }
...to this:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var tennisBallImage:Sprite; for each (var tennisBallBody:b2Body in tennisBallArray) { tennisBallImage = tennisBallImages[tennisBallArray.indexOf(tennisBallBody)]; tennisBallImage.x = (tennisBallBody.GetPosition().x * mToPx) - (tennisBallImage.width * 0.5); tennisBallImage.y = (tennisBallBody.GetPosition().y * mToPx) - (tennisBallImage.height * 0.5); } 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); crateImage.rotation = crateBody.GetAngle() * radToDeg; } }
(It's really just a case of doing a find-and-replace, switching "wheel" to "tennisBall".) Try the SWF now:
Better, but we don't have any rotation (we didn't need it before, because the wheels were all rotationally symmetric). Add that now:
var tennisBallImage:Sprite; for each (var tennisBallBody:b2Body in tennisBallArray) { tennisBallImage = tennisBallImages[tennisBallArray.indexOf(tennisBallBody)]; tennisBallImage.x = (tennisBallBody.GetPosition().x * mToPx) - (tennisBallImage.width * 0.5); tennisBallImage.y = (tennisBallBody.GetPosition().y * mToPx) - (tennisBallImage.height * 0.5); tennisBallImage.rotation = tennisBallBody.GetAngle() * radToDeg; }
Ah. Remember we had this problem with the crates? Remember how we solved it? Have a go at doing the same here, on your own. My solution is below if you want to check:
private function createTennisBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var tennisBallBodyDef:b2BodyDef = new b2BodyDef(); tennisBallBodyDef.type = b2Body.b2_dynamicBody; tennisBallBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var tennisBallBody:b2Body = world.CreateBody(tennisBallBodyDef); var circleShape:b2CircleShape = new b2CircleShape(35 * .5 * pxToM); var tennisBallFixtureDef:b2FixtureDef = new b2FixtureDef(); tennisBallFixtureDef.shape = circleShape; tennisBallFixtureDef.restitution = 0.8; tennisBallFixtureDef.friction = 0.75; tennisBallFixtureDef.density = 1.0; var tennisBallFixture:b2Fixture = tennisBallBody.CreateFixture(tennisBallFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); tennisBallBody.SetLinearVelocity(startingVelocity); tennisBallArray.push(tennisBallBody); var tennisBallImage:Bitmap = new TennisBall(); var tennisBallSprite:Sprite = new Sprite(); tennisBallSprite.addChild(tennisBallImage); tennisBallImage.x -= tennisBallImage.width / 2; tennisBallImage.y -= tennisBallImage.height / 2; tennisBallSprite.x = pxStartX; tennisBallSprite.y = pxStartY; tennisBallImages.push(tennisBallSprite); this.addChild(tennisBallSprite); //tennisBallSprite.addEventListener(MouseEvent.CLICK, onClickWheelImage); } //... private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var tennisBallImage:Sprite; for each (var tennisBallBody:b2Body in tennisBallArray) { tennisBallImage = tennisBallImages[tennisBallArray.indexOf(tennisBallBody)]; tennisBallImage.x = (tennisBallBody.GetPosition().x * mToPx);// - (tennisBallImage.width * 0.5); tennisBallImage.y = (tennisBallBody.GetPosition().y * mToPx);// - (tennisBallImage.height * 0.5); tennisBallImage.rotation = tennisBallBody.GetAngle() * radToDeg; } 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); crateImage.rotation = crateBody.GetAngle() * radToDeg; } }
Test it out:
Ace.
Step 3: Creating Bowling Balls
I'd like to add three more types of ball: a bowling ball, a basketball, and a rubber ball. As you can see from the above, it's a pain to do the same thing over and over again. We'll add the bowling ball next, and try to simplify the process so we don't have to keep making this irritating little changes.
Here's my bowling ball image. Again, feel free to use your own, but try to make it 60x60px with the name BowlingBall.png:

Embed the image and add the necessary arrays:
[Embed(source='../lib/BowlingBall.png')] public var BowlingBall:Class; //... public var bowlingBallArray:Array; public var bowlingBallImages:Array; //... bowlingBallArray = new Array(); bowlingBallImages = new Array();
Write a createBowlingBall()
function:
private function createBowlingBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var bowlingBallBodyDef:b2BodyDef = new b2BodyDef(); bowlingBallBodyDef.type = b2Body.b2_dynamicBody; bowlingBallBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var bowlingBallBody:b2Body = world.CreateBody(bowlingBallBodyDef); var circleShape:b2CircleShape = new b2CircleShape(60 * .5 * pxToM); var bowlingBallFixtureDef:b2FixtureDef = new b2FixtureDef(); bowlingBallFixtureDef.shape = circleShape; bowlingBallFixtureDef.restitution = 0.0; //not bouncy bowlingBallFixtureDef.friction = 0.0; //very smooth bowlingBallFixtureDef.density = 20.0; //very heavy var bowlingBallFixture:b2Fixture = bowlingBallBody.CreateFixture(bowlingBallFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); bowlingBallBody.SetLinearVelocity(startingVelocity); bowlingBallArray.push(bowlingBallBody); var bowlingBallImage:Bitmap = new BowlingBall(); var bowlingBallSprite:Sprite = new Sprite(); bowlingBallSprite.addChild(bowlingBallImage); bowlingBallImage.x -= bowlingBallImage.width / 2; bowlingBallImage.y -= bowlingBallImage.height / 2; bowlingBallSprite.x = pxStartX; bowlingBallSprite.y = pxStartY; bowlingBallImages.push(bowlingBallSprite); this.addChild(bowlingBallSprite); //bowlingBallSprite.addEventListener(MouseEvent.CLICK, onClickWheelImage); }
...and create the actual objects:
for (var i:int = 0; i < 20; i++) { createTennisBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( 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 ) }
Phew. Okay, now for the rendering. Let's take another look at our rendering code so far:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var tennisBallImage:Sprite; for each (var tennisBallBody:b2Body in tennisBallArray) { tennisBallImage = tennisBallImages[tennisBallArray.indexOf(tennisBallBody)]; tennisBallImage.x = (tennisBallBody.GetPosition().x * mToPx);// - (tennisBallImage.width * 0.5); tennisBallImage.y = (tennisBallBody.GetPosition().y * mToPx);// - (tennisBallImage.height * 0.5); tennisBallImage.rotation = tennisBallBody.GetAngle() * radToDeg; } 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); crateImage.rotation = crateBody.GetAngle() * radToDeg; } }
Notice anything? Thanks to the change we made in the last step - to fix the rotation problem - the two highlighted sections are almost identical; just swap out the word "tennisBall" for "crate" in each case and they're a perfect match.
This suggests that we could combine them into a single loop, which could be used to render all objects. That way, we wouldn't need to create a new loop to render the bowling balls. Let's look at this next.
Step 4: Rendering in a Single Loop
To do this, we just need to combine the tennisBallArray
and crateArray
into a single array. It's easier if I show you:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var generalB2BodyArray:Array = new Array(); generalB2BodyArray = generalB2BodyArray.concat(tennisBallArray, crateArray); var generalImages:Array = new Array(); generalImages = generalImages.concat(tennisBallImages, crateImages); var generalImage:Sprite; for each (var generalBody:b2Body in generalB2BodyArray) { generalImage = generalImages[generalB2BodyArray.indexOf(generalBody)]; generalImage.x = (generalBody.GetPosition().x * mToPx); generalImage.y = (generalBody.GetPosition().y * mToPx); generalImage.rotation = generalBody.GetAngle() * radToDeg; } }
See how that works? In lines 6-10, we create a couple of new arrays: one contains all the tennis ball and crate b2Body objects, and the other contains all the tennis ball and crate Sprites. Most importantly, they contain them in the same order - if the 17th element of generalImages[]
is a tennis ball, then the 17th eleemnt of generalB2BodyArray[]
is the same tennis ball's b2Body.
The for
loop then does exactly the same as the two before it did; it's just way more general. Try it out:
The bowling ball images don't move, but look how easy it is to add them to the renderer:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var generalB2BodyArray:Array = new Array(); generalB2BodyArray = generalB2BodyArray.concat(tennisBallArray, crateArray, bowlingBallArray); var generalImages:Array = new Array(); generalImages = generalImages.concat(tennisBallImages, crateImages, bowlingBallImages); var generalImage:Sprite; for each (var generalBody:b2Body in generalB2BodyArray) { generalImage = generalImages[generalB2BodyArray.indexOf(generalBody)]; generalImage.x = (generalBody.GetPosition().x * mToPx); generalImage.y = (generalBody.GetPosition().y * mToPx); generalImage.rotation = generalBody.GetAngle() * radToDeg; } }
That's all it takes! Check it out:
It's beginning to run a little slowly on my machine, so I'm going to reduce the number of objects from 60 to 30:
for (var i:int = 0; i < 10; i++) { createTennisBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( 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 ) }
There's more room for them now, too:
Step 5: Creating Basketballs
Basketballs, next. My image is 75x75px and called Basketball.png:

As usual, start by embedding the image as a class:
[Embed(source='../lib/Basketball.png')] public var Basketball:Class;
Now we'll add the two arrays for the basketball b2Body objects and Sprites. Except... wait. Why do we need a separate array for these?
We don't, actually! Not any more. Let's combine all our b2Body objects into one array, and all our Sprites into another. Change this:
public var crateArray:Array; public var crateImages:Array; public var tennisBallArray:Array; public var tennisBallImages:Array; public var bowlingBallArray:Array; public var bowlingBallImages:Array;
...to this:
public var generalB2BodyArray:Array; public var generalImages:Array;
Change this:
wheelArray = new Array(); wheelImages = new Array(); crateArray = new Array(); crateImages = new Array(); tennisBallArray = new Array(); tennisBallImages = new Array(); bowlingBallArray = new Array(); bowlingBallImages = new Array();
...to this:
generalB2BodyArray = new Array(); generalImages = new Array();
In every create«Whatever»
function, change the equivalent of this:
tennisBallArray.push(tennisBallBody);
...to this:
generalB2BodyArray.push(tennisBallBody); //or .push(crateBody), or whatever
...and this:
tennisBallImages.push(tennisBallSprite);
...to this:
generalImages.push(tennisBallSprite); //or .push(crateSprite), or whatever
We can make onClickWheelImage()
applicable to all types of objects very easily now; just change this one line:
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() ); }
...like so:
private function onClickWheelImage(event:MouseEvent):void { var spriteThatWasClicked:Sprite = event.target as Sprite; var relatedBody:b2Body = generalB2BodyArray[generalImages.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() ); }
Finally, in onTick()
, we can delete the lines highlighted below:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var generalB2BodyArray:Array = new Array(); generalB2BodyArray = generalB2BodyArray.concat(tennisBallArray, crateArray, bowlingBallArray); var generalImages:Array = new Array(); generalImages = generalImages.concat(tennisBallImages, crateImages, bowlingBallImages); var generalImage:Sprite; for each (var generalBody:b2Body in generalB2BodyArray) { generalImage = generalImages[generalB2BodyArray.indexOf(generalBody)]; generalImage.x = (generalBody.GetPosition().x * mToPx); generalImage.y = (generalBody.GetPosition().y * mToPx); generalImage.rotation = generalBody.GetAngle() * radToDeg; } }
Compile your SWF to make sure it works:
If it doesn't, it's probably due to a missed array somewhere; just check your error messages and try again.
That took a while, but it makes it much simpler to add new objects. We're gradually streamlining the whole process.
There's another simplification we can make to these arrays, though...
Step 6: Linking Images to b2Body Objects
Why do we have the generalImages[]
array? It's simply so that we can link a b2Body to its associated image, and vice-versa. But there's a better way to do this.
Every b2Body has an internal "user data" object - this can be set to any object you like, and can be retrieved from anywhere. We can set this to point to the b2Body object's Sprite.
It's simple to do; in each create«Whatever»()
function, alter the equivalent of this line:
generalImages.push(tennisBallSprite);
...to the equivalent of this:
tennisBallBody.SetUserData(tennisBallSprite);
Then, in onTick()
, instead of finding the image from an array, we can find it using the user data:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var generalImage:Sprite; for each (var generalBody:b2Body in generalB2BodyArray) { generalImage = generalBody.GetUserData() as Sprite; generalImage.x = (generalBody.GetPosition().x * mToPx); generalImage.y = (generalBody.GetPosition().y * mToPx); generalImage.rotation = generalBody.GetAngle() * radToDeg; } }
(If you wanted to, you could replace the user data with a sprite sheet and use blitting to render the objects here. It's quite simple to do so, with this structure.)
You could actually remove the generalImages[]
array altogether now... apart from one thing: it's still used in onClickWheelImage()
to get a reference from the image to the b2Body.
We could just comment onClickWheelImage()
out, because we're not using it right now, but that would be lazy. And this raises a good point, anyway: we need to be able to find any image's associated b2Body. Unfortunately, we're using the Sprite class, and it doesn't have a built-in "user data" - but we could give it one.
Create a new class, called B2Sprite
, extending Sprite
, in your \src\
folder:
package { import flash.display.Sprite; public class B2Sprite extends Sprite { public function B2Sprite() { } } }
(I've used a capital B to distinguish it from the official Box2D classes.)
Create a b2Body
property:
package { import Box2D.Dynamics.b2Body; import flash.display.Sprite; public class B2Sprite extends Sprite { public var body:b2Body; public function B2Sprite() { } } }
Because this extends Sprite, it'll act exactly the same as a Sprite, except with that extra property that we need.
Now, in every create«Whatever»()
function, change the equivalent of this code:
var tennisBallSprite:Sprite = new Sprite();
...to this:
var tennisBallSprite:B2Sprite = new B2Sprite();
Also, wherever you have the equivalent of this line:
tennisBallBody.SetUserData(tennisBallSprite);
...add another line underneath it, to create a connection from the B2Sprite to the b2Body:
tennisBallBody.SetUserData(tennisBallSprite); tennisBallSprite.body = tennisBallBody;
Then, you can alter onClickWheelImage()
, like so:
private function onClickWheelImage(event:MouseEvent):void { if (event.target is B2Sprite) { var spriteThatWasClicked:B2Sprite = event.target as B2Sprite; var relatedBody:b2Body = spriteThatWasClicked.body; 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() ); } }
Now that you've done all of that, you can uncomment out the lines that add event listeners for onClickWheelImage()
(in the tennis ball and bowling ball creation functions), and try out your SWF:
Click a tennis ball or bowling ball to see the effect.
Rename the onClickWheelImage()
function to onClickClickableImage()
. If you like, you could try making the crates clickable, too!
Step 7: Creating Basketballs (Still)
Hey, weren't we adding basketballs? Let's get back to that.
So, we've removed the image array, which means we were about to write the createBasketball()
function. But, wait... I can't help but wonder whether we need the generalB2BodyArray[]
.
It turns out that we don't. The b2world
contains a list of all b2Body objects inside it, so the array is unnecessary. We'll just make a quick change - I promise - and then we can add the basketball.
This list is not an array. It works like this:
- You request
world.GetBodyList()
, and this gives you the first b2Body object in the list. - With this body, you call
.GetNext()
to retrieve the next b2Body object in the list. - If
.GetNext()
returnsnull
, you've reached the end of the list.
Have a go at re-writing the onTick()
rendering code to use this list instead of the generalB2BodyArray[]
. Here's my solution if you want to check:
private function onTick(a_event:TimerEvent):void { world.Step(0.025, 10, 10); world.DrawDebugData(); var currentBody:b2Body = world.GetBodyList(); var currentImage:B2Sprite; while (currentBody != null) { if (currentBody.GetUserData() is B2Sprite) //important, since the boundaries don't have associated images { currentImage = currentBody.GetUserData() as B2Sprite; currentImage.x = (currentBody.GetPosition().x * mToPx); currentImage.y = (currentBody.GetPosition().y * mToPx); currentImage.rotation = currentBody.GetAngle() * radToDeg; } currentBody = currentBody.GetNext(); } }
(I've taken the opportunity to add some additional checks and change the name of some variables; you don't have to do this, but it's probably easier if you do, to keep our code consistent.)
You can now remove all references to generalB2BodyArray[]
. Make sure you test your SWF to check it still runs fine:
Creating that basketball is now pretty simple. First, write createBasketball()
:
private function createBasketball(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var basketballBodyDef:b2BodyDef = new b2BodyDef(); basketballBodyDef.type = b2Body.b2_dynamicBody; basketballBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var basketballBody:b2Body = world.CreateBody(basketballBodyDef); var circleShape:b2CircleShape = new b2CircleShape(75 * .5 * pxToM); var basketballFixtureDef:b2FixtureDef = new b2FixtureDef(); basketballFixtureDef.shape = circleShape; basketballFixtureDef.restitution = 0.6; //fairly bouncy basketballFixtureDef.friction = 0.4; //a little rough basketballFixtureDef.density = 2.5; //pretty light var basketballFixture:b2Fixture = basketballBody.CreateFixture(basketballFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); basketballBody.SetLinearVelocity(startingVelocity); var basketballImage:Bitmap = new Basketball(); var basketballSprite:B2Sprite = new B2Sprite(); basketballSprite.addChild(basketballImage); basketballImage.x -= basketballImage.width / 2; basketballImage.y -= basketballImage.height / 2; basketballSprite.x = pxStartX; basketballSprite.y = pxStartY; basketballBody.SetUserData(basketballSprite); this.addChild(basketballSprite); basketballSprite.body = basketballBody; basketballSprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
...then, create a whole bunch of them:
for (var i:int = 0; i < 10; i++) { createTennisBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBasketball( 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 ) }
Done! Try it:
He shoots, he scores.
Step 8: Making Object Construction Easier
Every time we write a new create«Whatever»()
function, we copy and paste an old one. They're almost identical. And, as you've seen, that usually means we can improve our code structure to prevent us from having to copy and paste the same lines over and over again; it means there's a better way to do things, which will make it easier for us to create new objects in the future.
Look at this:
private function createTennisBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var tennisBallBodyDef:b2BodyDef = new b2BodyDef(); tennisBallBodyDef.type = b2Body.b2_dynamicBody; tennisBallBodyDef.position.Set(pxStartX * pxToM, pxStartY * pxToM); var tennisBallBody:b2Body = world.CreateBody(tennisBallBodyDef); var circleShape:b2CircleShape = new b2CircleShape(35 * .5 * pxToM); var tennisBallFixtureDef:b2FixtureDef = new b2FixtureDef(); tennisBallFixtureDef.shape = circleShape; tennisBallFixtureDef.restitution = 0.8; tennisBallFixtureDef.friction = 0.75; tennisBallFixtureDef.density = 1.0; var tennisBallFixture:b2Fixture = tennisBallBody.CreateFixture(tennisBallFixtureDef); var startingVelocity:b2Vec2 = new b2Vec2(mVelocityX, mVelocityY); tennisBallBody.SetLinearVelocity(startingVelocity); var tennisBallImage:Bitmap = new TennisBall(); var tennisBallSprite:B2Sprite = new B2Sprite(); tennisBallSprite.addChild(tennisBallImage); tennisBallImage.x -= tennisBallImage.width / 2; tennisBallImage.y -= tennisBallImage.height / 2; tennisBallSprite.x = pxStartX; tennisBallSprite.y = pxStartY; tennisBallBody.SetUserData(tennisBallSprite); tennisBallSprite.body = tennisBallBody; this.addChild(tennisBallSprite); tennisBallSprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
I've been using it as the template for all my b2Body creation functions. Here's how they all boil down:
private function create«Whatever»(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { //create body def and set its starting position //create body //create shape and set its type and size //create fixture def and set its properties //create fixture //set object's starting velocity //create instance of image and add it to stage at correct position //link image to body (and vice-versa) //add required event listener to image }
For our round objects, some of these steps are identical, and most of them only differ by a couple of parameters or instance names. So, what if we could inherit all that identical code from some general createRoundObject()
function?
We can't do exactly that, but we can do something similar. Create a new folder in \src\
called \builders\
, and create a new class inside that called RoundObjectBuilder.as
:
package builders { public class RoundObjectBuilder { public function RoundObjectBuilder() { } } }
This is going to take a b2world and create a b2Body, so add appropriate code for that:
package builders { import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2World; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; //sets this.world to parameter called world this.pxToM = pxToM; } } }
It's going to create a round object, so we'll give it a function to create such a shape:
package builders { import Box2D.Collision.Shapes.b2Shape; import Box2D.Collision.Shapes.b2CircleShape; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2World; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2Shape; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; this.pxToM = pxToM; } public function createShape():void { shape = new b2CircleShape(mRadius); } } }
(Where's the radius? You'll see...)
It's going to create a fixture def, so create a function for that, too:
package builders { import Box2D.Collision.Shapes.b2Shape; import Box2D.Collision.Shapes.b2CircleShape; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2FixtureDef; import Box2D.Dynamics.b2World; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2CircleShape; protected var fixtureDef:b2FixtureDef; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; this.pxToM = pxToM; } public function createShape():void { shape = new b2CircleShape(mRadius); } public function createFixtureDef():void { fixtureDef = new b2FixtureDef(); fixtureDef.shape = shape; fixtureDef.restitution = restitution; fixtureDef.friction = friction; fixtureDef.density = density; } } }
(Where do we define the properties? Again, you'll see...)
It's going to create a body def, too:
package builders { import Box2D.Collision.Shapes.b2Shape; import Box2D.Collision.Shapes.b2CircleShape; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2BodyDef; import Box2D.Dynamics.b2FixtureDef; import Box2D.Dynamics.b2World; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2CircleShape; protected var fixtureDef:b2FixtureDef; protected var bodyDef:b2BodyDef; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; this.pxToM = pxToM; } public function createShape():void { shape = new b2CircleShape(mRadius); } public function createFixtureDef():void { fixtureDef = new b2FixtureDef(); fixtureDef.shape = shape; fixtureDef.restitution = restitution; fixtureDef.friction = friction; fixtureDef.density = density; } public function createBodyDef(mStartX:Number, mStartY:Number):void { bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.position.Set(mStartX, mStartY); } } }
It's going to create an actual body and fixture, and set the body's initial velocity:
package builders { import Box2D.Collision.Shapes.b2Shape; import Box2D.Collision.Shapes.b2CircleShape; import Box2D.Common.Math.b2Vec2; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2BodyDef; import Box2D.Dynamics.b2Fixture; import Box2D.Dynamics.b2FixtureDef; import Box2D.Dynamics.b2World; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2CircleShape; protected var fixtureDef:b2FixtureDef; protected var bodyDef:b2BodyDef; protected var fixture:b2Fixture; protected var startingVelocity:b2Vec2; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; this.pxToM = pxToM; } public function createShape():void { shape = new b2CircleShape(mRadius); } public function createFixtureDef():void { fixtureDef = new b2FixtureDef(); fixtureDef.shape = shape; fixtureDef.restitution = restitution; fixtureDef.friction = friction; fixtureDef.density = density; } public function createBodyDef(mStartX:Number, mStartY:Number):void { bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.position.Set(mStartX, mStartY); } public function createBody():void { body = world.CreateBody(bodyDef); } public function createFixture():void { fixture = body.CreateFixture(fixtureDef); } public function setStartingVelocity(mVelocityX:Number, mVelocityY:Number):void { startingVelocity = new b2Vec2(mVelocityX, mVelocityY); body.SetLinearVelocity(startingVelocity); } } }
Next, we add the image:
package builders { import Box2D.Collision.Shapes.b2Shape; import Box2D.Collision.Shapes.b2CircleShape; import Box2D.Common.Math.b2Vec2; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2BodyDef; import Box2D.Dynamics.b2Fixture; import Box2D.Dynamics.b2FixtureDef; import Box2D.Dynamics.b2World; import flash.display.Bitmap; public class RoundObjectBuilder { public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2CircleShape; protected var fixtureDef:b2FixtureDef; protected var bodyDef:b2BodyDef; protected var fixture:b2Fixture; protected var startingVelocity:b2Vec2; protected var sprite:B2Sprite; public function RoundObjectBuilder(world:b2World, pxToM:Number) { this.world = world; this.pxToM = pxToM; } public function createShape():void { shape = new b2CircleShape(mRadius); } public function createFixtureDef():void { fixtureDef = new b2FixtureDef(); fixtureDef.shape = shape; fixtureDef.restitution = restitution; fixtureDef.friction = friction; fixtureDef.density = density; } public function createBodyDef(mStartX:Number, mStartY:Number):void { bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.position.Set(mStartX, mStartY); } public function createBody():void { body = world.CreateBody(bodyDef); } public function createFixture():void { fixture = body.CreateFixture(fixtureDef); } public function setStartingVelocity(mVelocityX:Number, mVelocityY:Number):void { startingVelocity = new b2Vec2(mVelocityX, mVelocityY); body.SetLinearVelocity(startingVelocity); } public function setImage():void { sprite = new B2Sprite(); var bitmap:Bitmap = new Image(); sprite.addChild(); bitmap.x -= bitmap.width / 2; bitmap.y -= bitmap.height / 2; body.SetUserData(sprite); sprite.body = body; } public function linkImageAndBody():void { body.SetUserData(sprite); sprite.body = body; } } }
Do you see where we're going with this? You might have guessed that we'll eventually create an instance of RoundObjectBuilder
and then call createShape()
, createFixtureDef()
, and so on, all in the right order, with the right parameters, then retrieve the b2Body object after everything's set up. That's correct, but there is a little more to it than that.
Step 9: Creating a Specific Builder
First, create protected variables to hold all the properties that are missing:
public var body:b2Body; protected var world:b2World, pxToM:Number; protected var shape:b2CircleShape; protected var fixtureDef:b2FixtureDef; protected var bodyDef:b2BodyDef; protected var fixture:b2Fixture; protected var startingVelocity:b2Vec2; protected var sprite:B2Sprite; protected var mRadius:Number; protected var restitution:Number, friction:Number, density:Number; protected var Image:Class;
Now, create a new class, in your \builders\
folder, called TennisBallBuilder.as
, and make sure it extends RoundObjectBuilder
:
package builders { import Box2D.Dynamics.b2World; public class TennisBallBuilder extends RoundObjectBuilder { public function TennisBallBuilder(world:b2World, pxToM:Number) { super(world, pxToM); } } }
(Remember, super()
will run the code in the constructor function of RoundObjectBuilder
, because we extended it.)
Now, underneath that call to super()
, set the restitution, friction, and density of the tennis ball:
package builders { import Box2D.Dynamics.b2World; public class TennisBallBuilder extends RoundObjectBuilder { public function TennisBallBuilder(world:b2World, pxToM:Number) { super(world, pxToM); restitution = 0.8; friction = 0.75; density = 1.0; } } }
Next, embed the tennis ball image, and use it to set the Image class which will be used to create the bitmap
; also, set the radius:
package builders { import Box2D.Dynamics.b2World; public class TennisBallBuilder extends RoundObjectBuilder { [Embed(source='../../lib/TennisBall.png')] public var TennisBall:Class; public function TennisBallBuilder(world:b2World, pxToM:Number) { super(world, pxToM); restitution = 0.8; friction = 0.75; density = 1.0; Image = TennisBall; mRadius = 35 * 0.5 * pxToM; } } }
So now we have a class that encapsulates all of the information and all the functions needed to create a tennis ball. We've just got to put it all together. Modify Main.createTennisBall()
like so:
private function createTennisBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var tennisBallBuilder:TennisBallBuilder = new TennisBallBuilder(world, pxToM); tennisBallBuilder.createShape(); tennisBallBuilder.createFixtureDef(); tennisBallBuilder.createBodyDef(pxStartX * pxToM, pxStartY * pxToM); tennisBallBuilder.createBody(); tennisBallBuilder.createFixture(); tennisBallBuilder.setStartingVelocity(mVelocityX, mVelocityY); tennisBallBuilder.setImage(); tennisBallBuilder.linkImageAndBody(); var tennisBallSprite:B2Sprite = tennisBallBuilder.body.GetUserData() as B2Sprite; tennisBallSprite.x = pxStartX; tennisBallSprite.y = pxStartY; this.addChild(tennisBallSprite); tennisBallSprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
If you test your SWF, you'll see that this still works:
But so what? We haven't gained much here. In fact, we've ended up with more code than we started with, and it still requires copying and pasting.
Time for one more improvement. Create a new class, in \builders\
, called RoundObjectDirector.as
:
package builders { public class RoundObjectDirector { public function RoundObjectDirector() { } } }
Here's how this will work:
- We'll tell the RoundObjectDirector to use a specific RoundObjectBuilder.
- We'll tell the RoundObjectDirector to create a new round object.
- The RoundObjectDirector will do so, using the RoundObjectBuilder that we specified, and will return said object.
Here's the code:
package builders { import Box2D.Dynamics.b2Body; public class RoundObjectDirector { protected var roundObjectBuilder:RoundObjectBuilder; public function RoundObjectDirector() { } public function setRoundObjectBuilder(builder:RoundObjectBuilder):void { this.roundObjectBuilder = builder; } public function createRoundObject(mStartX:Number, mStartY:Number, mVelocityX:Number, mVelocityY:Number):b2Body { roundObjectBuilder.createShape(); roundObjectBuilder.createFixtureDef(); roundObjectBuilder.createBodyDef(mStartX, mStartY); roundObjectBuilder.createBody(); roundObjectBuilder.createFixture(); roundObjectBuilder.setStartingVelocity(mVelocityX, mVelocityY); roundObjectBuilder.setImage(); roundObjectBuilder.linkImageAndBody(); return roundObjectBuilder.body; } } }
See how most of the code in createRoundObject()
is taken from our Main.createNewTennisBall()
function? All you have to do is replace "tennisBall" with "roundObject" in each case. (Well, okay, I've also changed all measurements to be in meters rather than pixels, since we're not dealing with any images here.)
Now we can modify Main.createNewTennisBall()
like so:
private function createTennisBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var tennisBallBuilder:TennisBallBuilder = new TennisBallBuilder(world, pxToM); var roundObjectDirector:RoundObjectDirector = new RoundObjectDirector(); roundObjectDirector.setRoundObjectBuilder(tennisBallBuilder); var tennisBallBody:b2Body = roundObjectDirector.createRoundObject(pxStartX * pxToM, pxStartY * pxToM, mVelocityX, mVelocityY); var tennisBallSprite:B2Sprite = tennisBallBody.GetUserData() as B2Sprite; tennisBallSprite.x = pxStartX; tennisBallSprite.y = pxStartY; this.addChild(tennisBallSprite); tennisBallSprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
(Note that I've converted the starting positions to pixels here, before passing them to the Director.)
Step 10: Creating Rubber Balls
Let's create a brand new type of object, and see how much faster it is. We'll use a super-bouncy rubber ball:

Mine is 15x15px and called RubberBall.png.
First step: create a new class, extending RoundObjectBuilder, to create the rubber ball:
package builders { import Box2D.Dynamics.b2World; public class RubberBallBuilder extends RoundObjectBuilder { [Embed(source='../../lib/RubberBall.png')] public var RubberBall:Class; public function RubberBallBuilder(world:b2World, pxToM:Number) { super(world, pxToM); restitution = 1.0; friction = 0.0; density = 5.0; Image = RubberBall; mRadius = 15 * 0.5 * pxToM; } } }
Second step: create Main.createRubberBall()
:
private function createRubberBall(pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { var rubberBallBuilder:RubberBallBuilder = new RubberBallBuilder(world, pxToM); var roundObjectDirector:RoundObjectDirector = new RoundObjectDirector(); roundObjectDirector.setRoundObjectBuilder(rubberBallBuilder); var rubberBallBody:b2Body = roundObjectDirector.createRoundObject(pxStartX * pxToM, pxStartY * pxToM, mVelocityX, mVelocityY); var rubberBallSprite:B2Sprite = rubberBallBody.GetUserData() as B2Sprite; rubberBallSprite.x = pxStartX; rubberBallSprite.y = pxStartY; this.addChild(rubberBallSprite); rubberBallSprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
Third step: create the actual objects:
for (var i:int = 0; i < 10; i++) { createTennisBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBasketball( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createRubberBall( 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 it. Easy.
Step 11: Getting Even More General
I can't help myself. I have to make one more simplification.
Did you notice that Main.createRubberBall()
and Main.createTennisBall()
are almost identical? The only things that differ are the type of RoundObjectBuilder and the variable names.
Let's combine them into a single function: Main.createRoundObject()
:
private function createRoundObject(roundObjectBuilder:RoundObjectBuilder, pxStartX:Number, pxStartY:Number, mVelocityX:Number, mVelocityY:Number):void { roundObjectDirector.setRoundObjectBuilder(roundObjectBuilder); var body:b2Body = roundObjectDirector.createRoundObject(pxStartX * pxToM, pxStartY * pxToM, mVelocityX, mVelocityY); var sprite:B2Sprite = body.GetUserData() as B2Sprite; sprite.x = pxStartX; sprite.y = pxStartY; this.addChild(sprite); sprite.addEventListener(MouseEvent.CLICK, onClickClickableImage); }
I've also made roundObjectDirector
a protected variable, belonging to Main
, so that we don't have to keep creating a new one:
protected var roundObjectDirector:RoundObjectDirector = new RoundObjectDirector();
Delete the createTennisBall()
and createRubberBall()
functions.
Now we can alter the code that creates objects, from this:
for (var i:int = 0; i < 10; i++) { createTennisBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBasketball( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createRubberBall( 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 ) }
...to this:
var tennisBallBuilder:TennisBallBuilder = new TennisBallBuilder(world, pxToM); var rubberBallBuilder:RubberBallBuilder = new RubberBallBuilder(world, pxToM); for (var i:int = 0; i < 10; i++) { createRoundObject( tennisBallBuilder, Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBowlingBall( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createBasketball( Math.random() * (stage.stageWidth - 20) + 10, Math.random() * (stage.stageHeight - 20) + 10, (Math.random() * 100) - 50, 0 ); createRoundObject( rubberBallBuilder, 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 ) }
The SWF still works:
Excellent.
Conclusion
I'll admit; that took us a long time, and didn't give us a spectactuarly different SWF. I hope you agree that the cleaner code was worth it, though.
This new approach to creating object (called the Builder pattern) is going to make it very easy for us to create new types of object in the future, to add them to the world, and to make changes to all of them at once by modifying the base Builder.
If you're feeling a little left behind by the pace of this tutorial, don't worry; we ended up focusing on design patterns rather than Box2D in particular here, but in the next part of the series we'll get back to Box2D's built-in features. In particular, we'll be working our way towards a physics-powered, keyboard-controlled, 2D platform game!
Until then, I suggest you do the following:
- Change your code for creating basketballs, bowling balls, and wheels so that they also use Builders.
- Create a new round object (medicine ball? ping-pong ball?), with a new image and set of properties, using Builders.
- If you're up for a challenge, have a go at creating a brand new set of builder for rectangular objects, like the crate.
Good luck! If you've got any questions, please ask them below.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post