How to create destructible terrain using Box2D – step 2

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()&&currentBody.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.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers