はじめに
p2.jsを物理エンジンを使用した衝突判定を行います。
準備
【EgretEngine】p2.js 物理エンジンを使用して1000個の玉を描画する方法を読んで、玉を2つ生成するコードを用意してください。
上記の記事のコードとは、若干異なる部分もあります。
Main.ts
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 |
class Main extends eui.UILayer { public constructor() { super(); this.once(egret.Event.ADDED_TO_STAGE, this.addToStage, this); } private addToStage(event:egret.Event) { const world = new p2.World(); world.sleepMode = p2.World.BODY_SLEEPING; world.gravity = [0, 9.8 * 50]; //見えない壁や地面の生成 for(let i = 0; i < 3; i++){ const planeBody: p2.Body[] = []; planeBody[i] = new p2.Body({ fixedRotation:true ,type:p2.Body.STATIC }); const planeShape: p2.Plane[] = []; planeShape[i] = new p2.Plane({ }); switch(i){ //地面 case 0: planeBody[i].position= [0, 700]; planeBody[i].angle = Math.PI;//rad表記 break; //右の壁 case 1: planeBody[i].position= [1260, 700]; planeBody[i].angle = Math.PI/2;//rad表記 break; //左の壁 case 2: planeBody[i].position= [20, 700]; planeBody[i].angle = 3* Math.PI/2;//rad表記 break; } planeBody[i].addShape(planeShape[i]); world.addBody(planeBody[i]); } const shape:egret.Shape[] = []; const body :p2.Body[] = []; const bodyShape : p2.Circle[] = []; const ballNumber:number = 2; for (let i = 0; i < ballNumber; i++){ shape[i] = new egret.Shape(); this.addChild(shape[i]); body[i] = new p2.Body({mass:1, position:[30 , 50-i*50], fixedRotation:true}); bodyShape[i] = new p2.Circle({ radius: 5, }); body[i].addShape(bodyShape[i]); world.addBody(body[i]); } //繰り返しメソッド egret.Ticker.getInstance().register(loopMethod,this); function loopMethod(dt:number) { //1/60秒に一度実行 world.step(1/60, dt/1000, 10); for (let i = 0; i < ballNumber; i++){ drawBall(shape[i], body[i].position[0], body[i].position[1]); } } function drawBall(shape:egret.Shape, x:number, y:number){ //一旦ステージに描画した図形を消す。これがないと図形が重なって線になる。 shape.graphics.clear(); //shapeの座標をbodyと一致させる shape.x = x; shape.y = y; //円の描画 shape.graphics.beginFill(0xff0000); shape.graphics.drawCircle(0, 0, 5); shape.graphics.endFill(); } } } |
p2.js 物理エンジンを使用した衝突判定
Main.ts
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 |
class Main extends eui.UILayer { public constructor() { super(); this.once(egret.Event.ADDED_TO_STAGE, this.addToStage, this); } private addToStage(event:egret.Event) { const world = new p2.World(); world.sleepMode = p2.World.BODY_SLEEPING; world.gravity = [0, 9.8 * 50]; //見えない壁や地面の生成 for(let i = 0; i < 3; i++){ const planeBody: p2.Body[] = []; planeBody[i] = new p2.Body({ fixedRotation:true ,type:p2.Body.STATIC }); const planeShape: p2.Plane[] = []; planeShape[i] = new p2.Plane({ collisionGroup: GraphicShape.PLANE, collisionMask:GraphicShape.CIECLE }); switch(i){ //地面 case 0: planeBody[i].position= [0, 700]; planeBody[i].angle = Math.PI;//rad表記 break; //右の壁 case 1: planeBody[i].position= [1260, 700]; planeBody[i].angle = Math.PI/2;//rad表記 break; //左の壁 case 2: planeBody[i].position= [20, 700]; planeBody[i].angle = 3* Math.PI/2;//rad表記 break; } planeBody[i].addShape(planeShape[i]); world.addBody(planeBody[i]); } const shape:egret.Shape[] = []; const body :p2.Body[] = []; const bodyShape : p2.Circle[] = []; const ballNumber:number = 2; for (let i = 0; i < ballNumber; i++){ shape[i] = new egret.Shape(); this.addChild(shape[i]); body[i] = new p2.Body({mass:1, position:[30 , 50-i*50], fixedRotation:true}); bodyShape[i] = new p2.Circle({ radius: 5, collisionGroup: GraphicShape.CIECLE, collisionMask:GraphicShape.CIECLE | GraphicShape.PLANE }); body[i].addShape(bodyShape[i]); world.addBody(body[i]); } //繰り返しメソッド egret.Ticker.getInstance().register(loopMethod,this); function loopMethod(dt:number) { //1/60秒に一度実行 world.step(1/60, dt/1000, 10); for (let i = 0; i < ballNumber; i++){ drawBall(shape[i], body[i].position[0], body[i].position[1]); } world.on("beginContact", beginMethod, this); } //衝突した瞬間に実行されるメソッド function beginMethod(evt) : void { const bodyA: p2.Body = evt.bodyA; const bodyB: p2.Body = evt.bodyB; const shapeA = evt.shapeA; const shapeB = evt.shapeB; if(shapeA.collisionGroup == GraphicShape.CIECLE && shapeB.collisionGroup == GraphicShape.CIECLE){ console.log("CircleとCircleが衝突"); } else if((shapeA.collisionGroup == GraphicShape.PLANE && shapeB.collisionGroup == GraphicShape.CIECLE) || (shapeB.collisionGroup == GraphicShape.PLANE && shapeA.collisionGroup == GraphicShape.CIECLE) ){ console.log("PlaneとCircleが衝突"); } } function drawBall(shape:egret.Shape, x:number, y:number){ //一旦ステージに描画した図形を消す。これがないと図形が重なって線になる。 shape.graphics.clear(); //shapeの座標をbodyと一致させる shape.x = x; shape.y = y; //円の描画 shape.graphics.beginFill(0xff0000); shape.graphics.drawCircle(0, 0, 5); shape.graphics.endFill(); } } } enum GraphicShape{ CIECLE = Math.pow(2,0), RECT= Math.pow(2,1), PLANE= Math.pow(2,2), } |
collisionGroupとcollisionMaskの追加
1、enum クラスの追加
1 2 3 4 5 |
enum GraphicShape{ CIECLE = Math.pow(2,0), RECT= Math.pow(2,1), PLANE= Math.pow(2,2), } |
Class Main の外側にenumクラスを作成してください。名前は何でも構いません。
p2.js物理エンジンでは、collisionGroupでオブジェクトの種類を設定し、collisionMaskで衝突判定を行うオブジェクトを設定します。
これらはnumberで管理されていますが、使用できる値は2の累乗のみで、2の31乗までです。なので、powを使用して定義しております。
2、collisionGroupとcollisionMaskの追加
1 2 3 4 5 6 7 8 |
const planeBody: p2.Body[] = []; planeBody[i] = new p2.Body({ fixedRotation:true ,type:p2.Body.STATIC }); const planeShape: p2.Plane[] = []; planeShape[i] = new p2.Plane({ collisionGroup: GraphicShape.PLANE, collisionMask:GraphicShape.CIECLE }); |
p2のShape系(p2.Circleやp2.Plane等)のプロパティに、
1 |
collisionGroup: GraphicShape.PLANE, collisionMask:GraphicShape.CIECLE |
のように追加してください。
これで、collisionGroupはPLANE、collisionMaskはCIECLEになります。
collisionMaskには衝突判定を行いたいオブジェクトの値を入力します。複数ある場合は、
1 |
collisionMask:GraphicShape.CIECLE | collisionMask:GraphicShape.PLANE |
のように記入してください。
ちなみに、p2.Bodyの方にはcollisionGroupとcollisionMaskプロパティはないので、Shape系で使用してください。
玉の方も同様にcollisionGroupとcollisionMaskを追加してください。
World.on( “beginContact”, beginMethod, this);の追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//繰り返しメソッド egret.Ticker.getInstance().register(loopMethod,this); function loopMethod(dt:number) { //1/60秒に一度実行 world.step(1/60, dt/1000, 10); for (let i = 0; i < ballNumber; i++){ drawBall(shape[i], body[i].position[0], body[i].position[1]); } world.on("beginContact", beginMethod, this); } |
world.onはaddListenerと同じような役割を果たし、メソッドをworldに追加します。これを、繰り返しメソッドの中に入れてください。ちなみに、world.offで削除できます。
“beginContact”はp2.jsが用意した変数で、これは衝突した瞬間を指します。離れた瞬間は”endContact”です。
続いて、beginMethodを作成してください。こちらのメソッド名は何でも構いません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//衝突した瞬間に実行されるメソッド function beginMethod(evt) : void { const bodyA: p2.Body = evt.bodyA; const bodyB: p2.Body = evt.bodyB; const shapeA = evt.shapeA; const shapeB = evt.shapeB; if(shapeA.collisionGroup == GraphicShape.CIECLE && shapeB.collisionGroup == GraphicShape.CIECLE){ console.log("CircleとCircleが衝突"); } else if((shapeA.collisionGroup == GraphicShape.PLANE && shapeB.collisionGroup == GraphicShape.CIECLE) || (shapeB.collisionGroup == GraphicShape.PLANE && shapeA.collisionGroup == GraphicShape.CIECLE) ){ console.log("PlaneとCircleが衝突"); } } |
beginMethod(evt)のevt はevent でもaでもなんでも構いません。anyです。
オブジェクト同士が接触したときに、bodyAやshapeAに、接触したオブジェクトのパラメータが代入されます。
後は、ifの条件式で場合分けをします。
同じオブジェクト同士であれば、
1 |
if(shapeA.collisionGroup == GraphicShape.CIECLE && shapeB.collisionGroup == GraphicShape.CIECLE) |
のようにShapeA,Bの場合分けで済みます。
しかし、違うオブジェクトの場合は、AがPLANE、BがCIECLEの時と、その逆で場合分けする必要があります。
補足ですが、shapeはworldに生成された順番でナンバリングされており、shape.idでそのナンバーを取得できるので、それで場合分けすることも可能です。
参考URL
yUI API
Aurelien Ribon’s Dev Blog
Box2D Totorial: Collision filtering
てっくぼっと!
ブラウザゲーム開発に使えるJavaScript製物理エンジンp2.jsを使ってみる
Egret社区