Install Circle Chain on your iPhone for free and get the source code!! 645 downloads to go - last updated: April 21, 2012

Slicing, splitting and cutting objects with Box2D – part 3: cutting your own Sprites

In the second part of this series I showed you how to cut Box2D objects.

Unfortunately I was working in the debug draw environment, so the whole process can’t be applied in a real-world example, unless you want to publish a game with the debug draw graphics.

So it’s time to see how to cut your own sprites. This is what you’ll get at the end of this step:

Cut the debug draw polygons to see randomly colored debris fall down. These debris are Sprites generated in real time.

So let’s take a look at the source code, and see what changed since the previous step:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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 debris:Sprite;
		private var canvas:Sprite;
		private var laserSegment:b2Segment;
		private var drawing:Boolean=false;
		private var affectedByLaser:Vector.<b2Body>;
		private var entryPoint:Vector.<b2Vec2>;
		public function Main() {
			debugDraw();
			addStuff();
			debris = new Sprite();
			addChild(debris);
			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,29);
			floorBody.userData=new Object();
			var floorShape:b2PolygonShape = new b2PolygonShape();
			floorShape.SetAsBox(40,15);
			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.<b2Vec2>=new Vector.<b2Vec2>();
			var circleSteps:int=12;
			var circleRadius:Number=3;
			for (var i:int=0; i<circleSteps; i++) {
				circleVector.push(new b2Vec2(circleRadius*Math.cos(2*Math.PI/circleSteps*i),circleRadius*Math.sin(2*Math.PI/circleSteps*i)));
			}
			var circleBody:b2BodyDef= new b2BodyDef();
			circleBody.position.Set(5,5);
			var circleShape:b2PolygonShape = new b2PolygonShape();
			circleShape.SetAsVector(circleVector,circleSteps);
			var circleFixture:b2FixtureDef = new b2FixtureDef();
			circleFixture.shape=circleShape;
			var worldCircle:b2Body=world.CreateBody(circleBody);
			worldCircle.CreateFixture(circleFixture);
		}
		private function updateWorld(e:Event):void {
			world.Step(1/30,10,10);
			world.ClearForces();
			if (laserSegment&&! drawing) {
				affectedByLaser=new Vector.<b2Body>();
				entryPoint=new Vector.<b2Vec2>();
				world.RayCast(laserFired,laserSegment.p1,laserSegment.p2);
				world.RayCast(laserFired,laserSegment.p2,laserSegment.p1);
				laserSegment=null;
			}
			for (var currentBody:b2Body=world.GetBodyList(); currentBody; currentBody=currentBody.GetNext()) {
				if (currentBody.GetUserData()!=null) {
					currentBody.GetUserData().x=currentBody.GetPosition().x*worldScale;
					currentBody.GetUserData().y=currentBody.GetPosition().y*worldScale;
					currentBody.GetUserData().rotation=currentBody.GetAngle()*180/Math.PI;
				}
			}
			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.<b2Vec2>=affectedPolygon.GetVertices();
				var newPolyVertices1:Vector.<b2Vec2>=new Vector.<b2Vec2>();
				var newPolyVertices2:Vector.<b2Vec2>=new Vector.<b2Vec2>();
				var currentPoly:int=0;
				var cutPlaced1:Boolean=false;
				var cutPlaced2:Boolean=false;
				for (var i:int=0; i<polyVertices.length; i++) {
					var worldPoint:b2Vec2=affectedBody.GetWorldPoint(polyVertices[i]);
					var cutAngle:Number=Math.atan2(worldPoint.y-rayCenter.y,worldPoint.x-rayCenter.x)-rayAngle;
					if (cutAngle<Math.PI*-1) {
						cutAngle+=2*Math.PI;
					}
					if (cutAngle>0&&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);
				if (affectedBody.GetUserData()!=null) {
					debris.removeChild(affectedBody.GetUserData());
				}
				world.DestroyBody(affectedBody);
			}
			return 1;
		}
		private function findCentroid(vs:Vector.<b2Vec2>, 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<count?vs[int(i+1)]:vs[0];
				var e1X:Number=p2.x-p1X;
				var e1Y:Number=p2.y-p1Y;
				var e2X:Number=p3.x-p1X;
				var e2Y:Number=p3.y-p1Y;
				var D:Number = (e1X * e2Y - e1Y * e2X);
				var triangleArea:Number=0.5*D;
				area+=triangleArea;
				c.x += triangleArea * inv3 * (p1X + p2.x + p3.x);
				c.y += triangleArea * inv3 * (p1Y + p2.y + p3.y);
			}
			c.x*=1.0/area;
			c.y*=1.0/area;
			return c;
		}
		private function createSlice(vertices:Vector.<b2Vec2>,numVertices:int):void {
			var centre:b2Vec2=findCentroid(vertices,vertices.length);
			var sliceBody:b2BodyDef=new b2BodyDef  ;
			sliceBody.position.Set(centre.x,centre.y);
			sliceBody.type=b2Body.b2_dynamicBody;
			sliceBody.userData=new Sprite  ;
			debris.addChild(sliceBody.userData);
			sliceBody.userData.graphics.lineStyle(2,Math.random()*0xFFFFFF);
			sliceBody.userData.graphics.beginFill(Math.random()*0xFFFFFF);
			for (var i:int=0; i<numVertices; i++) {
				vertices[i].Subtract(centre);
				if (i==0) {
					sliceBody.userData.graphics.moveTo(vertices[i].x*worldScale,vertices[i].y*worldScale);
				} else {
					sliceBody.userData.graphics.lineTo(vertices[i].x*worldScale,vertices[i].y*worldScale);
				}
			}
			sliceBody.userData.graphics.lineTo(vertices[0].x*worldScale,vertices[0].y*worldScale);
			sliceBody.userData.graphics.endFill();
			var slicePoly:b2PolygonShape=new b2PolygonShape  ;
			slicePoly.SetAsVector(vertices,numVertices);
			var sliceFixture:b2FixtureDef=new b2FixtureDef  ;
			sliceFixture.shape=slicePoly;
			sliceFixture.density=1;
			var worldSlice:b2Body=world.CreateBody(sliceBody);
			worldSlice.CreateFixture(sliceFixture);
			for (i=0; i<numVertices; i++) {
				vertices[i].Add(centre);
			}
		}
	}
}

Line 12: debris is the Sprite which will act as container for all runtime generated debris.

Lines 21-22: add debris to Display List, before canvas Sprite, which will contain the laser ray, so the laser will always stay on top of debris.

Now that we have a container for all debris, let’s modify createSlice function to add some children to debris:

Line 197: creates a custom Sprite for the sliced body, and saves it into body’s userData

Line 198: adds the Sprite as a child of debris Sprite

Line 199-200: set a random line and fill colors

Lines 203-207: at this time we are inside the for loop which scans for all vertices. In order to draw the polygon, we have to move the graphic pen to the first vertex (when i is equal to 0) and then draw a line connecting it to the second vertex, then to the third vertex and so on.

Also note we are drawing **after** centre has been subtracted from vertices, in order to have coordinates relative to polygon’s centroid.

Lines 209-210: once we connected the last vertex with the previous one, it’s time to connect the last vertex with the first one, to close the polygon, and call endFill method to stop drawing.

This way we’ll have our polygons drawn with their origins at (0,0) and, above all, static. They won’t move. Now it’s time to make polygons move and rotate according to the slice they represent. To do it, we need to modify updateWorld function.

Lines 102-108: this is the good old snippet of code which comes in Box2D’s Hello World example, allowing us to sync Flash Display Objects with Box2D objects.

Now we know how to draw and move custom Sprites, we just need to remove them once the object has been sliced. Lines 162-164 do this work.

And this task has been completed. During next step, the last one, we’ll see how to use these custom sprites as masks allowing us to cut textured objects.

Download the source code.

Rate this post: 1 Star2 Stars3 Stars4 Stars5 Stars (13 votes, average: 5.00 out of 5)
Loading ... Loading ...
Flash Templates provided by Template Monster are pre-made web design products developed using Flash technology.
They can be easily customized to meet the unique requirements of your project.
Be my fan on Facebook and follow me on Twitter! Exclusive content for my Facebook fans and Twitter followers

This post has 17 comments

  1. C.Senthil Kumaran

    on July 4, 2011 at 11:21 am

    WOW … FANTASTIC…

  2. Rackdoll

    on July 4, 2011 at 5:21 pm

    Real nice job there.

  3. Billy Moore

    on July 5, 2011 at 1:15 am

    Impressive!
    Though, I found a bug. When I cut the shapes up and try to cut the triangle to the left (green with line through it in the image) a triangle in the top left appears and starts flashing.
    http://img269.imageshack.us/img269/7384/shapesv.png

  4. Mauro Junior

    on July 5, 2011 at 4:11 am

    Muito bom…PERFEITO!!! PERFECT !!!!!!!!!

    Leio seu site todo dia… olhando os tutoriais… são muito bons e detalhadsos… está de parabens… ;)

  5. MC

    on July 5, 2011 at 8:37 am

    The script creates a new (sliced) sprite, but if you were using a custom texture (bitmap or movieclip) the sliced part would not match the other part i think

  6. Emanuele Feronato

    on July 5, 2011 at 5:35 pm

    @billy: could not replicate the error but I had another issue when I created an empty object at line 60, try to remove it then reproducing the error.

    @MC: you will see how to do it in the 4th and last part of this series

  7. MC

    on July 5, 2011 at 5:59 pm

    Thanks! Great Tutorial

  8. Ensis

    on July 8, 2011 at 7:49 pm

    Very useful tutoriel, it helped me a lot !
    But like Billy, I found a bug : I bet it is caused by a slice on a polygon vertex… So when you slice on thow vertices of the left polygon, that’s what I got : http://img23.imageshack.us/img23/2926/slicebug.jpg
    Thanks again =)

  9. tibbi

    on July 14, 2011 at 2:35 pm

    You use Math.PI * 2 pretty often, so maybe for better performance you should make a constant out of it, like advised at http://www.nbilyk.com/optimizing-actionscript-3

  10. tibbi

    on July 14, 2011 at 6:37 pm

    or nevedmind, its not used that many times like in tests, so it wont change anything :D

  11. mert ozcan

    on July 25, 2011 at 9:57 am

    Hi, thanks for this great tutorial..Can we use the Box2d engine in Flash-IOS apps ?

  12. Erkalanger

    on August 1, 2011 at 10:35 am

    Hi Emanuele!

    Thanks for a very nice series!
    When will part 4 be out?
    *eagerly waiting*

  13. Erkalanger

    on August 1, 2011 at 12:46 pm

    Will this system ever support concave polygons?
    (created with multiple convex shapes stored in one body)

  14. Anton

    on August 2, 2011 at 8:37 am

    Can I use beginBitmapFill () to fill?

  15. Antoan

    on August 3, 2011 at 1:07 pm

    That was a nice tutorial there. I followed some of your steps to make a Box2D cutter myself, and I made it optimized to the max. I didn’t have to use a vector storing the affected bodies like you did. I used determinants to arrange the vertices in clockwise order, which is very fast. I also extract the BitmapData out of an image and then use the graphics.beginBitmapFill() method – that way I can apply textures to sliced objects. Anyways, keep on with the good work :)

  16. Emanuele Feronato

    on August 3, 2011 at 5:47 pm

    @antoan: great! any chance to see your code?

  17. sengu

    on March 21, 2012 at 11:46 pm

    gud but it shows some errs