すくまりのメモ帳すくまりのメモ帳
← 記事一覧に戻る

【Egret Engine】p2.js 物理エンジンを使用した衝突判定

【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

Circle Class「collisionGroup」

World「on」

World Class「beginContact 」

p2.js「Events」

Aurelien Ribon's Dev Blog

Box2D Totorial: Collision filtering

てっくぼっと!

ブラウザゲーム開発に使えるJavaScript製物理エンジンp2.jsを使ってみる

Egret社区

p2衝突検出はどのように行われますか?

HTML5 Egret Engine入門講座へ戻る