Slicing, splitting and cutting objects with Box2D – part 4: using real graphics

The fourth part of this tutorial comes from Antoan Angelov and not only allows us to slice any kind of bitmap image mapped on our Box2D objects, but also improves the code making it faster and more robust.

This is the final result:

Use the mouse to slice objects.

And this is the fully commented and explained code:

package {
	import flash.display.*;
	import Box2D.Dynamics.*;
	import Box2D.Dynamics.Joints.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.geom.Matrix;

	public class main extends MovieClip {
/*
			Box2D body slicer, created by Antoan Angelov.
		*/

		var world:b2World;
		var tempBox:b2Body;
		var stageW:Number=stage.stageWidth,stageH:Number=stage.stageHeight;
		var cont:Sprite = new Sprite();
		var begX:Number,begY:Number,endX:Number,endY:Number;
		var polyShape:b2PolygonShape;
		var enterPointsVec:Vector. = new Vector.();
		var mouseReleased:Boolean=false;
		var objectsCont:Sprite = new Sprite();
		var laserCont:Sprite = new Sprite();
		var numEnterPoints:int=0,i:int;
		var boxDef:b2PolygonShape;
		var fixtureDef:b2FixtureDef,bodyDef:b2BodyDef,body:b2Body;
		var woodTexture:BitmapData,rockTexture:BitmapData;

		public function main() {
			// First, I create the textures, which are BitmapData objects.
			var tempSpr:Sprite;

			tempSpr = new texture1();
			woodTexture=new BitmapData(tempSpr.width,tempSpr.height);
			woodTexture.draw(tempSpr);

			tempSpr = new texture2();
			rockTexture=new BitmapData(tempSpr.width,tempSpr.height);
			rockTexture.draw(tempSpr);

			// World setup
			world=new b2World(new b2Vec2(0,10),true);

			// Making the ground, which is a static body, and then creating its Sprite equivalent.
			bodyDef = new b2BodyDef();
			bodyDef.type=b2Body.b2_staticBody;
			fixtureDef = new b2FixtureDef();
			fixtureDef.density=5;
			fixtureDef.friction=1;
			fixtureDef.restitution=0;
			boxDef = new b2PolygonShape();
			boxDef.SetAsBox((stageW)/30, 10/30);
			bodyDef.position.Set((0.5*stageW)/30, stageH/30);
			fixtureDef.shape=boxDef;
			tempBox=world.CreateBody(bodyDef);
			tempBox.CreateFixture(fixtureDef);

			tempSpr = new Sprite();
			tempSpr.graphics.lineStyle(2, 0x00FF00);
			tempSpr.graphics.beginFill(0x00FF00, 0.3);
			tempSpr.graphics.drawRect(-0.5*stageW, (stageH-10), 2*stageW, 20);
			tempSpr.graphics.endFill();
			addChild(tempSpr);

			// Initializing bodies that can be sliced.
			fixtureDef.density=5;
			fixtureDef.friction=0.2;
			fixtureDef.restitution=0;

			createBody((230)/30, 50/30, [new b2Vec2(-100/30, -75/30), new b2Vec2(100/30, -75/30), new b2Vec2(100/30, 75/30), new b2Vec2(-100/30, 75/30)], woodTexture);
			createBody((stageW-230)/30, 50/30, [new b2Vec2(3.0795984417042894, 1.275611441216966), new b2Vec2(1.2756114412169661, 3.0795984417042894), new b2Vec2(-1.275611441216966, 3.0795984417042894), new b2Vec2(-3.0795984417042894, 1.2756114412169663), new b2Vec2(-3.0795984417042894, -1.2756114412169657), new b2Vec2(-1.2756114412169677, -3.0795984417042885), new b2Vec2(1.2756114412169666, -3.079598441704289), new b2Vec2(3.0795984417042885, -1.275611441216968)], rockTexture);

			// You can see the reason for creating the enterPointsVec in the coments in the intersection() method.
			enterPointsVec=new Vector.(numEnterPoints);

			this.addChild(cont);
			cont.addChild(objectsCont);
			cont.addChild(laserCont);

			stage.addEventListener(MouseEvent.MOUSE_DOWN, mDown);
			addEventListener(Event.ENTER_FRAME, update);
		}

		private function createBody(xPos:Number, yPos:Number, verticesArr:Array, texture:BitmapData) {
			var vec:Vector.=Vector.(verticesArr);
			bodyDef = new b2BodyDef();
			bodyDef.type=b2Body.b2_dynamicBody;
			boxDef = new b2PolygonShape();
			boxDef.SetAsVector(vec);
			bodyDef.position.Set(xPos, yPos);
			// I use my own userData class to store the unique ID of each body, its vertices and its texture.
			bodyDef.userData=new userData(numEnterPoints,vec,texture);
			objectsCont.addChild(bodyDef.userData);
			fixtureDef.shape=boxDef;
			tempBox=world.CreateBody(bodyDef);
			tempBox.CreateFixture(fixtureDef);
			tempBox.SetBullet(true);
			numEnterPoints++;
		}

		private function mDown(e:MouseEvent) {
			begX=mouseX;
			begY=mouseY;

			stage.addEventListener(MouseEvent.MOUSE_UP, mUp);
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mMove);
		}

		private function mMove(e:MouseEvent) {
			laserCont.graphics.clear();
			laserCont.graphics.lineStyle(2);
			laserCont.graphics.moveTo(begX, begY);
			laserCont.graphics.lineTo(mouseX, mouseY);
		}

		private function mUp(e:MouseEvent) {
			mouseReleased=true;

			stage.removeEventListener(MouseEvent.MOUSE_UP, mUp);
			stage.removeEventListener(MouseEvent.MOUSE_MOVE, mMove);
		}

		public function update(e:Event):void {
			if (mouseReleased) {
				// Here I use the world.RayCast() method (I use it twice, see why in the comments in the intersection() method below) to get the intersection points between the line you just drew and all bodies in the Box2D world.

				endX=mouseX;
				endY=mouseY;

				var p1:b2Vec2=new b2Vec2(begX/30,begY/30);
				var p2:b2Vec2=new b2Vec2(endX/30,endY/30);

				world.RayCast(intersection, p1, p2);
				world.RayCast(intersection, p2, p1);
				enterPointsVec=new Vector.(numEnterPoints);
				mouseReleased=false;
			}

			world.Step(1/24, 90, 90);
			world.ClearForces();

			var p:b2Body,spr:Sprite;

			// Here all the bodies' Sprite equivalents are synchronized to the bodies themselves.

			for (p = world.GetBodyList(); p; p = p.GetNext()) {
				spr=p.GetUserData();
				if (spr) {
					spr.x=p.GetPosition().x*30;
					spr.y=p.GetPosition().y*30;
					spr.rotation=p.GetAngle()*180/Math.PI;
				}
			}
		}

		private function intersection(fixture:b2Fixture, point:b2Vec2, normal:b2Vec2, fraction:Number):Number {
			var spr:Sprite=fixture.GetBody().GetUserData();

			// Throughout this whole code I use only one global vector, and that is enterPointsVec. Why do I need it you ask? 
			// Well, the problem is that the world.RayCast() method calls this function only when it sees that a given line gets into the body - it doesnt see when the line gets out of it.
			// I must have 2 intersection points with a body so that it can be sliced, thats why I use world.RayCast() again, but this time from B to A - that way the point, at which BA enters the body is the point at which AB leaves it!
			// For that reason, I use a vector enterPointsVec, where I store the points, at which AB enters the body. And later on, if I see that BA enters a body, which has been entered already by AB, I fire the splitObj() function!
			// I need a unique ID for each body, in order to know where its corresponding enter point is - I store that id in the userData of each body.

			if (spr is userData) {
				var userD:userData=spr as userData;

				if (enterPointsVec[userD.id]) {
					// If this body has already had an intersection point, then it now has two intersection points, thus it must be split in two - thats where the splitObj() method comes in.
					// Before calling splitObj() however, I first draw the two intersection points - the blue one is the enter point and the red one is the end point.
					laserCont.graphics.lineStyle(4, 0x0000FF);
					laserCont.graphics.drawCircle(enterPointsVec[userD.id].x*30, enterPointsVec[userD.id].y*30, 7);

					laserCont.graphics.lineStyle(4, 0xFF0000);
					laserCont.graphics.drawCircle(point.x*30, point.y*30, 7);

					splitObj(fixture.GetBody(), enterPointsVec[userD.id], point.Copy());
				} else {
					enterPointsVec[userD.id]=point;
				}

			}
			return 1;
		}

		private function splitObj(sliceBody:b2Body, A:b2Vec2, B:b2Vec2):void {
			var origFixture:b2Fixture=sliceBody.GetFixtureList();
			var poly:b2PolygonShape=origFixture.GetShape() as b2PolygonShape;
			var verticesVec:Vector.=poly.GetVertices(),numVertices:int=poly.GetVertexCount();
			var shape1Vertices:Vector. = new Vector.(), shape2Vertices:Vector. = new Vector.();
			var origUserData:userData=sliceBody.GetUserData(),origUserDataId:int=origUserData.id,d:Number;

			// First, I destroy the original body and remove its Sprite representation from the childlist.
			world.DestroyBody(sliceBody);
			objectsCont.removeChild(origUserData);

			// The world.RayCast() method returns points in world coordinates, so I use the b2Body.GetLocalPoint() to convert them to local coordinates.
			A=sliceBody.GetLocalPoint(A);
			B=sliceBody.GetLocalPoint(B);

			// I use shape1Vertices and shape2Vertices to store the vertices of the two new shapes that are about to be created. 
			// Since both point A and B are vertices of the two new shapes, I add them to both vectors.
			shape1Vertices.push(A, B);
			shape2Vertices.push(A, B);

			// I iterate over all vertices of the original body. 
			// I use the function det() ("det" stands for "determinant") to see on which side of AB each point is standing on. The parameters it needs are the coordinates of 3 points:
			// - if it returns a value >0, then the three points are in clockwise order (the point is under AB)
			// - if it returns a value =0, then the three points lie on the same line (the point is on AB)
			// - if it returns a value <0, then the three points are in counter-clockwise order (the point is above AB). 
			for (i=0; i0) {
					shape1Vertices.push(verticesVec[i]);
				} else {
					shape2Vertices.push(verticesVec[i]);
				}
			}

			// In order to be able to create the two new shapes, I need to have the vertices arranged in clockwise order.
			// I call my custom method, arrangeClockwise(), which takes as a parameter a vector, representing the coordinates of the shape's vertices and returns a new vector, with the same points arranged clockwise.
			shape1Vertices=arrangeClockwise(shape1Vertices);
			shape2Vertices=arrangeClockwise(shape2Vertices);

			// setting the properties of the two newly created shapes
			bodyDef = new b2BodyDef();
			bodyDef.type=b2Body.b2_dynamicBody;
			bodyDef.position.SetV(sliceBody.GetPosition());
			fixtureDef = new b2FixtureDef();
			fixtureDef.density=origFixture.GetDensity();
			fixtureDef.friction=origFixture.GetFriction();
			fixtureDef.restitution=origFixture.GetRestitution();

			// creating the first shape
			polyShape = new b2PolygonShape();
			polyShape.SetAsVector(shape1Vertices);
			fixtureDef.shape=polyShape;

			bodyDef.userData=new userData(origUserDataId,shape1Vertices,origUserData.texture);
			objectsCont.addChild(bodyDef.userData);
			enterPointsVec[origUserDataId]=null;

			body=world.CreateBody(bodyDef);
			body.SetAngle(sliceBody.GetAngle());
			body.CreateFixture(fixtureDef);
			body.SetBullet(true);

			// creating the second shape
			polyShape = new b2PolygonShape();
			polyShape.SetAsVector(shape2Vertices);
			fixtureDef.shape=polyShape;

			bodyDef.userData=new userData(numEnterPoints,shape2Vertices,origUserData.texture);
			objectsCont.addChild(bodyDef.userData);
			enterPointsVec.push(null);
			numEnterPoints++;

			body=world.CreateBody(bodyDef);
			body.SetAngle(sliceBody.GetAngle());
			body.CreateFixture(fixtureDef);
			body.SetBullet(true);
		}

		private function arrangeClockwise(vec:Vector.):Vector. {
			// The algorithm is simple: 
			// First, it arranges all given points in ascending order, according to their x-coordinate.
			// Secondly, it takes the leftmost and rightmost points (lets call them C and D), and creates tempVec, where the points arranged in clockwise order will be stored.
			// Then, it iterates over the vertices vector, and uses the det() method I talked about earlier. It starts putting the points above CD from the beginning of the vector, and the points below CD from the end of the vector. 
			// That was it!

			var n:int=vec.length,d:Number,i1:int=1,i2:int=n-1;
			var tempVec:Vector.=new Vector.(n),C:b2Vec2,D:b2Vec2;

			vec.sort(comp1);

			tempVec[0]=vec[0];
			C=vec[0];
			D=vec[n-1];

			for (i=1; ib.x) {
				return 1;
			} else if (a.x

userData class is as follows:

package 
{
	import Box2D.Common.Math.b2Vec2;
	import flash.display.Sprite;
	import flash.display.BitmapData;
	import flash.geom.Matrix;

	public class userData extends Sprite
	{
		var id:int, texture:BitmapData;

		public function userData(id:int, verticesVec:Vector., texture:BitmapData)
		{
			this.id = id;
			this.texture = texture;

			// I use the matrix so that I can have the center of the shape I'm drawing match the center of the BitmapData image - I "move" the BitmapData projection left by half its width and up by half its height.
			var m:Matrix = new Matrix();
			m.tx = -texture.width*0.5;
			m.ty = -texture.height*0.5;
			
			// I then draw lines from each vertex to the next, in clockwise order and use the beginBitmapFill() method to add the texture.
			this.graphics.lineStyle(2);
			this.graphics.beginBitmapFill(texture, m, true, true);
			this.graphics.moveTo(verticesVec[0].x*30, verticesVec[0].y*30);
			for (var i:int=1; i

The result is really amazing so I think we'll see some new game concepts using slicing.

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

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