Box2D Part 3: Anti-Gravity

Can you get the objects to float in space using your mouse? This is the conclusion to the three part series on using the Box2D Physics engine with ActionScript.

Box2D Anti-Gravity Example:

Box2D Anti-Gravity Example

Get Adobe Flash player

(click and hold the mouse down for anti-gravity)

Now that we have a Box2D environment set up and lots of polygons in it, let’s make it cool.

Anti-gravity boolean.
Add this boolean and if statement into your shape environment class.

public var counterGravity:Boolean = false;

override protected function update(e:Event):void
{
	// ...(from part 2)... //
		if (bb.m_userData is Sprite) {
			if(counterGravity){
				world.SetGravity(new b2Vec2(0,-9.8));
				bb.WakeUp();
			}else{
				world.SetGravity(new b2Vec2(0,9.8));
			}
	// ...(from part 2)... // 
}

Now add a mouse event handler and the following functions into your main class.

private function addCounterGravity(event:MouseEvent):void
{
	_shape2d.counterGravity = true;
}

private function removeCounterGravity(event:MouseEvent):void
{
	_shape2d.counterGravity = false;
}		

Now you have anti-gravity!

Miss out on the basics? Check out part 1 and part 2 of this series.

Full source code here.

Box2d Part 2: Shape Factory

Now that we have created our small world using the Box2D physic engine let’s start creating shapes! The shape factory will allow you to define any shapes you would like to add to your world. In this example we will create polygons.

Polygons in this example will be defined as a shape with 3 or more vertices with sides of equal length.

There are three steps in creating the object. Create the sprite, assign it to a body and generate a physics object. Keep in mind the sprite and the physics object to not need to be the same shape but they generally are. You could have a square displayed that acts in the physics world as a circle.

Create your sprite

public static function generatePolySprite(polyVO:PolyShapeVO):Sprite
{
	var polyClip:Sprite = new Sprite();
	polyClip.graphics.beginFill(polyVO.color);
	var tempPoints:Array = new Array()
	var ratio:Number = 360/polyVO.numSides;
	for(var i:int = 0;i <= 360;i += ratio)
	{
		var xx:Number=Math.sin(radians(i))*polyVO.radius;
		var yy:Number=(polyVO.radius-Math.cos(radians(i))*polyVO.radius);
		tempPoints.push(new Point(xx,yy));
	}
	
	polyClip.graphics.moveTo(tempPoints[tempPoints.length - 1].x, tempPoints[tempPoints.length - 1].y);
	for(var n:int = 0; n < tempPoints.length; n++){
		polyClip.graphics.lineTo(tempPoints[n].x, tempPoints[n].y);
	}
	return polyClip;
}

public static function radians(n:Number):Number 
{
	return(Math.PI/180*n);
}

Body definition.
The next function we will want is a body definition which will create an object in the physics world and assign our sprite to that object.

public static function generateBodyDef(shapeSprite:Sprite, box_width:int, box_height:int):b2BodyDef
{
	var result:b2BodyDef = new b2BodyDef();
	
	// Randomly place the shape.
	result.position.Set((Math.random() * (box_width - 200) + 100)/Constants.PHYS_SCALE, (Math.random() * (box_height - 200) + 100)/ Constants.PHYS_SCALE);
	
	// Set the userData (object Sprite)
	result.userData = shapeSprite;
	
	return result;
}		

And finally create the polygon definition.

public static function generatePolyDef(polyVO:PolyShapeVO):b2PolygonDef
{
	var result:b2PolygonDef = new b2PolygonDef();
	result.vertexCount = polyVO.numSides;
	
	var xf1:b2XForm = new b2XForm();
	
	var tempPoints:Array = new Array()
	var ratio:Number = 360/polyVO.numSides;
	for(var i:int = 0;i <= 360;i += ratio)
	{
		var xx:Number=Math.sin(radians(i))*polyVO.radius;
		var yy:Number=(polyVO.radius-Math.cos(radians(i))*polyVO.radius);
		tempPoints.push(new Point(xx,yy));
	}
	
	for(var n:int = 0; n < tempPoints.length; n++){
		result.vertices[n] = b2Math.b2MulX(xf1, new b2Vec2(tempPoints[n].x/Constants.PHYS_SCALE, tempPoints[n].y/Constants.PHYS_SCALE));
	}
				
	result.density = 10;
	result.restitution = Constants.RESTITUTION;
	result.friction = Constants.FRICTION;
	
	return result;
}

Now let’s extend our base environment from part 1 to allow for the addition of shapes.

public function addPoly(shapeVO:PolyShapeVO):void
{
	var squareClip:Sprite = ShapeFactory.generatePolySprite(shapeVO);
	
	if(debug_mode)
	{squareClip.alpha = 0.5;}
	
	addChild(squareClip);

	var bd:b2BodyDef = ShapeFactory.generateBodyDef(squareClip, box_width, box_height);
	var cd:b2PolygonDef = ShapeFactory.generatePolyDef(shapeVO);
	
	addShape(bd, cd);			
}

override protected function update(e:Event):void
{
	super.update(e);

 	for (var bb:b2Body = world.m_bodyList; bb; bb=bb.m_next) {
		if (bb.m_userData is Sprite) {
			bb.m_userData.x = bb.GetPosition().x * Constants.PHYS_SCALE;
			bb.m_userData.y = bb.GetPosition().y * Constants.PHYS_SCALE;
			bb.m_userData.rotation = bb.GetAngle() * 180 / Math.PI;
		}
	}  
}

private function addShape(bd:b2BodyDef, cd:b2ShapeDef):void
{
	var x_speed:Number = Math.random() * 10;
	
	body = world.CreateBody(bd);
	body.CreateShape(cd);
	body.SetMassFromShapes();
	
	// Assign a random speed to the shape
	body.m_linearVelocity.x = x_speed;
	body.m_linearVelocity.y = 10 - x_speed;						
}

Finally we will add logic to our main class to create the shapes.

private var _colors:Array = new Array(0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF);
private var _shape2d:ShapeEnvironment;

public function Box2DQuickStart()
{
	stage.scaleMode = StageScaleMode.NO_SCALE;
	stage.align = StageAlign.TOP_LEFT;
				
	_shape2d = new ShapeEnvironment(380, 280, true);
	_shape2d.x = 10;
	_shape2d.y = 10;
	
	//Add polygons
	for(var i:int = 1;i <=45; i++){
		var tempShapeVO:PolyShapeVO = new PolyShapeVO();
		tempShapeVO.color = _colors[i % _colors.length];
		tempShapeVO.numSides = 3 + i % 10;
		tempShapeVO.radius = 5 + Math.random()*20;
		_shape2d.addPoly(tempShapeVO);
	}
	
 	
	_shape2d.start();
	
	this.addChild(_shape2d);
}

Check out the final product along with some Anti-Gravity in part 3.

Box2D Part 1: It’s a Small World

The Box2d Physics Engine for ActionScript from the Ground Up

We’ll start with the basic 2d environment (part 1) and work our way into creating dynamic polygons (part 2) with an anti-gravity effect (part 3).

Here’s a preview of what we will end up with:

Google Maps Text Event

Get Adobe Flash player

(click and hold the mouse down for anti-gravity)

What is Box2d?
It’s a physics engine that was written in C++ and ported over to ActionScript. Learn more about it here.

Now what?
Download the framework here. Start a new ActionScript project and put the Box2D folder into your src directory.

Set up some constants.
Boring! I know, but you’ll be happy later :) Luckily they are all commented so you know what effect they will have when we start adding objects. You’ll notice the PHYS_SCALE variable. The physics environment is not one to one with the stage in Flash. This scale is used to translate coordinates between the two environments.

// Number of pixels in a Meter (Usually between 10 and 30).
// The closer to 1, the more accurate. And more processor intensive.
public static const PHYS_SCALE:Number = 10; 

// Restitution is used to make objects bounce (usually set to be between 0 and 1).
// 0 means the ball won't bounce. 1 means the ball's velocity will be exactly reflected. 
public static const RESTITUTION:Number = 0.7;

// Friction is used to make objects slide along each other (usually set to be between 0 and 1).
// 0 turns off friction and a value of one makes the friction strong. 
public static const FRICTION:Number = 0.6;

// How many physical calculations per timestep.
// Usually 1 / 15 or 1 / 60.
public static const TIMESTEP:Number = 1.0 / 15.0;  

// The suggested iteration count is 10.
public static const ITERATIONS:Number = 10.0;

Create a base 2d environment.
Starting simple. We need a world. The upper bound and lower bound are in the physics scale and should be larger than your current stage. Objects will stay alive as long as they are in the bounds of the world. Right now that is 100 * 10 (our physics scale) = 1000 pixels positive and negative.

protected var world:b2World;

public function Base2dEnvironment()
{
	var gravity:b2Vec2 = new b2Vec2(0,9.8);
	var worldAABB:b2AABB = new b2AABB();
	
	worldAABB.lowerBound.Set(-100,-100);
	worldAABB.upperBound.Set(100,100);
	world = new b2World(worldAABB, gravity, true);
}

We also need a way to kickstart our world.

protected var running:Boolean = false;

public function start():void
{
	if(!running){
		addEventListener(Event.ENTER_FRAME, update, false, 0, true);
		running = true;
	}
}

public function pause():void
{
	if(running){
		removeEventListener(Event.ENTER_FRAME, update);
		running = false;
	}
}

protected function update(e:Event):void {
	world.Step(timestep, iterations);
}

Now it’s time to add objects right? Well, first we want some boundaries so our objects don’t fall out of our display area. And how about a debug mode so we can actually see our invisible boundaries?

protected var box_width:int;
protected var box_height:int;
protected var debug_mode:Boolean;
protected var margin:int = 5;

public function Base2dEnvironment(boxWidth:int = 800, boxHeight:int = 500, debugMode:Boolean = false)
{		
	// ...(world code from above)... //

	box_width = boxWidth;
	box_height = boxHeight;
	debug_mode = debugMode;

	//Shows boundries (green transparency) used to see 'Ground'
	if(debugMode){
		drawGroundBoundries();
	}
	
 	// Bottom wall
 	AddStaticBox((box_width / 2)/Constants.PHYS_SCALE, (box_height + margin)/Constants.PHYS_SCALE, (box_width / 2) / Constants.PHYS_SCALE, margin / Constants.PHYS_SCALE);			
	// Top wall
	AddStaticBox((box_width / 2)/Constants.PHYS_SCALE, -margin / Constants.PHYS_SCALE, (box_width / 2) / Constants.PHYS_SCALE, margin / Constants.PHYS_SCALE);
	// Left wall
	AddStaticBox(-margin / Constants.PHYS_SCALE,(box_height / 2) / Constants.PHYS_SCALE, margin / Constants.PHYS_SCALE,(box_height / 2 ) / Constants.PHYS_SCALE);
	// Right wall
	AddStaticBox((box_width + margin) / Constants.PHYS_SCALE,(box_height / 2)/Constants.PHYS_SCALE, margin / Constants.PHYS_SCALE,((box_height) / 2) / Constants.PHYS_SCALE);
}

private function drawGroundBoundries():void
{
	var m_sprite:Sprite;
	m_sprite = new Sprite();
	addChild(m_sprite);
	
	var dbgDraw:b2DebugDraw = new b2DebugDraw();
	var dbgSprite:Sprite = new Sprite();
	m_sprite.addChild(dbgSprite);
	dbgDraw.m_sprite = m_sprite;
	dbgDraw.m_drawScale = Constants.PHYS_SCALE;
	dbgDraw.m_alpha = 1;
	dbgDraw.m_fillAlpha = 0.5;
	dbgDraw.m_lineThickness = 1;
	dbgDraw.m_drawFlags = b2DebugDraw.e_shapeBit;
	world.SetDebugDraw(dbgDraw);
}

// Used for drawing the ground
private function AddStaticBox(_x:Number,_y:Number,_halfwidth:Number,_halfheight:Number):void {
	var bodyDef:b2BodyDef = new b2BodyDef();
	bodyDef.position.Set(_x, _y);
	var boxDef:b2PolygonDef = new b2PolygonDef();
	boxDef.SetAsBox(_halfwidth, _halfheight);
	boxDef.density = 0.0;
	var body:b2Body = world.CreateBody(bodyDef);
	body.CreateShape(boxDef);
	body.SetMassFromShapes();
}		

Now we have our world! Let’s start adding objects. Click here for part 2.

Google Maps: InfoWindowOptions and TextEvents

Problem:
Info windows in Google maps are great but how do you pick up specific text events? It is very common to have a ‘learn more’ or ‘click here’ link nested in your text fields. Without direct access to the TextField class in the InfoWindowOptions how do we pick up these events?

Solution:
Well, we could set the customContent variable and pass in a custom DisplayObject but that sounds like a lot of work just to get some simple interaction with the text. Luckily the TextEvent class bubbles by default. This is important since we do not have direct access to the TextField within the info window.

Sample:

Google Maps Text Event

Get Adobe Flash player

(click learn more, close the window and click on the map to re-open)

Code:

private var _map:Map;
private var _infoWindow:InfoWindowOptions;
private var _ready:Boolean = false;

public function GoogleMapsTextEvent()
{
	stage.scaleMode = StageScaleMode.NO_SCALE;
	stage.align = StageAlign.TOP_LEFT;

	_map = new Map();
	_map.key = "YOUR GOOGLE MAPS KEY";
	_map.setSize(new Point(300, 300));
	_map.addEventListener(MapEvent.MAP_READY, onMapReady);
	_map.addEventListener(MapMouseEvent.CLICK, addInfoWindow);
	
	_infoWindow = new InfoWindowOptions();

	this.addChild(_map);
}

private function onMapReady(event:Event):void 
{
	_ready = true;
	_map.setCenter(new LatLng(40.736072,-73.992062), 14, MapType.NORMAL_MAP_TYPE);
	_map.addEventListener(TextEvent.LINK, testFunction, false, 0, true);
	addInfoWindow();
}

private function addInfoWindow(event:MapMouseEvent = null):void
{
	if(_ready){
		_infoWindow.contentHTML = "Simple... <a href='event:myEvent'>Learn More.</a>";
		_map.openInfoWindow(_map.getCenter(), _infoWindow);
	}
}

private function testFunction(event:TextEvent):void
{
	_infoWindow.contentHTML = "Extended text.";
	_map.openInfoWindow(_map.getCenter(), _infoWindow);
}

Useful Links:
InfoWindowOptions
TextEvent
Custom Google Map Markers

Enjoy!