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.
They can be easily customized to meet the unique requirements of your project.


























This post has 17 comments
C.Senthil Kumaran
WOW … FANTASTIC…
Rackdoll
Real nice job there.
Billy Moore
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
Mauro Junior
Muito bom…PERFEITO!!! PERFECT !!!!!!!!!
Leio seu site todo dia… olhando os tutoriais… são muito bons e detalhadsos… está de parabens… ;)
MC
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
Emanuele Feronato
@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
MC
Thanks! Great Tutorial
Ensis
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 =)
tibbi
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
tibbi
or nevedmind, its not used that many times like in tests, so it wont change anything :D
mert ozcan
Hi, thanks for this great tutorial..Can we use the Box2d engine in Flash-IOS apps ?
Erkalanger
Hi Emanuele!
Thanks for a very nice series!
When will part 4 be out?
*eagerly waiting*
Erkalanger
Will this system ever support concave polygons?
(created with multiple convex shapes stored in one body)
Anton
Can I use beginBitmapFill () to fill?
Antoan
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 :)
Emanuele Feronato
@antoan: great! any chance to see your code?
sengu
gud but it shows some errs