How to create destructible terrain using Box2D – step 2
Talking about Actionscript 3, Box2D and Flash.
In the second step of this tutorial we’ll see how to turn the terrain created with geometry in step 1 into a real Box2D terrain.
I am going to use b2Separator class By Antoan Angelov to render any type of polygon with Box2D.
This will allow you to quickly render any kind of polygon but won’t save us from the infamous hole bug. This happens when one or more points of the “circle” which will represents the hole lie on a segment of a polygon:

I (obviously) found a solution for this bug, and I am going to show you the final result, next time, meanwhile have a look at this step, here is the source code with every line commented and highlights on new pieces of code.
package {
import flash.display.Sprite;
import flash.geom.Point;
import flash.events.MouseEvent;
import flash.events.Event;
import com.logicom.geom.Clipper;
import com.logicom.geom.ClipType;
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import Box2DSeparator.*;
public class Main extends Sprite {
// the canvas where we'll draw the terrain
private var terrainCanvas:Sprite=new Sprite();
// the array of polygons forming the terrain
private var terrainPolygons:Array=new Array();
// Box2D world
private var world:b2World=new b2World(new b2Vec2(0,5),true);
// old worldScale conversion
private var worldScale:Number=30;
public function Main():void {
addChild(terrainCanvas);
// creation of a 13x8 grid of squares, this will be our terrain
for (var i:Number=0; i<13; i++) {
for (var j:Number=0; j<8; j++) {
var thePoly:Array = new Array(new Point(-5+i*50,80+j*50),new Point(45+i*50,80+j*50),new Point(45+i*50,130+j*50),new Point(-5+i*50,130+j*50));
terrainPolygons.push(thePoly);
}
}
// drawing the terrain
drawTerrain();
// placing debug draw over the terrain, so you can see if geometry and physics terrain match
debugDraw();
// listeners: basically we destroy the terrain with a mouse click or a mouse drag
stage.addEventListener(MouseEvent.MOUSE_DOWN,function(){stage.addEventListener(MouseEvent.MOUSE_MOVE,doExplosion)});
stage.addEventListener(MouseEvent.MOUSE_UP,function(){stage.removeEventListener(MouseEvent.MOUSE_MOVE,doExplosion)});
stage.addEventListener(MouseEvent.CLICK,doExplosion);
//
addEventListener(Event.ENTER_FRAME,update);
}
// the core of the script, doExplosion function
private function doExplosion(e:MouseEvent):void {
// first we are going to remove all "rock" bodies. This can be optimized by removing only
// bodies affected by the explosion
for (var currentBody:b2Body=world.GetBodyList(); currentBody; currentBody=currentBody.GetNext()) {
if (currentBody.GetUserData()&¤tBody.GetUserData()=="rock") {
world.DestroyBody(currentBody);
}
}
// creation of an explosion polygon, looking like a circle, obviously it can be any shape you want
var explosionPolygon:Array=createCircle(20,new Point(mouseX,mouseY),30);
// for each existing terrain polygon, check the difference between the polygon itself and the
// explosion polygon. This should be optimized in some way, checking only for terrain polygons
// which are actually affected by the explosion.
// Then we remove the terrain polygon from the array, and we add the resulting polygon(s) after
// difference is calculated.
for (var i:Number=terrainPolygons.length-1; i>=0; i--) {
var resultPolygons:Array=Clipper.clipPolygon(terrainPolygons[i],explosionPolygon,ClipType.DIFFERENCE);
var totalArea:Number=0;
terrainPolygons.splice(i,1);
for (var j:Number=0; j<resultPolygons.length; j++) {
terrainPolygons.push(resultPolygons[j]);
}
}
// now it's time to redraw the terrain
drawTerrain();
}
// function to create a "circular" polygon
private function createCircle(precision:Number,origin:Point,radius:Number):Array {
var angle:Number=2*Math.PI/precision;
var circleArray:Array=new Array();
for (var i:Number=0; i<precision; i++) {
circleArray.push(new Point(origin.x+radius*Math.cos(angle*i),origin.y+radius*Math.sin(angle*i)));
}
return circleArray;
}
// function to create a body from a polygon
private function createBody(a:Array):void {
// using b2Separator class. Will need optimization to avoid the infamous hole error
var sep:b2Separator = new b2Separator();
// creation of the body definition
var bodyDef:b2BodyDef = new b2BodyDef();
// we always place the body at (0,0) then adjust fixture position
bodyDef.position.Set(0,0);
// some userData stuff to let the world know we are dealing with the terrain
bodyDef.userData="rock";
// body creation
var body:b2Body=world.CreateBody(bodyDef);
// fixture definition, this is where b2Separator comes into play
var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.restitution=0.5;
fixtureDef.friction=0;
// now we create a vector and we place inside the vector all points we found in a array
// translated into their b2Vec2 counterparts
var vec:Vector.<b2Vec2> = new Vector.<b2Vec2>();
for (var i:Number=0; i<a.length; i++) {
vec.push(new b2Vec2(a[i].x/worldScale,a[i].y/worldScale));
}
// finally we call Separate method
sep.Separate(body,fixtureDef,vec,30);
}
// these remaining functions are not interesting, they just display the terrain
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 update(e:Event):void {
world.Step(1/30,10,10);
world.ClearForces();
world.DrawDebugData();
}
private function drawTerrain() {
terrainCanvas.graphics.clear();
for (var i:Number=0; i<terrainPolygons.length; i++) {
drawPolygon(terrainPolygons[i],terrainCanvas,0x0000FF);
createBody(terrainPolygons[i]);
}
}
private function drawPolygon(polygon:Array,canvas:Sprite,color:Number):void {
canvas.graphics.lineStyle(0.1,0xffffff);
canvas.graphics.beginFill(color);
var n:uint=polygon.length;
if (n<3) {
return;
}
var p:Point=polygon[0];
canvas.graphics.moveTo(p.x, p.y);
for (var i:Number = 1; i <= n; ++i) {
p=polygon[i%n];
canvas.graphics.lineTo(p.x, p.y);
}
}
}
}
And this is the result:
Click or drag on the terrain to create a hole or dig. Watch out for the infamous bug.
Download the source code. Next time, the final prototype.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.