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

【EgretEngine】p2.js 物理エンジンを使用して1000個の玉を描画する方法

【EgretEngine】p2.js 物理エンジンを使用して1000個の玉を描画する方法

はじめに

この記事では、p2.js 物理エンジンを使用して、1000個の玉を描画してみようと思います。

フィア: いっぱい玉が出てきたら楽しいよね!

カレン: 前回の記事も参照してみてね!

前回の記事

【p2.js】p2.js 物理エンジンを使用した自然落下

p2.js 物理エンジンを使用して1000個の玉を描画する方法

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[] = [];
 
        for (let i = 0; i < 1000; i++){
            shape[i] = new egret.Shape();
            this.addChild(shape[i]);
 
            body[i] = new p2.Body({mass:1, position:[30 + i , -100-i*10], 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 < 1000; 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.graphics.beginFill(0xff0000);
            shape.graphics.drawCircle(x, y, 5);
            shape.graphics.endFill();
 
        }
        
    }
 
 
}

p2.js 物理世界を構築

物理エンジンを使用するために、まずp2.worldを構築します。

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]);
}

物理エンジンを組み込んだオブジェクトを生成します。

基本は「p2.Body」を生成して、「p2.Plane」で形成します。Planeは厚みの無い平面です。

Body系の位置を変更するには、

Body.position = [0, 700];//[x, y 座標]
Body.position[0] = 10;//x座標
Body.position[1] = 10;//y座標

を使用します。

Body系を回転させるには、

Body.angle = Math.PI;//rad表記

を使用します。回転はラジアンで制御します。180°回転 = π、90°回転 = π/2 です。

BodyにShapeを適用し、worldに追加します。

Body.addShape(planeShape);
world.addBody(Body);

1000個の玉のコライダーを生成

const shape:egret.Shape[] = [];
const body :p2.Body[] = [];
const bodyShape : p2.Circle[] = [];
 
for (let i = 0; i < 1000; i++){
    shape[i] = new egret.Shape();
    this.addChild(shape[i]);
 
    body[i] = new p2.Body({mass:1, position:[30 + i , -100-i*10], fixedRotation:true});
    bodyShape[i] = new p2.Circle({ radius: 5 });
    body[i].addShape(bodyShape[i]);
    world.addBody(body[i]);
}

まず、1000分の玉のコライダーを生成します。少しでCPU負荷を下げるため、fixedRotation:trueで玉が回転しないようにします。

1000個の玉を描画する

function drawBall(shape:egret.Shape, x:number, y:number){
    //一旦ステージに描画した図形を消す。これがないと図形が重なって線になる。
    shape.graphics.clear();
 
    //円の描画
    shape.graphics.beginFill(0xff0000);
    shape.graphics.drawCircle(x, y, 5);
    shape.graphics.endFill();
 
}

Bodyだけでは図形は表示されないので、Shapeを使用して描画します。

描画をアップデートする

//繰り返しメソッド
egret.Ticker.getInstance().register(loopMethod,this);
 
function loopMethod(dt:number) {
    //1/60秒に一度実行
    world.step(1/60, dt/1000, 10);
 
    for (let i = 0; i < 1000; i++){
        drawBall(shape[i], body[i].position[0], body[i].position[1]);              
    }
 
}

Shapeは一度描画するだけなので、「egret.Ticker.getInstance().register(loopMethod,this);」で描画をアップデートし、動いているように表現します。

(2019/01/12 追記)

Shapeを移動させて描画する方法

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[] = [];
 
        for (let i = 0; i < 1000; i++){
            shape[i] = new egret.Shape();
            this.addChild(shape[i]);
 
            body[i] = new p2.Body({mass:1, position:[30 + i , -100-i*10], 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 < 1000; 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();
 
        }
        
    }
 
 
}

冒頭のMain.tsとの違いは、drawBallメソッド内です。

        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();
 
        }

冒頭の方では、drawCircle(x, y, 5); として、bodyの動きに合わせて描画を移動させていきました。

しかし、この方法では衝突判定を行いたいときなどで、shapeの位置が常に(x, y) =  (0, 0)となり都合が悪いときがあります。

なので、実際にshapeを移動させて、drawCircle(0, 0, 5);で描画する方がいいと思います。

また、こちらの方法ではshape.graphics.clear(); をする必要はありません。上記のコードをからclear()を削除して実行してみてください。以下のような結果が得られます。

ただし、clear()しないからといって、FPSが向上するということはなく、むしろ重くなった気もします…。clear()に関してはお好みでどうぞ(ちょっと図形がにじんでる気もします)。

HTML5 Egret Engine入門講座へ戻る