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

【p2.js】衝突したオブジェクトのHPを個別に減らす方法

【p2.js】衝突したオブジェクトのHPを個別に減らす方法

p2.js物理エンジン関連記事

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

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

【p2.js】クラス分けで拡張性が向上したp2.js物理ボールの生成方法

【p2.js】p2.js 物理エンジンを使用した衝突判定「collisionGroup」「collisionMask」

はじめに

p2.js物理エンジンを使って、ボールとブロックが衝突したときにHPを減らす方法を紹介します。

Unity等のコンポーネント指向でプログラムを組むと、以下のようなことが起きます。

これは、Box ClassのコリジョンイベントにHPを減らす処理を書いてしまったため、後からインスタンス化したBoxのHPだけが減る結果になりました。

以下に失敗したコードも載せておきます。

Main.ts (失敗コード)
enum GraphicShape{
    NONE = Math.pow(2,0),
    CIECLE = Math.pow(2,1),
    BOX = Math.pow(2,2),
}
class Main extends eui.UILayer {
 
    static timeStamp : number;
 
    public constructor() {
        super();
        this.once(egret.Event.ADDED_TO_STAGE, this.addToStage, this);
    }
 
    private addToStage() {
        GameObject.initial( this.stage );
        CreateGameScene.init();
       
    }
 
 
    
}
 
class CreateGameScene{
 
    static width : number;
    static height: number;
 
    static init() {
        this.width  = egret.MainContext.instance.stage.stageWidth;
        this.height = egret.MainContext.instance.stage.stageHeight;
 
        new CreatePhysicsWorld();
        new Ball(CreateGameScene.width/2, 0, 20);
 
//------------------------------------------------------------
//Boxを2つインスタンス化すると、1つ目のBoxにボールが当たっても、2つ目のBoxのHPが減る
 
        new Box(CreateGameScene.width/2, 500, 100, 30, 10);
        new Box(CreateGameScene.width/2, 800, 100, 30, 10);
 
//---------------------------------------------------------------
        egret.startTick(this.tickLoop, this);
 
    }
        
    static tickLoop(timeStamp:number = Main.timeStamp):boolean{
        GameObject.update();
        CreatePhysicsWorld.worldBegin(timeStamp);       
        return false;
    }
 
 
}
 
abstract class GameObject {
    
    protected shape:egret.Shape = null;
    public body : p2.Body = null;
    protected bodyShape : p2.Circle | p2.Box = null;
    protected world : p2.World = null;
    
    static objects: GameObject[];
    static display: egret.DisplayObjectContainer;
 
    constructor() {
        GameObject.objects.push(this);
    }
 
 
    static initial(mainStage: egret.DisplayObjectContainer){
        GameObject.objects = [];
        GameObject.display = mainStage;
    }
 
    abstract updateContent() : void;
 
    static update(){
        GameObject.objects.forEach(obj => obj.updateContent());
 
 
    }
 
}
 
 
 
class CreatePhysicsWorld extends GameObject{
    static world : p2.World = null;
    constructor(){
        super();
        this.createWorld();
    }
 
    createWorld(){
        CreatePhysicsWorld.world = new p2.World();
        CreatePhysicsWorld.world.sleepMode = p2.World.BODY_SLEEPING;
        CreatePhysicsWorld.world.gravity = [0, 9.8];
 
    }
    
    updateContent(){
 
 
    }
 
 
    static worldBegin(dt : number) :boolean{
       
        CreatePhysicsWorld.world.step(1/60, dt/1000, 10);
        return false;
    }
 
}
 
class Ball extends GameObject{
 
    static I:Ball = null; 
 
    constructor(x: number, y:number, radius: number){
        super();
 
        Ball.I = this;
        this.setBody(x, y, radius);
        this.setShape(radius);
    }
 
    setBody(x: number, y:number, radius: number){
        Ball.I =this;
        this.body = new p2.Body({mass : 1, position:[x,y]});
        this.bodyShape = new p2.Circle({
            radius : radius, collisionGroup: GraphicShape.CIECLE, collisionMask:GraphicShape.BOX , fixedRotation:true
        });
        this.body.addShape(this.bodyShape);
        CreatePhysicsWorld.world.addBody(this.body);
        
    }
 
    setShape(radius: number){
        this.shape = new egret.Shape();
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        this.shape.graphics.beginFill(0xff0000);
        this.shape.graphics.drawCircle(0, 0, radius);
        this.shape.graphics.endFill();
        GameObject.display.addChild(this.shape);
        
    }
 
    updateDrowShape(){
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        GameObject.display.addChild(this.shape);
    }
 
 
    updateContent(){
        this.updateDrowShape();
       
    }
 
 
}
 
class Box extends GameObject{
 
    private hp :number = null;
    private hPText : MyText = null;
 
    constructor(x: number, y:number, boxWidth: number, boxHeight: number, hp : number){
        super();
        this.hp = hp;
        this.setBody(x, y, boxWidth, boxHeight);
        this.setShape(boxWidth, boxHeight);
        this.hPText = new MyText(x,y, hp.toString() ,100, 0.5,0xFFFFFF,"Meiryo",0x000000, 0);
        CreatePhysicsWorld.world.on("beginContact",  this.collision, this);
    }
 
    setBody(x: number, y:number, width: number, height : number){
 
        this.body = new p2.Body({mass : 1, position:[x,y], type:p2.Body.STATIC});
        this.bodyShape = new p2.Box({
            width : width, height : height,collisionGroup: GraphicShape.BOX, collisionMask:GraphicShape.CIECLE, fixedRotation:true
        });
 
        this.body.addShape(this.bodyShape);
        CreatePhysicsWorld.world.addBody(this.body);
        
    }
 
    setShape(width: number, height : number){
 
        this.shape = new egret.Shape();
        this.shape.anchorOffsetX += width/2;//p2とEgretは座標軸とアンカー位置が違うので調整
        this.shape.anchorOffsetY += height/2;
        this.shape.x = this.body.position[0] /*+ width*/;
        this.shape.y = this.body.position[1] /*- height/2*/;
        this.shape.graphics.beginFill(0xff0000);
        this.shape.graphics.drawRect(0, 0, width , height);
        this.shape.graphics.endFill();
        GameObject.display.addChild(this.shape);
        
    }
 
    setHpText(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number){
        new MyText(x,y,text,100, 0.5,0xFFFFFF,"Meiryo",0x000000, 0);
    }
 
    updateContent(){
 
    }
 
 
//----------------------------------------------------
//コリジョンイベント
 
    collision(evt){
        const bodyA: p2.Body = evt.bodyA;
        const shapeA  = evt.shapeA;
 
        Ball.I.body.applyForceLocal([0,-10000],[0,0]);
 
        this.hp -= 1;
        this.hPText.updateText(this.hp.toString());
    }
 
//------------------------------------------------------
}
 
class MyText extends GameObject{
    public myTextField : egret.TextField | null = null;
    public myText : string | null = null;
 
    public x : number|null = 0;
    public y : number|null = 0;
    public size : number|null = 1;
    public ratio : number|null = 1;
    public color : number|null = 0x000000;
    public stColor : number|null =0x0000000;
    public stSize : number|null = 0;
    public font : string|null = "Meiryo";
    public text : string|null = "";
 
    public constructor(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number){
        super();
        this.newText(x, y, text, size, ratio, color, font, stColor, stSize);
        this.x = x;
        this.y = y;
        this.text = text;
        this.size = size;
        this.ratio = ratio;
        this.color= color;
        this.font = font;
        this.stColor = stColor;
        this.stSize = stSize;
    }
 
    protected newText(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number): void {
        
        this.myTextField = new egret.TextField();
        this.myTextField.x = x || 0;
        this.myTextField.y = y || 0;
 
        this.myTextField.scaleX = ratio || 1;
        this.myTextField.scaleY = ratio || 1;
        this.myTextField.textFlow = <Array<egret.ITextElement>>[ 
            {text: text, 
                style: {
                    "textColor": color || 0x000000, "size": size ||1, "fontFamily": font ||"Meiryo", "strokeColor": stColor || 0x000000, "stroke": stSize || 0,
                }
            }
        ];    
        GameObject.display.addChild(this.myTextField);
 
    }
 
    public updateText(text:string): void{
 
        this.myTextField.textFlow = <Array<egret.ITextElement>>[ 
            {text: text, 
                style: {
                    "textColor": this.color || 0x000000, "size": this.size ||1, "fontFamily": this.font ||"Meiryo", "strokeColor": this.stColor || 0x000000, "stroke": this.stSize || 0,
                }
            }
        ];    
        GameObject.display.addChild(this.myTextField);
    }
 
    
    updateContent(){
 
    }
 
 
 
}

【p2.js】衝突したオブジェクトのHPを減らす方法

Main.ts
enum GraphicShape{
    NONE = Math.pow(2,0),
    CIECLE = Math.pow(2,1),
    BOX = Math.pow(2,2),
}
class Main extends eui.UILayer {
 
    static timeStamp : number;
 
    public constructor() {
        super();
        this.once(egret.Event.ADDED_TO_STAGE, this.addToStage, this);
    }
 
    private addToStage() {
        GameObject.initial( this.stage );
        CreateGameScene.init();
       
    }
 
 
    
}
 
class CreateGameScene{
 
    static width : number;
    static height: number;
    static box:Box[] = [];
 
    static init() {
        this.width  = egret.MainContext.instance.stage.stageWidth;
        this.height = egret.MainContext.instance.stage.stageHeight;
 
        new CreatePhysicsWorld();
        new Ball(CreateGameScene.width/2, 0, 20);
 
//------------------------------------------------------------
//Boxを2つインスタンス化すると、1つ目のBoxにボールが当たっても、2つ目のBoxのHPが減る
 
        let a = new Box(CreateGameScene.width/2, 500, 100, 30, 10);
        let b = new Box(CreateGameScene.width/2, 800, 100, 30, 10);
        CreateGameScene.box.push(a);
        CreateGameScene.box.push(b);
 
//---------------------------------------------------------------
        egret.startTick(this.tickLoop, this);
 
    }
        
    static tickLoop(timeStamp:number = Main.timeStamp):boolean{
        GameObject.update();
        CreatePhysicsWorld.worldBegin(timeStamp);       
        return false;
    }
 
 
}
 
abstract class GameObject {
    
    public shape:egret.Shape = null;
    public body : p2.Body = null;
    protected bodyShape : p2.Circle | p2.Box = null;
    protected world : p2.World = null;
    
    static objects: GameObject[];
    static display: egret.DisplayObjectContainer;
 
    constructor() {
        GameObject.objects.push(this);
    }
 
 
    static initial(mainStage: egret.DisplayObjectContainer){
        GameObject.objects = [];
        GameObject.display = mainStage;
    }
 
    abstract updateContent() : void;
 
    static update(){
        GameObject.objects.forEach(obj => obj.updateContent());
 
 
    }
 
}
 
 
 
class CreatePhysicsWorld extends GameObject{
    static world : p2.World = null;
    constructor(){
        super();
        this.createWorld();
    }
 
    createWorld(){
        CreatePhysicsWorld.world = new p2.World();
        CreatePhysicsWorld.world.sleepMode = p2.World.BODY_SLEEPING;
        CreatePhysicsWorld.world.gravity = [0, 9.8];
 
    }
    
    updateContent(){
 
 
    }
 
 
    static worldBegin(dt : number) :boolean{
       
        CreatePhysicsWorld.world.step(1/60, dt/1000, 10);
        return false;
    }
 
}
 
class Ball extends GameObject{
 
    static I:Ball = null; 
 
    constructor(x: number, y:number, radius: number){
        super();
 
        Ball.I = this;
        this.setBody(x, y, radius);
        this.setShape(radius);
        CreatePhysicsWorld.world.on("beginContact",  this.collision, this);
 
    }
 
    setBody(x: number, y:number, radius: number){
        Ball.I =this;
        this.body = new p2.Body({mass : 1, position:[x,y]});
        this.bodyShape = new p2.Circle({
            radius : radius, collisionGroup: GraphicShape.CIECLE, collisionMask:GraphicShape.BOX , fixedRotation:true
        });
        this.body.addShape(this.bodyShape);
        CreatePhysicsWorld.world.addBody(this.body);
        
    }
 
    setShape(radius: number){
        this.shape = new egret.Shape();
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        this.shape.graphics.beginFill(0xff0000);
        this.shape.graphics.drawCircle(0, 0, radius);
        this.shape.graphics.endFill();
        GameObject.display.addChild(this.shape);
        
    }
 
    updateDrowShape(){
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        GameObject.display.addChild(this.shape);
    }
 
 
    updateContent(){
        this.updateDrowShape();
       
    }
 
//----------------------------------------------------
//コリジョンイベント
 
    collision(evt){
        const bodyA: p2.Body = evt.bodyA;
        const shapeA  = evt.shapeA;
 
        this.body.applyForceLocal([0,-10000],[0,0]);
 
 
        CreateGameScene.box.forEach(obj =>{
            //このifが無いと、すべてのboxのhpが減る
            if(obj.body == bodyA){
                obj.hp -= 1;
                obj.hPText.updateText(obj.hp.toString());
 
            }
        });
 
    }
 
//------------------------------------------------------
 
}
 
class Box extends GameObject{
 
    public hp :number = null;
    public hPText : MyText = null;
 
    constructor(x: number, y:number, boxWidth: number, boxHeight: number, hp : number){
        super();
        this.hp = hp;
        this.setBody(x, y, boxWidth, boxHeight);
        this.setShape(boxWidth, boxHeight);
        this.hPText = new MyText(x,y, hp.toString() ,100, 0.5,0xFFFFFF,"Meiryo",0x000000, 0);
 
    }
 
    setBody(x: number, y:number, width: number, height : number){
 
        this.body = new p2.Body({mass : 1, position:[x,y], type:p2.Body.STATIC});
        this.bodyShape = new p2.Box({
            width : width, height : height,collisionGroup: GraphicShape.BOX, collisionMask:GraphicShape.CIECLE, fixedRotation:true
        });
 
        this.body.addShape(this.bodyShape);
        CreatePhysicsWorld.world.addBody(this.body);
        
    }
 
    setShape(width: number, height : number){
 
        this.shape = new egret.Shape();
        this.shape.anchorOffsetX += width/2;//p2とEgretは座標軸とアンカー位置が違うので調整
        this.shape.anchorOffsetY += height/2;
        this.shape.x = this.body.position[0] /*+ width*/;
        this.shape.y = this.body.position[1] /*- height/2*/;
        this.shape.graphics.beginFill(0xff0000);
        this.shape.graphics.drawRect(0, 0, width , height);
        this.shape.graphics.endFill();
        GameObject.display.addChild(this.shape);
        
    }
 
    setHpText(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number){
        new MyText(x,y,text,100, 0.5,0xFFFFFF,"Meiryo",0x000000, 0);
    }
 
    updateContent(){
 
    }
 
 
 
}
 
class MyText extends GameObject{
    public myTextField : egret.TextField | null = null;
    public myText : string | null = null;
 
    public x : number|null = 0;
    public y : number|null = 0;
    public size : number|null = 1;
    public ratio : number|null = 1;
    public color : number|null = 0x000000;
    public stColor : number|null =0x0000000;
    public stSize : number|null = 0;
    public font : string|null = "Meiryo";
    public text : string|null = "";
 
    public constructor(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number){
        super();
        this.newText(x, y, text, size, ratio, color, font, stColor, stSize);
        this.x = x;
        this.y = y;
        this.text = text;
        this.size = size;
        this.ratio = ratio;
        this.color= color;
        this.font = font;
        this.stColor = stColor;
        this.stSize = stSize;
    }
 
    protected newText(x:number, y:number, text:string, size:number, ratio:number, color:number, font:string, stColor:number, stSize:number): void {
        
        this.myTextField = new egret.TextField();
        this.myTextField.x = x || 0;
        this.myTextField.y = y || 0;
 
        this.myTextField.scaleX = ratio || 1;
        this.myTextField.scaleY = ratio || 1;
        this.myTextField.textFlow = <Array<egret.ITextElement>>[ 
            {text: text, 
                style: {
                    "textColor": color || 0x000000, "size": size ||1, "fontFamily": font ||"Meiryo", "strokeColor": stColor || 0x000000, "stroke": stSize || 0,
                }
            }
        ];    
        GameObject.display.addChild(this.myTextField);
 
    }
 
    public updateText(text:string): void{
 
        this.myTextField.textFlow = <Array<egret.ITextElement>>[ 
            {text: text, 
                style: {
                    "textColor": this.color || 0x000000, "size": this.size ||1, "fontFamily": this.font ||"Meiryo", "strokeColor": this.stColor || 0x000000, "stroke": this.stSize || 0,
                }
            }
        ];    
        GameObject.display.addChild(this.myTextField);
    }
 
    
    updateContent(){
 
    }
 
 
 
}

CreateGameSceneにboxの配列を追加

class CreateGameScene{
 
    static width : number;
    static height: number;
    static box:Box[] = [];
 
    static init() {
        this.width  = egret.MainContext.instance.stage.stageWidth;
        this.height = egret.MainContext.instance.stage.stageHeight;
 
        new CreatePhysicsWorld();
        new Ball(CreateGameScene.width/2, 0, 20);
 
//------------------------------------------------------------
//Boxを2つインスタンス化すると、1つ目のBoxにボールが当たっても、2つ目のBoxのHPが減る
 
        let a = new Box(CreateGameScene.width/2, 500, 100, 30, 10);
        let b = new Box(CreateGameScene.width/2, 800, 100, 30, 10);
        CreateGameScene.box.push(a);
        CreateGameScene.box.push(b);
 
//---------------------------------------------------------------
        egret.startTick(this.tickLoop, this);
 
    }
        
    static tickLoop(timeStamp:number = Main.timeStamp):boolean{
        GameObject.update();
        CreatePhysicsWorld.worldBegin(timeStamp);       
        return false;
    }
 
 
}

インスタンス化したBoxをstatic box の配列内にpush(配列に格納する) します。これで、別のクラスでもBoxを利用できます。

Ball.ts
class Ball extends GameObject{
 
    static I:Ball = null; 
 
    constructor(x: number, y:number, radius: number){
        super();
 
        Ball.I = this;
        this.setBody(x, y, radius);
        this.setShape(radius);
        CreatePhysicsWorld.world.on("beginContact",  this.collision, this);
 
    }
 
    setBody(x: number, y:number, radius: number){
        Ball.I =this;
        this.body = new p2.Body({mass : 1, position:[x,y]});
        this.bodyShape = new p2.Circle({
            radius : radius, collisionGroup: GraphicShape.CIECLE, collisionMask:GraphicShape.BOX , fixedRotation:true
        });
        this.body.addShape(this.bodyShape);
        CreatePhysicsWorld.world.addBody(this.body);
        
    }
 
    setShape(radius: number){
        this.shape = new egret.Shape();
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        this.shape.graphics.beginFill(0xff0000);
        this.shape.graphics.drawCircle(0, 0, radius);
        this.shape.graphics.endFill();
        GameObject.display.addChild(this.shape);
        
    }
 
    updateDrowShape(){
        this.shape.x = this.body.position[0];
        this.shape.y = this.body.position[1];
        GameObject.display.addChild(this.shape);
    }
 
 
    updateContent(){
        this.updateDrowShape();
       
    }
 
//----------------------------------------------------
//コリジョンイベント
 
    collision(evt){
        const bodyA: p2.Body = evt.bodyA;
        const shapeA  = evt.shapeA;
 
        this.body.applyForceLocal([0,-10000],[0,0]);
 
 
        CreateGameScene.box.forEach(obj =>{
            //このifが無いと、すべてのboxのhpが減る
            if(obj.body == bodyA){
                obj.hp -= 1;
                obj.hPText.updateText(obj.hp.toString());
 
            }
        });
 
    }
 
//------------------------------------------------------
 
}

Boxに記述していたコリジョンイベントをBallへ移しました。コリジョンイベントはどこに書いてもいいのですが、今回はボールが能動的に動くので、ここに入れました。

コリジョンイベントの中でboxの配列をforEachで取得します。forEachを使用しないと、hpやhpTextをstaticにする必要があります。しかし、staticにすると、個別にhpを設定することはできません。

また、forEachは全てのBoxを参照してしまうので、if(obj.body == bodyA)で衝突したboxのbodyと一致したときのみhpを減らすようにしました。

CreateGameScene.box.forEach(obj =>{
    //このifが無いと、すべてのboxのhpが減る
    if(obj.body == bodyA){
        obj.hp -= 1;
        obj.hPText.updateText(obj.hp.toString());
 
    }
});

if(obj.body == bodyA)がないと、以下のようになります。

HTML5 Egret Engine入門講座へ戻る