Slicing, splitting and cutting objects with Box2D – part 2

Here we are with the second part of the Box2D splitter engine. In the first part of this tutorial we managed to find laser entry and exit points, now it’s time to make the laser cut the object.

In just three steps, we’ll have our object splitted, so let’s start:

Finding the centre of the laser ray

In this step we need to find the center of the laser ray inside the object. In other words, if we consider the segment going from the entry point to the exit point, we must find its centre point. This will help us a lot in the slicing process.

This is quite easy, although we have to modify a bit laserFired function

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;
		private var affectedByLaser:Vector.;
		private var entryPoint:Vector.;
		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);
			floorBody.userData=new Object();
			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();
				entryPoint=new Vector.();
				world.RayCast(laserFired,laserSegment.p1,laserSegment.p2);
				world.RayCast(laserFired,laserSegment.p2,laserSegment.p1);
				laserSegment=null;
			}
			world.DrawDebugData();
		}
		private function laserFired(fixture:b2Fixture,point:b2Vec2,normal:b2Vec2,fraction:Number):Number {
			var affectedBody:b2Body=fixture.GetBody()
			var fixtureIndex:int=affectedByLaser.indexOf(affectedBody)
			if (fixtureIndex==-1) {
				affectedByLaser.push(affectedBody);
				entryPoint.push(point);
			} else {
				drawCircle(entryPoint[fixtureIndex],0xff0000);
				drawCircle(point,0xffff00);
				var rayCenter:b2Vec2=new b2Vec2((point.x+entryPoint[fixtureIndex].x)/2,(point.y+entryPoint[fixtureIndex].y)/2);
				drawCircle(rayCenter,0xff8800);
			}
			return 1;
		}
		private function drawCircle(origin:b2Vec2,color:Number):void {
			canvas.graphics.lineStyle(2,color);
			canvas.graphics.drawCircle(origin.x*worldScale,origin.y*worldScale,5);
		}
	}
}

First, we added two new class level variables:

Line 15: affectedByLaser is a b2Body vector which will store all bodies affected by the laser

Line 16: entryPoint is a b2Vec2 vector which will store all entry points generated by the laser

Now let’s look at the new laserFired function (lines 101-114).

Now it’s not simply a matter of drawing a circle, we have to distinguish entry point from exit point as it could be useful if you want to add two different graphical effects, one for the entry point and one for the exit point, keep track of all bodies affected by the laser ray and finding the centre of each laser segment which is cutting the body.

Line 102: creates a b2Body variable called affectedBody which gets the body touched by the laser ray starting from the given fixture with getBody method.

Line 103: creates a variable called fixtureIndex which, using indexOf method, will tell us if affectedBody is already inside affectedByLaser vector.

Line 104: if fixtureIndex is equal to -1 we know affectedBody is not inside affectedByLaser vector, so we can say it’s the first time the laser hits affectedBody, and obviously we must be dealing with the entry point.

Lines 105-106: push both the affected body and the intersection point in affectedByLaser and entryPoint vectors respectively.

Line 107: what happens if fixtureIndex is different than -1? This means it’s not the first time we meet affectedBody, and this suggests us two things: it’s an exit point and affectedBody is spliced by the laser.

Line 108: draws the entry point

Line 109: draws the exit point

Line 110: finds the centre of the segment going from the entry point to the exit point

Lines 11: draws the centre point

And this is the result:

Try to slice bodies with the laser.

Now only bodies entirely sliced by the laser have entry, exit and centre points. In all other cases, when the laser intersects the body in just one point, we don’t do anything since the body is not sliced.

Entry point is marked with red, exit point with yellow and the centre with orange.

Defining new polygons vertices

Before you split the body in two, you have to know which vertices will be part of the first slice and wich ones will be part of the second slice.

How can we do it? The concept is simple: assuming the laser is an axis, all the vertices above such axis are part of a slice, and all the vertices below the axis are part of another slice. Both slices will also have laser entry and exit points among their vertices.

Time to add some code to laserFired function:

private function laserFired(fixture:b2Fixture,point:b2Vec2,normal:b2Vec2,fraction:Number):Number {
	var affectedBody:b2Body=fixture.GetBody();
	var affectedPolygon:b2PolygonShape=fixture.GetShape() as b2PolygonShape;
	var fixtureIndex:int=affectedByLaser.indexOf(affectedBody);
	if (fixtureIndex==-1) {
		affectedByLaser.push(affectedBody);
		entryPoint.push(point);
	} else {
		drawCircle(entryPoint[fixtureIndex],0xff0000);
		drawCircle(point,0xffff00);
		var rayCenter:b2Vec2=new b2Vec2((point.x+entryPoint[fixtureIndex].x)/2,(point.y+entryPoint[fixtureIndex].y)/2);
		var rayAngle:Number=Math.atan2(entryPoint[fixtureIndex].y-point.y,entryPoint[fixtureIndex].x-point.x);
		drawCircle(rayCenter,0xff8800);
		var polyVertices:Vector.=affectedPolygon.GetVertices();
		for (var i:int=0; i0&&cutAngle<=Math.PI) {
				drawCircle(worldPoint,0xffffff);
			} else {
				drawCircle(worldPoint,0x000000);
			}
		}
	}
	return 1;
}

Line 112: rayAngle is the angle of the laser segment which slices the polygon

Line 114: populates polyVertices vector with all affectedPolygon vertices thanks to GetVertices() method.

Line 115: looping through all vertices. It's time to see which ones will be part of the first slice, and which ones will be part of the second slice.

Line 116: worldPoint is a b2Vec2 variable which gets the vertex coordinates thanks to GetWorldPoint method

Line 117: the core of the script: cutAngle variable is the angle of the vertex using the laser as reference axis. This is when the centre of the laser segment comes into play.

Lines 121-125: each vertex is marked with a white or a black point according to its angle.

Now we can spot the vertices of the new slices:

Try to cut an object and you will see the vertices of the new slices marked in black and white.

Finally we can split the objects!

Splitting objects

Although this is the last step, it's not the easiest... we can say it's the hardest. But we'll manage to have our objects sliced.

First, some theory. As you can see from the previous movie, it's easy to visually find the vertices of the new slices: one is made by all white vertices and the entry and exit laser points, and another is made by all black vertices and the entry and exit laser points.

Putting it into coding is a bit more complicated because Box2D wants polygon vertices to be defined in clockwise order. In most sites and forums you may read it wants it into counter clockwise order, but they are wrong. Or, at least, they are wrong when talking about Box2D and Flash.

So the idea is: create one slice with the white points in clockwise order and append the exit and the entry laser points, and the other slice with black points in clockwise order and append the entry and exit laser points. Notice how laser points order is inverted when I switch from "white" to "black" slice.

Let's see the whole script:

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;
		private var affectedByLaser:Vector.;
		private var entryPoint:Vector.;
		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);
			floorBody.userData=new Object();
			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();
				entryPoint=new Vector.();
				world.RayCast(laserFired,laserSegment.p1,laserSegment.p2);
				world.RayCast(laserFired,laserSegment.p2,laserSegment.p1);
				laserSegment=null;
			}
			world.DrawDebugData();
		}
		private function laserFired(fixture:b2Fixture,point:b2Vec2,normal:b2Vec2,fraction:Number):Number {
			var affectedBody:b2Body=fixture.GetBody();
			var affectedPolygon:b2PolygonShape=fixture.GetShape() as b2PolygonShape;
			var fixtureIndex:int=affectedByLaser.indexOf(affectedBody);
			if (fixtureIndex==-1) {
				affectedByLaser.push(affectedBody);
				entryPoint.push(point);
			} else {
				var rayCenter:b2Vec2=new b2Vec2((point.x+entryPoint[fixtureIndex].x)/2,(point.y+entryPoint[fixtureIndex].y)/2);
				var rayAngle:Number=Math.atan2(entryPoint[fixtureIndex].y-point.y,entryPoint[fixtureIndex].x-point.x);
				var polyVertices:Vector.=affectedPolygon.GetVertices();
				var newPolyVertices1:Vector.=new Vector.();
				var newPolyVertices2:Vector.=new Vector.();
				var currentPoly:int=0;
				var cutPlaced1:Boolean=false;
				var cutPlaced2:Boolean=false;
				for (var i:int=0; i0&&cutAngle<=Math.PI) {
						if (currentPoly==2) {
							cutPlaced1=true;
							newPolyVertices1.push(point);
							newPolyVertices1.push(entryPoint[fixtureIndex]);
						}
						newPolyVertices1.push(worldPoint);
						currentPoly=1;
					} else {
						if (currentPoly==1) {
							cutPlaced2=true;
							newPolyVertices2.push(entryPoint[fixtureIndex]);
							newPolyVertices2.push(point);
						}
						newPolyVertices2.push(worldPoint);
						currentPoly=2;

					}
				}
				if (! cutPlaced1) {
					newPolyVertices1.push(point);
					newPolyVertices1.push(entryPoint[fixtureIndex]);
				}
				if (! cutPlaced2) {
					newPolyVertices2.push(entryPoint[fixtureIndex]);
					newPolyVertices2.push(point);
				}
				createSlice(newPolyVertices1,newPolyVertices1.length);
				createSlice(newPolyVertices2,newPolyVertices2.length);
				world.DestroyBody(affectedBody);
			}
			return 1;
		}
		private function findCentroid(vs:Vector., count:uint):b2Vec2 {
			var c:b2Vec2 = new b2Vec2();
			var area:Number=0.0;
			var p1X:Number=0.0;
			var p1Y:Number=0.0;
			var inv3:Number=1.0/3.0;
			for (var i:int = 0; i < count; ++i) {
				var p2:b2Vec2=vs[i];
				var p3:b2Vec2=i+1,numVertices:int):void {
			var centre:b2Vec2=findCentroid(vertices,vertices.length);
			for (var i:int=0; i

As usual, laserFired function does the hard job, saving the vertices of the new polygons in two vectors called newPolyVertices1 and newPolyVertices2 declared at lines 112-113.

Lines 117-149 populate such vectors with the vertices of each slice in a "not that smart" way as I am using a bunch of useless variables, so feel free to change and optimize this part of code.

Anyway, once we have the vectors populated with all vertices, lines 150 and 151 call createSlice function I will explain in a moment and finally we can remove the body we just sliced with DestroyBody method at line 152.

Now let's talk about createSlice function, which takes two arguments: the vector of vertices forming the slice and the length of such vectors. And, at the time of writing, I recognize it's a bit having two arguments when the second argument can be obtained starting from the first one.

Anyway... at line 180 I am finding the centroid of the slice. findCentroid function is the copy/paste of the protected ComputeCentroid function you can find in b2PolygonShape.as file.

Lines 181-183: once I have the centroid, I have to transform vertices coordinates from absolute in the Box2D world into relative to the centroid.

Lines 184-193: create the body from a vector of points thanks to SetAsVector method at line 188.

Finally, since the new slices share two points (the entry and the exit laser points), and such points are passed by reference, I have to restore the coordinates from relative to absolute at lines 194-196.

And that's it!!!

Finally you can cut the objects.

But it's not over yet, now we have to get rid of the debug draw and skin objects. I will explain how to do it soon, meanwhile download the source code of all examples.

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

215 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
// Stairs
// 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