Slicing, splitting and cutting objects with Box2D

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.

Download the source code.

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