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:
|
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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
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.<b2Vec2> = new Vector.<b2Vec2>(); 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.<b2Vec2>(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.<b2Vec2>=Vector.<b2Vec2>(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.<b2Vec2>(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.<b2Vec2>=poly.GetVertices(),numVertices:int=poly.GetVertexCount(); var shape1Vertices:Vector.<b2Vec2> = new Vector.<b2Vec2>(), shape2Vertices:Vector.<b2Vec2> = new Vector.<b2Vec2>(); 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; i<numVertices; i++) { d=det(A.x,A.y,B.x,B.y,verticesVec[i].x,verticesVec[i].y); if (d>0) { 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.<b2Vec2>):Vector.<b2Vec2> { // 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.<b2Vec2>=new Vector.<b2Vec2>(n),C:b2Vec2,D:b2Vec2; vec.sort(comp1); tempVec[0]=vec[0]; C=vec[0]; D=vec[n-1]; for (i=1; i<n-1; i++) { d=det(C.x,C.y,D.x,D.y,vec[i].x,vec[i].y); if (d<0) { tempVec[i1++]=vec[i]; } else { tempVec[i2--]=vec[i]; } } tempVec[i1]=vec[n-1]; return tempVec; } private function comp1(a:b2Vec2, b:b2Vec2):Number { // This is a compare function, used in the arrangeClockwise() method - a fast way to arrange the points in ascending order, according to their x-coordinate. if (a.x>b.x) { return 1; } else if (a.x<b.x) { return -1; } return 0; } private function det(x1:Number, y1:Number, x2:Number, y2:Number, x3:Number, y3:Number):Number { // This is a function which finds the determinant of a 3x3 matrix. // If you studied matrices, you'd know that it returns a positive number if three given points are in clockwise order, negative if they are in anti-clockwise order and zero if they lie on the same line. // Another useful thing about determinants is that their absolute value is two times the face of the triangle, formed by the three given points. return x1*y2+x2*y3+x3*y1-y1*x2-y2*x3-y3*x1; } } } |
userData class is as follows:
|
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 |
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.<b2Vec2>, 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<verticesVec.length; i++) this.graphics.lineTo(verticesVec[i].x*30, verticesVec[i].y*30); this.graphics.lineTo(verticesVec[0].x*30, verticesVec[0].y*30); this.graphics.endFill(); } } } |
The result is really amazing so I think we’ll see some new game concepts using slicing.
They can be easily customized to meet the unique requirements of your project.





(30 votes, average: 4.73 out of 5)






This post has 29 comments
James
That is so cool. I’m sure it won’t take long for someone to develop a game that revolves around Sharks with lazer beams on their heads…
mmankt
nice post but hm.. u’re using the easy way. you should do this with uv mapping of polygons
Ben
This is great! Nice follow-up to the previous samples, love the code comments.
Hasan
simply AWESOME..
????
real graphics,wow,it’s really i want ~~
thx very much!!!
MC
Very impressive, good post!
Guy
Prehaps next time you could look at playerio and make a tutorial because everybody likes playin a mulitpalyer game
MC
I noticed this developer used beginBitmapFill insted of masking to show the texture… Is this better / optimal than masking in cpu terms?
Antoan Angelov
@MC
I think this is the best way to do it. If you make a sprite which holds the texture image and a mask, its probably slower because the CPU has to render both the sprite and the mask. You could use an alpha mask on a DisplayObject (MovieClip, Sprite, Bitmap, etc.) – you do it by using BlendMode.ALPHA (http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/DisplayObject.html#blendMode), but I think its slower too, because the CPU renders two display objects here as well. The beginBitmapFill() method does not require additional sprites, thus it makes is easier for the CPU to render.
mmankt
as i said the right way to do this is to have polygons and map them using uv data. when fp 11 comes along we’ll be only doing this.
Pablo
Nice example, but I wonder why this is performing so much slower FPS than the original one.
zoe
MM, split engine is fun.
And I made a Cut Cut Zombie game inspired by ninja fruit:)
http://www.0000park.com/1741/cutcutzombie/
JJ Smith
Hey your source files flash movie seems to have a glitch!
sogetsu
Really nice pos it on wonderfl.net XD
All you need to know about how to write Box2D game | Artits Rastas
[...] Slicing, splitting and cutting objects with Box2D – part 4: using real graphics [...]
Dev
Thank you for such a nice tutorial, But in my case I am using physics editor for editing physics parameters and getting vertices of a complex object. Physics editor gives me shape using a combination of different shapes. and eventually a single body will have different fixtures associated with it. This algorithm assumes a body will have single shape,fixture associated with a body.
so how can we slice an object which is composed of different shapes.
@mmankt, you are saying that the best way of doing this is using uv mapping of polygon. Do you have any reference/blog post regarding this or can you please provide me hints on this.
bht
I’ve tried to port it to iOS, and most of the time its quite ok, but there are a lot of b2Asserts unfortunatelly… (Assertion failed: (m_I > 0.0f))
sengu
superb but it shows some errs
Anrew
@bht: any chance to see the iOS port on github o similar? maybe I can help you to improve\fix.
How To Make A Game Like Fruit Ninja With Box2D and Cocos2D Part 1 | Ray Wenderlich
[...] the foundations for the project this tutorial is based on. He was responsible for porting this flash-based slicing demo into Cocos2D, and also for porting CCBlade and PRKit to Cocos2D [...]
How To Make A Game Like Fruit Ninja With Box2D and Cocos2D Part 1 | | Hacker ColonyHacker Colony
[...] the foundations for the project this tutorial is based on. He was responsible for porting this flash-based slicing demo into Cocos2D, and also for porting CCBlade and PRKit to Cocos2D [...]
mohit parihar
sir its possible to change this code into j2me for mobile, if it possible then give me j2me version of this source code.
???? Box2D ? Cocos2D ????Fruit Ninja??? Part 1 | ??
[...] ????Rick Smorawski ???????????????flash-based slicing demo ?Cocos2D???????CCBlade?PRKit?Cocos2D 2.0??????????????????????????????????? [...]
Tetsuken
I’m new on game programming and begginer on programming and i found your site and its been very usefull as knowledge source, i’m here to ask if is possible use that slicing functions on STENCYL?
????Box2D?Cocos2D?????Fruit Ninja?????-?1??
[...] Smorawski??????????????????????flash-based slicing demo????Cocos2D????CCBlade?PRKit????Cocos2D [...]
Cómo hacer un juego como Fruit Ninja utilizando Box2D y Cocos2D – Parte 1
[...] bases del proyecto en el que está basado este tutorial. Fue el responsable de portar este demo de cortes basado en flash a Cocos2D, y también por portar CCBlade y PRKit a Cocos2D [...]
????Box2D?Cocos2D?????Fruit Ninja?????-?1??
[...] Smorawski??????????????????????flash-based slicing demo????Cocos2D????CCBlade?PRKit????Cocos2D [...]
????Box2D?Cocos2D?????Fruit Ninja?????-?1??
[...] Smorawski??????????????????????flash-based slicing demo????Cocos2D????CCBlade?PRKit????Cocos2D [...]
yann
Hello
how you made ??to raise the limit on the number of vertices in a polygon.
because in java the limit is 8 vertices per polygon.
In other words your code if a cut was creating a polygon with 9 sides my application crach.
Bye