はじめに
この記事では、p2.js 物理エンジンを使用して、1000個の玉を描画してみようと思います。
前回の記事
p2.js 物理エンジンを使用して1000個の玉を描画する方法
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 |
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を構築します。
1 2 3 |
const world = new p2.World(); world.sleepMode = p2.World.BODY_SLEEPING; world.gravity = [0, 9.8 * 50]; |
見えない壁を構築
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 |
//見えない壁や地面の生成 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系の位置を変更するには、
1 2 3 |
Body.position = [0, 700];//[x, y 座標] Body.position[0] = 10;//x座標 Body.position[1] = 10;//y座標 |
を使用します。
Body系を回転させるには、
1 |
Body.angle = Math.PI;//rad表記 |
を使用します。回転はラジアンで制御します。180°回転 = π、90°回転 = π/2 です。
BodyにShapeを適用し、worldに追加します。
1 2 |
Body.addShape(planeShape); world.addBody(Body); |
1000個の玉のコライダーを生成
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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個の玉を描画する
1 2 3 4 5 6 7 8 9 10 |
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を使用して描画します。
描画をアップデートする
1 2 3 4 5 6 7 8 9 10 11 12 |
//繰り返しメソッド 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
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 |
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メソッド内です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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()に関してはお好みでどうぞ(ちょっと図形がにじんでる気もします)。