Talking about Actionscript 3, Box2D and Flash.
After showing you the video of my work in progress engine, it’s time to tell you how to split, cut and slice objects with Box2D version 2.1
There’s a lot to say so let’s start at once.
Drawing the laser
Drawing the laser it’s easy, as it’s the old “press the mouse, move the mouse, release the mouse” action you saw in a million drawing games.
Being the laser a straight line, once the player presses the mouse we will show a line connecting the starting point to the current mouse position, and when the player releases the mouse button we’ll show the final laser ray.
This way the laser will be defined by two points: the starting point and the ending point. Such points will be saved in a b2Segment
variable. It’s not mandatory, but since Box2D gives us a segment definition, why not using it?
This is the script:
package {
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import flash.display.Sprite;
import flash.events.MouseEvent;
public class Main extends Sprite {
private var worldScale:int=30;
private var canvas:Sprite;
private var laserSegment:b2Segment;
private var drawing:Boolean=false;
public function Main() {
canvas = new Sprite();
addChild(canvas);
stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
stage.addEventListener(MouseEvent.MOUSE_UP,mouseReleased);
}
private function mousePressed(e:MouseEvent):void {
drawing=true;
laserSegment=new b2Segment();
laserSegment.p1=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function mouseMoved(e:MouseEvent):void {
if (drawing) {
canvas.graphics.clear();
canvas.graphics.lineStyle(1,0xff0000);
canvas.graphics.moveTo(laserSegment.p1.x*worldScale,laserSegment.p1.y*worldScale);
canvas.graphics.lineTo(mouseX,mouseY);
}
}
private function mouseReleased(e:MouseEvent):void {
drawing=false;
laserSegment.p2=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
}
}
Let’s see the variables used:
worldScale: this is used to convert Box2D meters to Flash pixels. If you aren’t used to Box2D pixels and meters, read this post. In our case, 1 meter will be represented by 30 pixels.
canvas: it’s the Sprite we will use to draw the laser.
laserSegment: the b2Segment variable used to store laser starting and ending point.
drawing: a simple Boolean variable which tells us whether the player is drawing or not.
Then we have three listeners: one when the mouse is pressed, one when the mouse is moved and one when the mouse is released.
When the mouse is pressed, we set drawing
to true
because we are actually drawing, we construct a new b2Segment
instance and we set its p1
property (the starting point) to a new b2Vec2
variable (think about it just like a Flash Point) with the current x and y mouse coordinates translated into Box2D units.
When the mouse is moved we check if we are drawing, then we draw the laser (a thin red line) from the starting point (the p1
property of laserSegment
) to current mouse position.
When the mouse is released, we need to set drawing to false as we aren’t drawing anymore, and set p2 property (the ending point) of our segment to a new b2Vec2
variable with the current x and y mouse coordinates translated into Box2D units, just as we made when we pressed the mouse.
At this time, you are able to draw lasers with your mouse. Try it by yourself:
Click and drag the mouse to draw lasers.
Now we need some object to be cut.
Adding objects
We are going to add three static elements: a floor, a box and a circle. In this step we are only adding objects, so most of you could find this section a bit boring, but I would like you to focus how I am creating the circle: since Box2D does not natively support arcs, I am using a 12 vertices polygon to approximate the circle. Feel free to use a 24 or 36 vertices polygon to achieve a better approximation, I used a low vertices polygon for a teaching purpose. In an everyday project I’d use a 36 vertices polygon.
This is the script at this stage:
package {
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.Event;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
private var canvas:Sprite;
private var laserSegment:b2Segment;
private var drawing:Boolean=false;
public function Main() {
debugDraw();
addStuff();
canvas = new Sprite();
addChild(canvas);
addEventListener(Event.ENTER_FRAME, updateWorld);
stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
stage.addEventListener(MouseEvent.MOUSE_UP,mouseReleased);
}
private function mousePressed(e:MouseEvent):void {
drawing=true;
laserSegment=new b2Segment();
laserSegment.p1=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function mouseMoved(e:MouseEvent):void {
if (drawing) {
canvas.graphics.clear();
canvas.graphics.lineStyle(1,0xff0000);
canvas.graphics.moveTo(laserSegment.p1.x*worldScale,laserSegment.p1.y*worldScale);
canvas.graphics.lineTo(mouseX,mouseY);
}
}
private function mouseReleased(e:MouseEvent):void {
drawing=false;
laserSegment.p2=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw = new b2DebugDraw();
var debugSprite:Sprite = new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function addStuff():void {
var floorBody:b2BodyDef= new b2BodyDef();
floorBody.position.Set(11,16);
var floorShape:b2PolygonShape = new b2PolygonShape();
floorShape.SetAsBox(15,0.5);
var floorFixture:b2FixtureDef = new b2FixtureDef();
floorFixture.shape=floorShape;
var worldFloor:b2Body=world.CreateBody(floorBody);
worldFloor.CreateFixture(floorFixture);
//
var squareBody:b2BodyDef= new b2BodyDef();
squareBody.position.Set(16,5);
var squareShape:b2PolygonShape = new b2PolygonShape();
squareShape.SetAsBox(2.5,2.5);
var squareFixture:b2FixtureDef = new b2FixtureDef();
squareFixture.shape=squareShape;
var worldSquare:b2Body=world.CreateBody(squareBody);
worldSquare.CreateFixture(squareFixture);
//
var circleVector:Vector.=new Vector.();
var circleSteps:int=24;
var circleRadius:Number=3
for (var i:int=0; i
That's a lot of code, but most of it is used to enable the debug draw (lines 42-51) and to add the polygons (lines 52-85). Just look how I created the "circle" at lines 71-84 using a polygon created starting from a vector of b2Vec2
points, populated using trigonometry.
At the end of this step, this is what you have on your stage:
You are still able to draw the laser, and you have a nice set of objects ready to be sliced.
Detecting laser entry point
Since the laser is supposed to slice the objects, every affected object must have an entry point and an exit point. It's important to know the laser can slice more than one object at once.
This is an easy task thanks to b2World
's RayCast
method, which takes three arguments:
laserFired: the name of the callback function
laserSegment.p1: the laser starting point, in b2Vec2
format
laserSegment.p2: the laser ending point, in b2Vec2
format
laserFired
callback function already comes with a set of arguments which will do the job for us:
fixture: the fixture being hit by the laser.
point: the b2Vec2 point of contact (the entry point we are looking for).
normal: the normal vector at the point of intersection.
fraction: the fractional length along the ray of the intersection. You may find it useful if you want to know the ratio between the ray length and the point of contact.
It's important to know laserFired
is called for each body hit by the laser. If you don't want this to happen, return zero
and the ray detection will terminate. Returning one
, it will continue looking for other bodies.
Have a look at the code:
package {
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.Event;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
private var canvas:Sprite;
private var laserSegment:b2Segment;
private var drawing:Boolean=false;
public function Main() {
debugDraw();
addStuff();
canvas = new Sprite();
addChild(canvas);
addEventListener(Event.ENTER_FRAME, updateWorld);
stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
stage.addEventListener(MouseEvent.MOUSE_UP,mouseReleased);
}
private function mousePressed(e:MouseEvent):void {
drawing=true;
laserSegment=new b2Segment();
laserSegment.p1=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function mouseMoved(e:MouseEvent):void {
if (drawing) {
canvas.graphics.clear();
canvas.graphics.lineStyle(1,0xff0000);
canvas.graphics.moveTo(laserSegment.p1.x*worldScale,laserSegment.p1.y*worldScale);
canvas.graphics.lineTo(mouseX,mouseY);
}
}
private function mouseReleased(e:MouseEvent):void {
drawing=false;
laserSegment.p2=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw = new b2DebugDraw();
var debugSprite:Sprite = new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function addStuff():void {
var floorBody:b2BodyDef= new b2BodyDef();
floorBody.position.Set(11,16);
var floorShape:b2PolygonShape = new b2PolygonShape();
floorShape.SetAsBox(15,0.5);
var floorFixture:b2FixtureDef = new b2FixtureDef();
floorFixture.shape=floorShape;
var worldFloor:b2Body=world.CreateBody(floorBody);
worldFloor.CreateFixture(floorFixture);
//
var squareBody:b2BodyDef= new b2BodyDef();
squareBody.position.Set(16,5);
var squareShape:b2PolygonShape = new b2PolygonShape();
squareShape.SetAsBox(2.5,2.5);
var squareFixture:b2FixtureDef = new b2FixtureDef();
squareFixture.shape=squareShape;
var worldSquare:b2Body=world.CreateBody(squareBody);
worldSquare.CreateFixture(squareFixture);
//
var circleVector:Vector.=new Vector.();
var circleSteps:int=12;
var circleRadius:Number=3;
for (var i:int=0; i
At the moment the only thing we do in laserFired
function is drawing a small red circle to make you see the entry point.
Here it is the result:
Draw a laser to intersect one or more bodies and watch the red circle showing us the entry point. Now we have to determine the exit point.
Detecting laser exit point
Since RayCast
method performs a ray cast and does not simulate a laser passing through objects, there's no way in Box2D to determine the exit point.
Thank you for reading.
No... wait... since a laser is a straight line going from A to B, we can imagine another laser fired from B to A. B to A entry point will be A to B exit point. That is, only if the laser is large enough to cut the object, and that's what we want.
So with just another RayCast
call at line 91 we can do the job.
This is the code
package {
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.Event;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
private var canvas:Sprite;
private var laserSegment:b2Segment;
private var drawing:Boolean=false;
public function Main() {
debugDraw();
addStuff();
canvas = new Sprite();
addChild(canvas);
addEventListener(Event.ENTER_FRAME, updateWorld);
stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
stage.addEventListener(MouseEvent.MOUSE_UP,mouseReleased);
}
private function mousePressed(e:MouseEvent):void {
drawing=true;
laserSegment=new b2Segment();
laserSegment.p1=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function mouseMoved(e:MouseEvent):void {
if (drawing) {
canvas.graphics.clear();
canvas.graphics.lineStyle(1,0xff0000);
canvas.graphics.moveTo(laserSegment.p1.x*worldScale,laserSegment.p1.y*worldScale);
canvas.graphics.lineTo(mouseX,mouseY);
}
}
private function mouseReleased(e:MouseEvent):void {
drawing=false;
laserSegment.p2=new b2Vec2(mouseX/worldScale,mouseY/worldScale);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw = new b2DebugDraw();
var debugSprite:Sprite = new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function addStuff():void {
var floorBody:b2BodyDef= new b2BodyDef();
floorBody.position.Set(11,16);
var floorShape:b2PolygonShape = new b2PolygonShape();
floorShape.SetAsBox(15,0.5);
var floorFixture:b2FixtureDef = new b2FixtureDef();
floorFixture.shape=floorShape;
var worldFloor:b2Body=world.CreateBody(floorBody);
worldFloor.CreateFixture(floorFixture);
//
var squareBody:b2BodyDef= new b2BodyDef();
squareBody.position.Set(16,5);
var squareShape:b2PolygonShape = new b2PolygonShape();
squareShape.SetAsBox(2.5,2.5);
var squareFixture:b2FixtureDef = new b2FixtureDef();
squareFixture.shape=squareShape;
var worldSquare:b2Body=world.CreateBody(squareBody);
worldSquare.CreateFixture(squareFixture);
//
var circleVector:Vector.=new Vector.();
var circleSteps:int=12;
var circleRadius:Number=3;
for (var i:int=0; i
And this is the result:
Draw a laser line passing through one or more bodies to see the entry and the exit points.
And that's all for this post. Next time, we'll see in a few more steps how to physically slice the bodies.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.