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.