【Egret Engine】p2.js 物理エンジンを使用した衝突判定
はじめに
p2.jsを物理エンジンを使用した衝突判定を行います。
準備
【EgretEngine】p2.js 物理エンジンを使用して1000個の玉を描画する方法を読んで、玉を2つ生成するコードを用意してください。
上記の記事のコードとは、若干異なる部分もあります。
Main.ts
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
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 クラスの追加
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の追加
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等)のプロパティに、
collisionGroup: GraphicShape.PLANE, collisionMask:GraphicShape.CIECLEのように追加してください。
これで、collisionGroupはPLANE、collisionMaskはCIECLEになります。
collisionMaskには衝突判定を行いたいオブジェクトの値を入力します。複数ある場合は、
collisionMask:GraphicShape.CIECLE | collisionMask:GraphicShape.PLANEのように記入してください。
ちなみに、p2.Bodyの方にはcollisionGroupとcollisionMaskプロパティはないので、Shape系で使用してください。
玉の方も同様にcollisionGroupとcollisionMaskを追加してください。
World.on( "beginContact", beginMethod, this);の追加
//繰り返しメソッド
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を作成してください。こちらのメソッド名は何でも構いません。
//衝突した瞬間に実行されるメソッド
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の条件式で場合分けをします。
同じオブジェクト同士であれば、
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を使ってみる