Phaser 3 : ブロック崩しを作ってみる ④ Dataを使ってみる

引き続きブロック崩しを作っていきます。
今回はブロックを配置してみましょう。 gpnotes.hatenablog.jp

ブロックのグループを作る

まずはじめにブロックのオブジェクトを作成しましょう。
ブロック用のスプライトは自作しました。
Phaserの機能で色付けをしようと思っているので、真っ白なスプライトになっています。
(ここにブロック用のスプライトを張り付けているのですが真っ白なので見えないと思います)
f:id:Phaser_3:20181126150219p:plain ブロックオブジェクトは複数追加する予定なのではじめにブロック管理用のグループを作成します。
add.Groupでグループを作成し、group.addでグループにオブジェクトを追加できます。

            //ブロック用のグループを作成
            this.blockGroup = this.add.group();


            for (let i = 0; i < 10; i++) {
                //物理オブジェクトを作成
                let block = this.matter.add.image(
                    this.sys.canvas.width / 2,
                    50,
                    'block', null,
                    { label: "block", ignorePointer: true }
                );

                //スプライトに色付け
                block.setTint(0xff00ff, 0xffff00, 0x0000ff, 0xff0000);

                //ブロックの反発係数などを設定
                block.setBounce(1.2);
                (block.body as Phaser.Physics.Arcade.Body).mass = 10

                //グループにブロックを追加
                this.blockGroup.add(block)
            }

block.setTintでブロックのTintを設定しています。
Phaser2までは確か単純な色変更しかできなかったのですが、3からはグラデーションが使えるようになりました。
上記のまま実行してみると、ブロックをすべて同じ位置に置いているので重なっている部分がはじかれてしまいます。 f:id:Phaser_3:20181126150457p:plain

Phaser.Actionを使ってみる

適当にブロックを配置するためにPhaser3の新機能、Actionを使ってみましょう。

Phaser 3 API Documentation - Namespace: Actions Phaser 3 Examples

Phaser.actionは配列内のオブジェクトに一括で操作を行える関数です。
PlaceOnLineなどの関数を使えばオブジェクトを一直線に配置したりもできます。
今回はPlaceOnCircleを使って円周上にブロックを配置してみます。
まずPhaser.Geomを使い配置対象となる円オブジェクトを作成し、Phaser.Actions.PlaceOnCircleに配置したいオブジェクト群=blockgroupとcircleを渡します。

            //円周上に配置
            var circle = new Phaser.Geom.Circle(400, 300, 260);
            Phaser.Actions.PlaceOnCircle(this.blockGroup.getChildren(), circle);

実行してみるときれいに円状に配置されています。
f:id:Phaser_3:20181126152103p:plain

Dataを使ってブロックのHPを設定する

次はPhaser3からの新機能、Dataを用いてブロックにHPを設定してみます。

DataはPhaserのゲームオブジェクトならなんにでも設定できる

DataはPhaserのゲームオブジェクトに任意の値を持たせられる仕組みです。

Phaser 3 API Documentation - Class: DataManager

gameObject.setData("任意のキー",任意の値)でDataを設定でき、gameObject.getData("任意のキー")でデータを取得できます。
さらにgameObject.on('changedata', function () , context); を用いることでデータ変動時の処理を記述することができるので、今回はDataを用いてブロックとボールの衝突回数が一定の数値を超えた場合に破壊する処理を作ってみます。
先ほどのブロックオブジェクト作成ループの中に

                //Dataの初期設定
                block.setData("count", 0);

を追加します。
次にデータ変動時の処理を追加します。

                //Dataの変更時に実行されるイベントを記述
                block.on('changedata', function (gameObject, key, value, resetValue) {
                    //countが変更された場合
                    if (key === 'count') {
                        //ぶつかった回数が3回以上ならオブジェクトを削除
                        if (value > 3) {
                            block.destroy();
                        }
                    }
                }, this);

この処理はblockの保持しているデータのいずれかに処理があった場合に呼ばれます。
上記の処理はキーがcountの場合、数値が3以上であればブロックを破壊するという処理になっています。

ブロックとボールの衝突時にDataを変動させる

次はブロックとボールの衝突時の処理を書きます。
matterによる衝突の検知はupdateで待ち受けるのではなく、matterが常時実行している衝突判定関数に処理を追加していく形になるようです。
MyScene1のcreateの中に以下の処理を記述します。

            //物理オブジェクトの当たり判定
            this.matter.world.on('collisionstart', function (event) {
                for (var i = 0; i < event.pairs.length; i++) {
                    var bodyA = this.GetCollide(event.pairs[i].bodyA);
                    var bodyB = this.GetCollide(event.pairs[i].bodyB);

                    if ((bodyA.label === "ball" && bodyB.label === "block") || (bodyA.label === "block" && bodyB.label === "ball")) {
                        let body: Phaser.Physics.Matter.Image;
                        if (bodyA.label === "block") {
                            body = bodyA.parent.gameObject;
                        } else {
                            body = bodyB.parent.gameObject;
                        }
                        body.setData("count", body.getData("count") + 1);
                    }
                }
            }, this);

上の処理は物理オブジェクトの衝突があった場合に呼ばれ、その時点で衝突しているオブジェクトの組み合わせすべてをチェックし衝突しているオブジェクトのlabelがballとblockであった場合にブロックとボールの衝突があったと判断し、衝突したブロックのcountを+1します。
(labelはオブジェクトの作成時に設定しています)
上記の処理の補佐のために以下の関数を用意します。
これはPhaserの掲示板にあったものを持ってきたので正直中身がよくわからないのですが、おそらく物理ボディに親子関係があった場合親を返すものだと思います。
今回は親子設定を特にしていないのでいらない気もしますが使っておきます。

        //衝突を処理するための関数
        public GetCollide(body): Phaser.Physics.Arcade.Body {
            if (body.parent === body) {
                return body;
            }
            while (body.parent !== body) {
                body = body.parent;
            }
            return body;

        }


以上でブロックへの衝突判定ができました。
実行結果はこちらになります。
(gifが長くなってしまったので途中からの録画になります)

f:id:Phaser_3:20181126155530g:plain

DataはPhaserのオブジェクトにちょっとした処理を付け足したいときに大変便利だと思います。
またシーンそのものもデータの構造を持っており、UI要素の更新にも使えると思うので、自分は積極的に組み込んでいくつもりです。

まとめ

  • add.Groupでグループを作成し、group.addでグループにオブジェクトを追加

  • Phaser.Actiopnでオブジェクトの配列やグループに一括処理を行える

  • setDataでDataの設定、getDataでDataの取得

ソースは以下にアップロードしました。 http://firestorage.jp/download/60c4bb4f1b251b8d5b65e0af49601f2941f8c38e

Phaser 3 : ブロック崩しを作ってみる ② 入力処理(キーボード、ゲームパッド)を作る

前回に続きブロック崩しを作っていきます。

gpnotes.hatenablog.jp

まずは入力を検知して自機を操作できるようにしてみましょう。

入力検知用のシーンを作る

入力検知は長くなりそうなので、入力検知のためだけのシーン InputManagerを作成しました。

キーボードの入力

this.input.keyboard.addKey でキーコードとキーの紐づけ

まずはキーボードの入力を取得してみましょう。
自機の操作は上下左右の移動と回転が必要なので、横軸:xAxis 縦軸: yAxis 回転:xRotate の三つの変数を用意し、上下左右+左右回転の六つのキーを用意します。
Phaser.Input.Keyboard.Key がPhaserのキー入力ハンドラです。

class InputManager extends Phaser.Scene {

    //移動用の入力
    public xAxis: number;
    public yAxis: number;

    //回転用の入力
    public xRotate: number;

    //キーボードの入力取得用キーオブジェクト
    private keyA: Phaser.Input.Keyboard.Key;
    private keyD: Phaser.Input.Keyboard.Key;
    private keyW: Phaser.Input.Keyboard.Key;
    private keyS: Phaser.Input.Keyboard.Key;
    private keyLeft: Phaser.Input.Keyboard.Key;
    private keyRight: Phaser.Input.Keyboard.Key;

    constructor() {
        super({ key: 'InputManager', active: false });
    }
}

次にcreateでキーハンドラに対応するキーを設定します。
this.input.keyboard.addKeyでキーコードとの紐づけができます。
例えば

this.keyA  = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);

のコードでは、キーボードのAになんらかのイベントが起きた時にkeyAがイベントの詳細を保持するようになります。

    create() {
     
        //キーの登録
        this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
        this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
        this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
        this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
        this.keyLeft = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT);
        this.keyRight = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT);

    }
isDownで押下の取得

次にupdateで入力を処理しましょう。
key.isDownでキーが押されているかどうかを判定できます。 ADで左右の移動量、WSで上下、矢印キー←→で左右回転の移動を処理します。

    update() {
            //key.isDownで指定のキーの入力を取得できる
            if (this.keyA.isDown) {
                this.xAxis = -1;
            } else if (this.keyD.isDown) {
                this.xAxis = 1;
            } else {
                this.xAxis = 0;
            }

            if (this.keyW.isDown) {
                this.yAxis = -1;
            } else if (this.keyS.isDown) {
                this.yAxis = 1;
            } else {
                this.yAxis = 0;
            }

            if (this.keyLeft.isDown) {
                this.xRotate = -1;
            } else if (this.keyRight.isDown) {
                this.xRotate = 1;
            } else {
                this.xRotate = 0;
            }
    }

以上でキーボードの入力が作成できました。

ゲームパッドの入力

次にゲームパッドの入力を処理していきます。
はじめにゲームパッドのオブジェクトを宣言しておきましょう。

    //ゲームパッドの設定
    public gamepad: Phaser.Input.Gamepad.Gamepad;
input.gamepad.onceでゲームパッドを検知する

キーボードはPhaserが自動で取得してくれますが、ゲームパッドは自分で設定しなければなりません。
公式サンプルにinput.gamepad.onceを用いて最初に入力があったゲームパッドを取得する方法があったのでそちらを使います。
input.gamepad.onceはキーで指定されたゲームパッドのイベントがあった場合に一度だけ実行されます。 キーにdownを指定すれば初めにボタンを押したときだけ実行されます。

        //ゲームパッドの検知
        this.input.gamepad.once('down', function (pad, button, index) {
            this.gamepad = pad;
        }, this);
input.gamepad.axisで各軸の入力を取得する

ゲームパッドは各軸の入力をそのまま軸用変数に代入すればいいので処理が楽です。
gamepad.axis[]で各軸の入力を取得できます。
手持ちのXbox360ゲームパッドでは[0]が左スティックX軸、[1]が左スティックY軸、[2]が右スティックX軸でした。

        if (this.gamepad) {
            //ゲームパッドのx軸を取得する
            //手持ちのゲームパッドはブレがあるのでしきい値を設定する(0.2)
            this.xAxis = this.gamepad.axes[0].getValue();
            this.yAxis = this.gamepad.axes[1].getValue();
            this.xRotate = this.gamepad.axes[2].getValue();
        }

これでゲームパッドの設定も完了しました。
this.gamepadは設定したゲームパッドが接続されている間はtrueを返します。
これを用いてゲームパッドが有効になっている間はキーボードの入力を不可にしました。 最終的な入力処理シーンのコードはこちらです。

class InputManager extends Phaser.Scene {

    //ゲームパッドの設定
    public gamepad: Phaser.Input.Gamepad.Gamepad;

    //移動用の入力
    public xAxis: number;
    public yAxis: number;

    //回転用の入力
    public xRotate: number;

    //キーボードの入力取得用キーオブジェクト
    private keyA: Phaser.Input.Keyboard.Key;
    private keyD: Phaser.Input.Keyboard.Key;
    private keyW: Phaser.Input.Keyboard.Key;
    private keyS: Phaser.Input.Keyboard.Key;
    private keyLeft: Phaser.Input.Keyboard.Key;
    private keyRight: Phaser.Input.Keyboard.Key;

    constructor() {
        super({ key: 'InputManager', active: false });
    }

    create() {
        //ゲームパッドの検知
        this.input.gamepad.once('down', function (pad, button, index) {
            this.gamepad = pad;
        }, this);

        //キーの登録
        this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
        this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
        this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
        this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
        this.keyLeft = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT);
        this.keyRight = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT);

    }

    update() {
        if (this.gamepad) {
            //ゲームパッドのx軸を取得する
            //手持ちのゲームパッドはブレがあるのでしきい値を設定する(0.2)
            this.xAxis = this.gamepad.axes[0].getValue();
            this.yAxis = this.gamepad.axes[1].getValue();
            this.xRotate = this.gamepad.axes[2].getValue();
        } else {
            //key.isDownで指定のキーの入力を取得できる
            if (this.keyA.isDown) {
                this.xAxis = -1;
            } else if (this.keyD.isDown) {
                this.xAxis = 1;
            } else {
                this.xAxis = 0;
            }

            if (this.keyW.isDown) {
                this.yAxis = -1;
            } else if (this.keyS.isDown) {
                this.yAxis = 1;
            } else {
                this.yAxis = 0;
            }

            if (this.keyLeft.isDown) {
                this.xRotate = -1;
            } else if (this.keyRight.isDown) {
                this.xRotate = 1;
            } else {
                this.xRotate = 0;
            }
           
        }
    }
}

コントローラーの入力を物理オブジェクトの移動に反映する

MyScene1で入力処理シーンを呼び出して入力をオブジェクトの移動に反映させてみます。
まずはInputManagerシーンを宣言します。

    //入力検知シーンの設定
    public controller: InputManager;

createの最後でInputManagerシーンをlaunchしましょう。

create(){
~~~
          this.controller = this.scene.get("InputManager") as InputManager;
~~~
}

updateに移動用の処理を追加します。
angleで回転角度を、setVelocityで自機の速度を制御できます。
this.controllerから各軸の変数を取得して代入します。

        //中心の球の移動処理
        this.center.angle += this.controller.xRotate*this.speed;
        this.center.setVelocity(this.controller.xAxis * this.speed, this.controller.yAxis * this.speed);

これで自機を操作できるようになりました。

f:id:Phaser_3:20181122155909g:plain

MyScene1のサンプルコード全文はこちらです。

class MyScene1 extends Phaser.Scene {

    //物理オブジェクトの宣言
    public center: Phaser.Physics.Matter.Image;
    public bar: Phaser.Physics.Matter.Image;
    public ball: Phaser.Physics.Matter.Image;
    public text: Phaser.GameObjects.Text;

    //自機の移動速度
    public speed: number = 10;

    //入力検知シーンの設定
    public controller: InputManager;

    constructor() {
        super({ key: 'MyScene1', active: true });
    }

    preload() {
        //画像の読み込み
        this.load.image('ball', 'assets/images/ball.png');
        this.load.image('bar', 'assets/images/bar.png');
    }

    create() {

        this.text = this.add.text(10, 10, "0", { font: '16px Courier', fill: '#000' });

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

        //ワールドの重力を0に設定
        this.matter.world.disableGravity()

        //画面端に当たり判定を設定
        this.matter.world.setBounds(0, 0, this.sys.canvas.width, this.sys.canvas.height);

        //物理オブジェクトを作成
        this.ball = this.matter.add.image(
            this.sys.canvas.width/2,
            0,
            'ball', null,
            { shape: { type: 'circle', radius: 16 }, label: "ball", ignorePointer: true }
        );

        //ボールの反発係数、初期速度、摩擦係数を設定
        this.ball.setBounce(1.01);
        this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
        this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        this.ball.setFriction(0, 0, 0)

        //球を画面中央に配置
        this.center = this.matter.add.image(
            this.sys.canvas.width/2,
            this.sys.canvas.height/2,
            'ball', null,
            { shape: { type: 'circle', radius: 16 }, label: "center", ignorePointer: true }
        );
        this.center.setScale(2,2)

        //バーを球の周辺部に配置
        this.bar = this.matter.add.image(
            this.sys.canvas.width / 2,
            this.sys.canvas.height / 2 - 70,
            'bar',
            null,
            { label: "bar" }
        );

        //バーの反発係数を少し高くするとボールが勢いよく反射する
        this.bar.setBounce(1.2)
        this.bar.setFriction(0, 0, 0)

        //InputManagerシーンを実行
        this.scene.launch("InputManager")
        this.controller = this.scene.get("InputManager") as InputManager;
        
    }

    update() {
        //中心の球の移動処理
        this.center.angle += this.controller.xRotate*this.speed;
        this.center.setVelocity(this.controller.xAxis * this.speed, this.controller.yAxis * this.speed);

        //バーの位置と角度を自機の向きに合わせる
        this.bar.x = this.center.x + 70 * Math.cos(this.center.rotation);
        this.bar.y = this.center.y + 70 * Math.sin(this.center.rotation);
        this.bar.angle = this.center.angle - 90 ;

        //ボールの速度の取得
        let vel = (this.ball.body as Phaser.Physics.Arcade.Body).velocity

        //速度が落ちてきたら適当に設定する
        if ((Math.abs(vel.x) + Math.abs(vel.y)) < 10) {
            this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
            this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        }
    }
}

まとめ

  • this.input.keyboard.addKeyでキーハンドラとキーコードの紐づけ
  • ゲームパッドはまずはじめに検知処理を書く

Phaser 3 : ブロック崩しを作ってみる ① Physicsを使う

Phaser 3の物理エンジンを使ってブロック崩しの制作に挑戦してみます。
アクションゲームを作ろうと思ったら自分で物理演算の処理を作るのが一番だと思うのですが、 ブロック崩しなどの簡単なゲームであれば組み込みの物理エンジンで十分作れるはずです。
今回の制作ではスタンダードなブロック崩しではなく、全周囲型のブロック崩しを作ってみようと思います。 ボールは画面四辺で跳ね回り、バーで自機を守りながらブロックを消していくゲームをイメージしています。

GameConfigで使用する物理エンジンを設定

Phaser3にはArcarde、Matter、Impactの三つのPhysicsプラグインが用意されています。
Arcadeは跳ね返りの処理などが味気ない(接触相手の速度や形状にかかわらず単純に接触時の入射ベクトルのみから反射ベクトルを割り出している感じです)ので、今回はmatterを使ってみました。
使用する物理エンジンはGameConfigで設定できます。

var config: GameConfig = {
    parent: 'content',
    type: Phaser.AUTO,
    width: 600,
    height: 600,
    physics: {
        default: 'matter',
        matter: {
            enableSleeping: true
        }
    },
    scene: [MyScene1],
}

ボールが跳ね返るだけのシーンを作る

次は実際に物理オブジェクトを作ってみましょう。
MyScene1でボールとバーを宣言し、画像を読み込みます。
ボールとバーの画像は自作しました。
f:id:Phaser_3:20181121154750p:plain
f:id:Phaser_3:20181121154801p:plain

class MyScene1 extends Phaser.Scene {

    //物理オブジェクトの宣言
    public center: Phaser.Physics.Matter.Image;
    public bar: Phaser.Physics.Matter.Image;
    public ball: Phaser.Physics.Matter.Image;

    constructor() {
        super({ key: 'MyScene1', active: true });
    }

    preload() {
        //画像の読み込み
        this.load.image('ball', 'assets/images/ball.png');
        this.load.image('bar', 'assets/images/bar.png');
    }

createで物理オブジェクトを作成します。
matter.add.imageで物理ボディを持ったオブジェクトを作成することができます。 こちらmatter.jsに変更があったのか、Phaser 3のサンプルコード集には使えないコードがサンプルがいくつもあったので宣言に結構苦労しました。
特に球体物理ボディを設定するsetCircleはサンプルでは引数を指定していませんが、現行のPhaserでは半径とoptionの2つの引数が必要になっています。
matter.add.imageを設定したら、あとは球の反発係数などを設定していきます。

    create() {

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

        //ワールドの重力を0に設定
        this.matter.world.disableGravity()

        //画面端に当たり判定を設定
        this.matter.world.setBounds(0, 0, this.sys.canvas.width, this.sys.canvas.height);

        //物理オブジェクトを作成
        this.ball = this.matter.add.image(
            this.sys.canvas.width / 2,
            0,
            'ball', null,
            { shape: { type: 'circle', radius: 16 }, ignorePointer: true }
        );

        //ボールの反発係数、初期速度、摩擦係数を設定
        this.ball.setBounce(1.01);
        this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
        this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        this.ball.setFriction(0, 0, 0)

    }

これでボールが跳ね返るだけの単純なシーンができました。 f:id:Phaser_3:20181121155616g:plain

次はボールを跳ね返すバーをと自機を追加します。
createに以下を追記します。

        //球を画面中央に配置
        this.center = this.matter.add.image(
            this.sys.canvas.width/2,
            this.sys.canvas.height/2,
            'ball', null,
            { shape: { type: 'circle', radius: 16}, ignorePointer: true }
        );
        this.center.setScale(2,2)

        //バーを球の周辺部に配置
        this.bar = this.matter.add.image(
            this.sys.canvas.width / 2,
            this.sys.canvas.height / 2 - 70,
            'bar'
        );
        //バーの反発係数を少し高くするとボールが勢いよく反射する
        this.bar.setBounce(1.2)
        this.bar.setFriction(0, 0, 0)

これでバーと自機が画面に追加されました。 f:id:Phaser_3:20181121160025g:plain

バーを自機の円周上に配置する

全周型ブロック崩しということで、バーに自機のまわりを周回させたいと思います。
updateで自機のangleを取得し、自機の位置と角度に沿った位置にバーを配置します。

    update() {
        //中心の球の向きをボールに合わせる
        this.center.setRotation(Math.atan2(this.ball.y - this.center.y, this.ball.x - this.center.x));

        //バーの位置と角度をボールの向きに合わせる
        this.bar.x = this.center.x + 70 * Math.cos(this.center.rotation);
        this.bar.y = this.center.y + 70 * Math.sin(this.center.rotation);
        this.bar.angle = this.center.angle - 90 ;

        //ボールの速度の取得
        let vel = (this.ball.body as Phaser.Physics.Arcade.Body).velocity

        //速度が落ちてきたら適当に設定する
        if ((Math.abs(vel.x) + Math.abs(vel.y)) < 10) {
            this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
            this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        }
    }

以上で自機の角度に合わせて動くバーができました。
今回は入力処理を書いていないので、処理の結果をわかりやすくするために自動的に自機をボールに向かせています。
次からは入力処理、自機にあたってしまった場合のダメージ処理、ブロックの配置を行っていきます。 f:id:Phaser_3:20181121161513g:plain

まとめ

  • 使用する物理エンジンはGameConfigで指定する


今回のサンプルコード全文は以下になります。


class MyScene1 extends Phaser.Scene {

    //物理オブジェクトの宣言
    public center: Phaser.Physics.Matter.Image;
    public bar: Phaser.Physics.Matter.Image;
    public ball: Phaser.Physics.Matter.Image;

    constructor() {
        super({ key: 'MyScene1', active: true });
    }

    preload() {
        //画像の読み込み
        this.load.image('ball', 'assets/images/ball.png');
        this.load.image('bar', 'assets/images/bar.png');
    }

    create() {

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

        //ワールドの重力を0に設定
        this.matter.world.disableGravity()

        //画面端に当たり判定を設定
        this.matter.world.setBounds(0, 0, this.sys.canvas.width, this.sys.canvas.height);

        //物理オブジェクトを作成
        this.ball = this.matter.add.image(
            this.sys.canvas.width/2,
            0,
            'ball', null,
            { shape: { type: 'circle', radius: 16 }, ignorePointer: true }
        );

        //ボールの反発係数、初期速度、摩擦係数を設定
        this.ball.setBounce(1.01);
        this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
        this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        this.ball.setFriction(0, 0, 0)

        //球を画面中央に配置
        this.center = this.matter.add.image(
            this.sys.canvas.width/2,
            this.sys.canvas.height/2,
            'ball', null,
            { shape: { type: 'circle', radius: 16}, ignorePointer: true }
        );
        this.center.setScale(2,2)

        //バーを球の周辺部に配置
        this.bar = this.matter.add.image(
            this.sys.canvas.width / 2,
            this.sys.canvas.height / 2 - 70,
            'bar'
        );
        //バーの反発係数を少し高くするとボールが勢いよく反射する
        this.bar.setBounce(1.2)
        this.bar.setFriction(0, 0, 0)

    }

    update() {
        //中心の球の向きをボールに合わせる
        this.center.setRotation(Math.atan2(this.ball.y - this.center.y, this.ball.x - this.center.x));

        //バーの位置と角度をボールの向きに合わせる
        this.bar.x = this.center.x + 70 * Math.cos(this.center.rotation);
        this.bar.y = this.center.y + 70 * Math.sin(this.center.rotation);
        this.bar.angle = this.center.angle - 90 ;

        //ボールの速度の取得
        let vel = (this.ball.body as Phaser.Physics.Arcade.Body).velocity

        //速度が落ちてきたら適当に設定する
        if ((Math.abs(vel.x) + Math.abs(vel.y)) < 10) {
            this.ball.setVelocityY(Phaser.Math.Between(-20, 20));
            this.ball.setVelocityX(Phaser.Math.Between(-20, 20));
        }
    }
}


var config: GameConfig = {
    parent: 'content',
    type: Phaser.AUTO,
    width: 600,
    height: 600,

    physics: {
        default: 'matter',
        matter: {
            enableSleeping: true
        }
    },

    scene: [MyScene1],

}

window.onload = () => {
    new Phaser.Game(config);
}

Sceneの取り扱い② Sceneの構造、Scene間の連携

今回はシーンの基本的な構造について紹介いたします。

preload, create, update

前回のシーン実行の記事では説明を省きましたが、 preload, create, updateはシーンの基本構造です。
これらを使って再生ボタンの周りを回転する円弧を作ってみましょう。

f:id:Phaser_3:20181120182032g:plain


    //別シーンからアクセスできるようにpublicにしておく
    public sprite: Phaser.GameObjects.Sprite;

    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
        super({ key: 'MyScene1', active: true });
    }

    preload() {
        //画像の読み込み
        this.load.image('play', 'assets/images/play.png');
    }

    create() {

        //読み込んだ画像を表示
        this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "play");

        //PhaserのGraphicsで円弧を描く
        let graphics = this.add.graphics();
        graphics.lineStyle(4, 0xff00ff, 1);
        graphics.beginPath();
        graphics.lineStyle(10, 0x000000)
        graphics.arc(400, 300, 200, Phaser.Math.DegToRad(90), Phaser.Math.DegToRad(180), true);
        graphics.strokePath();

        //描いた円弧をテクスチャにする
        graphics.generateTexture("arc")

        //spriteとして表示するのでgraphicsを非表示に
        graphics.visible = false

        //テクスチャにしたgraphicsをスプライトにする
        this.sprite = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "arc")

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

    }

    update() {
        //毎フレーム実行される
        this.sprite.angle += 1
    }


preload : アセットの読み込み、シーンの前準備

preloadはシーンの起動時に最初に実行される関数です。
一度だけ実行され、この関数が終了するまでcreateやupdateは実行されません。
サンプルコードではassets/images内にあるplay.pngを読み込んでいます。 (素材はCMANさんからいただきました)

  preload() {
        //画像の読み込み
        this.load.image('play', 'assets/images/play.png');
    }

上記実行時点ではなにも表示されていませんね。 f:id:Phaser_3:20181120152412p:plain

create : ゲームオブジェクトの作成に適したタイミング

createはpreloadのあとに実行されます。
preloadの処理が終了するまでは呼ばれないため、 preloadでアセットを読み込み、createで読み込んだアセットを用いてゲームオブジェクトを作成する、
といった使い方ができます。 サンプルコードではPhaserのグラフィックス機能を用いて作成した円弧をgeneratetextureを用いてテクスチャ化し、スプライトを作成してpreloadで読み込んだ画像とともに表示しています。

    create() {

        //読み込んだ画像を表示
        this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "play");

        //PhaserのGraphicsで円弧を描く
        let graphics = this.add.graphics();
        graphics.lineStyle(4, 0xff00ff, 1);
        graphics.beginPath();
        graphics.lineStyle(10, 0x000000)
        graphics.arc(400, 300, 200, Phaser.Math.DegToRad(90), Phaser.Math.DegToRad(180), true);
        graphics.strokePath();

        //描いた円弧をテクスチャにする
        graphics.generateTexture("arc")

        //spriteとして表示するのでgraphicsを非表示に
        graphics.visible = false

        //テクスチャにしたgraphicsをスプライトにする
        this.sprite = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "arc")

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

    }

カメラを白背景にし、スプライトを追加したため、画像と黒い円弧が見えるようになりました。 f:id:Phaser_3:20181120182615p:plain

update : 毎フレーム実行、いわゆるゲームループ部分

updateはシーンが実行されている間、毎フレーム呼ばれる関数です。
ここにゲームオブジェクトを操作するための処理を書きます。
サンプルコードでは円弧を回転させるために円弧スプライトのangleに毎フレーム1を加算しています。

    update() {
        //毎フレーム実行される
        this.sprite.angle += 1
    }

これで冒頭のgifのように回転する円弧ができました。 f:id:Phaser_3:20181120182032g:plain

scene.getで他のシーンの中身を取得する

今度はもう一つシーンを作ってシーン間の連携を試してみましょう。

scene.getでシーンを取得する

scene.getを使うと他のシーンの要素を参照できます。
以下はMyScene1で作った円弧のangleを画面に表示するシーンのサンプルコードです。

class MyScene2 extends Phaser.Scene {
    
    private scene1: MyScene1;
    private text:Phaser.GameObjects.Text

    constructor() {
        //!!自動実行をfalseにしておく!!
        super({ key: 'MyScene2', active: false });
    }

    create() {
        //MyScene1のspriteを参照したいため取得したシーンをMyScene1にキャスト
        //constructorで指定したキーでシーンを取得
        this.scene1 = this.scene.get("MyScene1") as MyScene1;

        //MyScene1で作成したの原点を取得、Y軸をすこしずらす
        this.text = this.add.text(this.scene1.sprite.x, this.scene1.sprite.y + 140, '').setFontSize(64).setColor('#000');
        this.text.setOrigin(0.5)
    }

    update() {
        //円弧のangleをリアルタイムに取得する
        this.text.setText(this.scene1.sprite.angle.toFixed(0).toString())
    }

}

create内の

        this.scene1 = this.scene.get("MyScene1") as MyScene1;

というコードでシーンを取得しています。
scene.get("取得したいシーンのキー")でシーンを取得することができます。 MyScene1内のspriteにアクセスしやすいようにscene1をMyScene1にキャストしましょう。

※MyScene2はactiveを必ずfalseにしてください。
自動実行をオフにしておかないと、参照元のシーンではまだpreloadが終了していないのにMyScene2が実行され、作成されていないオブジェクトを参照するなどして大抵の場合エラーがおきます。
今回のサンプルコードの場合、参照したいシーン=MyScene1の最終行にscene.launchを追加して安全にMyScene2を実行しましょう。

class MyScene1 extends Phaser.Scene {
~~
    create() {
       ~~
        //ここでMyScene2をlaunch
        this.scene.launch("MyScene2")
    }
~~

}



これでMyScene1の要素にアクセスできるようになったため、 MyScene2内のupdateではMyScene1の円弧の角度をリアルタイムに取得して画面に出力しています。

今回のサンプルではangleを取得しているだけですが、getしたシーンの関数を使用することもできます。 f:id:Phaser_3:20181120183327g:plain

以上、今回はシーンに基本的な構造と連携について勉強しました。
まとめると、

  • preloadでアセットの読み込み、createでゲームオブジェクトの作成、updateで毎フレームの処理
  • scene.getで他シーンの要素にアクセスできる
    というところが要点になるでしょう。

以下が今回のサンプルコードの全文です。


class MyScene1 extends Phaser.Scene {

    //別シーンからアクセスできるようにpublicにしておく
    public sprite: Phaser.GameObjects.Sprite;

    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
        super({ key: 'MyScene1', active: true });
    }

    preload() {
        //画像の読み込み
        this.load.image('play', 'assets/images/play.png');
    }

    create() {

        //読み込んだ画像を表示
        this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "play");

        //PhaserのGraphicsで円弧を描く
        let graphics = this.add.graphics();
        graphics.lineStyle(4, 0xff00ff, 1);
        graphics.beginPath();
        graphics.lineStyle(10, 0x000000)
        graphics.arc(400, 300, 200, Phaser.Math.DegToRad(90), Phaser.Math.DegToRad(180), true);
        graphics.strokePath();

        //描いた円弧をテクスチャにする
        graphics.generateTexture("arc")

        //spriteとして表示するのでgraphicsを非表示に
        graphics.visible = false

        //テクスチャにしたgraphicsをスプライトにする
        this.sprite = this.add.sprite(this.sys.canvas.width / 2, this.sys.canvas.height / 2, "arc")

        //背景を白に
        this.cameras.main.setBackgroundColor('#ffffff')

        //ここでMyScene2をlaunch
        this.scene.launch("MyScene2")
    }

    update() {
        //毎フレーム実行される
        this.sprite.angle += 1
    }
}

class MyScene2 extends Phaser.Scene {
    
    private scene1: MyScene1;
    private text:Phaser.GameObjects.Text

    constructor() {
        //自動実行をfalseにしておく
        super({ key: 'MyScene2', active: false });
    }

    create() {
        //MyScene1のspriteを参照したいため取得したシーンをMyScene1にキャスト
        //constructorで指定したキーでシーンを取得
        this.scene1 = this.scene.get("MyScene1") as MyScene1;

        //MyScene1で作成したの原点を取得、Y軸をすこしずらす
        this.text = this.add.text(this.scene1.sprite.x, this.scene1.sprite.y + 140, '').setFontSize(64).setColor('#000');
        this.text.setOrigin(0.5)
    }

    update() {
        //円弧のangleをリアルタイムに取得する
        this.text.setText(this.scene1.sprite.angle.toFixed(0).toString())
    }

}

var config: GameConfig = {
    parent: 'content',
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: [MyScene1, MyScene2],

}

window.onload = () => {
    new Phaser.Game(config);
}

Phaser 3 新機能 : Sceneの取り扱い① 遷移、並列実行

今回はPhaser 3で追加された新機能、Sceneを紹介いたします。
SceneはPhaserのコンポーネントを簡単に一括管理できるので非常に便利です。

前回の記事、 gpnotes.hatenablog.jp 内のサンプルコードでは「Phaser3」を出力するSceneを作っています。
今回はこのコードを基にSceneの登録、実行などの基本的な扱い方を勉強していきます。

シーンを作る、登録する

前回のサンプルコードを少し変更しました。
シーンを複数記述する場合、呼び出すためのキーを設定する必要があるため、 コンストラクタ内で設定を記述しています。

class MyScene1 extends Phaser.Scene {
    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
  //keyでシーンのキー、activeでシーンの自動実行を設定できます
        super({ key: 'MyScene1', active: true });
    }
    create() {
        let text = this.add.text(100, 100, 'Phaser 3').setFontSize(64).setColor('#ff0');
    }
}


クラス名をMyScene1に変更したので、GameConfig内のsceneの中身も変更します。

var config: GameConfig = {
    parent: 'content',
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: [MyScene1],

}

実行すると、前回と同じ"Phaser 3"が出力されました。 f:id:Phaser_3:20181119150707p:plain

start()で遷移、launchで並列実行

今度は自動実行をオフにしたMyScene2クラスを作り実行してみましょう。
※configへの追加も忘れずに!

class MyScene2 extends Phaser.Scene {
    constructor() {
        //自動実行をオフに
        super({ key: 'MyScene2', active: false });
    }
    create() {
   //文字が重ならないようにy座標を少しずらして色も変更
        let text = this.add.text(100, 300, 'Phaser 2').setFontSize(64).setColor('#00f');
    }
}

自動実行がオフになっているので、表示されるのはMyScene1の"Phaser 3"だけですね。 f:id:Phaser_3:20181119152310p:plain

この状態からSceneを実行するにはstartかlaunchを使います。 遷移(切り替えながらの実行)をしたければstartを、 呼び出し元のSceneと並行で実行したければlaunchを使います。

startによる遷移

まずはstartを試してみましょう。 MyScene1のcreate関数の中に

this.scene.launch("MyScene2")

を記述してみます。

class MyScene1 extends Phaser.Scene {
    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
        //keyでシーンのキー、activeでシーンの自動実行を設定できます
        super({ key: 'MyScene1', active: true });
    }
    create() {
        let text = this.add.text(100, 100, 'Phaser 3').setFontSize(64).setColor('#ff0');
        //MyScene2の実行
        this.scene.start("MyScene2")
    }
}

MyScene1から遷移でMyScene2を呼び出したので、MyScene1で作成された文字は見えなくなっています。 f:id:Phaser_3:20181119153653p:plain

launchによる並列実行

次はlaunchです。 今度は簡単に先ほどの関数のstartをlaunchに書き換えるだけです。

class MyScene1 extends Phaser.Scene {
    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
  //keyでシーンのキー、activeでシーンの自動実行を設定できます
        super({ key: 'MyScene1', active: true });
    }
    create() {
        let text = this.add.text(100, 100, 'Phaser 3').setFontSize(64).setColor('#ff0');
        //MyScene2の実行
        this.scene.launch("MyScene2")
    }
}

並列実行なのでMyScene1の文字とMyScene2の文字が同時に表示されています。 f:id:Phaser_3:20181119153939p:plain

総括、サンプルコード全文

以上、シーンの基礎の基礎として登録と実行の方法を紹介いたしました。
基礎の基礎なのでまとめるほどの内容でもないかもしれませんが、 実行で肝要なのは

  • コンストラクタでキーを設定
  • GameConfig内でシーンを登録しておく *startで遷移しながらの実行、Launchで並列実行

というところになると思います。
次回からはシーン間の値の受け渡し、活用方法などを紹介していきます。
今回のサンプルコードの全文は以下になります。

class MyScene1 extends Phaser.Scene {

    constructor() {
        //Sceneを拡張してクラスを作る際にコンストラクタでSceneの設定を渡します
        super({ key: 'MyScene1', active: true });
    }

    create() {
        let text = this.add.text(100, 100, 'Phaser 3').setFontSize(64).setColor('#ff0');
        this.scene.launch("MyScene2")
    }

    update() {
    }
}

class MyScene2 extends Phaser.Scene {

    constructor() {
        //自動実行をオフに
        super({ key: 'MyScene2', active: false });
    }

    create() {
        let text = this.add.text(100, 300, 'Phaser 3').setFontSize(64).setColor('#00f');
    }

}

var config: GameConfig = {
    parent: 'content',
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: [MyScene1, MyScene2],

}

window.onload = () => {
    new Phaser.Game(config);
}

Phaser 2からの変更点

Phaser 3はPhaser 2とは別物と言ってもいいくらいに多くの点が変化しました。 変更点に関してはPhaser World Issue 116に詳しいことが書いてあります。

madmimi.com

個人的に気になった変更点

その中から個人的に気になった変更点をいくつかピックアップしたいと思います。

ディスプレイツリー構造が廃止になった

Phaser 2ではあるオブジェクトに別のオブジェクトを子要素として追加し、全体としてはツリーの構造にして一つの表示物を作るのが一般的でした。
Phaser 3ではそのようなことができなくなっています。ゲームオブジェクトが他のゲームオブジェクトを子要素として含むことはできず、全てのゲームオブジェクトがフラットになるようです。
この変更はかなり影響があるのではないでしょうか。Containerを使えばディスプレイツリーと同じようなことはできますが、Phaser 2の構造をそのままPhaser 3には移植できないと思います。

シーンの導入

Phaser 2にあったステートという仕組みの代わりに、シーンという仕組みが導入されています。
Phaser 2を使っていた頃にはステートをあまり使っていなかったのでどの程度変わったのかは分かりませんが、Phaser 3ではシーンを理解して使いこなすことが鍵になりそうです。

デフォルトで中央揃えになった

ゲームオブジェクトの基準となる位置は、Phaser 2では左上でした。Phaser 3では中央に変更されています。
そして基準となる位置を設定していたanchorプロパティが廃止になり、setOrigin(x, y)で設定するようになりました。

setDepth

ゲームオブジェクトの描画順を制御する機能として、setDepth()が導入されました。
Phaser 2の頃は描画順の制御が面倒だった記憶があるので、これは素直にありがたいです。

最後に

個人的に気になった変更点を挙げましたが、フラットディスプレイリストとシーンに関しては全てのPhaser 3プログラマーが気にしなくてはいけない点だと感じました。
それ以外にも多くの変更点があるので、Phaser 2に熟練したプログラマーでもPhaser 3は基礎から勉強する必要があると思います。
私も引き続き調査をしていきたいと思います。

Phaser 3 リンク集

Phaser 3の情報も徐々に多くなってきました。今日はその中でも特に役に立ちそうなページを紹介したいと思います。

 

Making your first Phaser 3 game: Part 1 - Introduction - Learn - Phaser

公式サイトのチュートリアルです。使用している言語はJavaScriptですが、これを読めばPhaserの基本的な使い方が理解できるでしょう。Part 4からはアクションゲームの作り方になるので、興味がなければ後回しでもいいと思います。

 

Getting Started with Phaser 3: Part 1 - Introduction - Learn - Phaser

こちらも公式のチュートリアルになっています。こちらの方はエディタやサーバーなど、環境構築が主な内容になっています。どんなエディタを使ったらいいのか迷っている人は読んでおいた方がいいでしょう。

 

https://photonstorm.github.io/phaser3-docs/

Phaser 3のドキュメントです。検索の使い勝手は現状あまりいいとは言えませんが、内容は充実しています。Phaser 3でバリバリコードを書くようになったら必須になってくると思います。

 

Phaser 3 Examples

Phaser 3のサンプルを集めたサイトです。こちらも使い勝手はあまり良くありませんが、サンプルの数はかなり充実しています。Phaser 2の時代にもサンプルの数はかなり多く、サンプルコードを見ることが何よりも勉強になったので、今回もきっと役に立つでしょう。

 

Phaser - Community - Phaser World

Phaserが発行しているニュースレターを読むことができます。Phaser 3に関する最新の情報を入手したり、新機能のチュートリアルを読むことができます。

 

www.html5gamedevs.com

HTML 5 Game Devsというサイトです。Phaserの実質的な公式フォーラムになっています。何かわからないことがあったらこのサイトで検索してみることをおすすめします。

 

Phaser 3はできたてホヤホヤのライブラリで、日々進化しています。ここに書いたサイトも情報が古くなったり移転したりすることがあるかもしれません。

また、他に役に立ちそうなサイトをご存知の方がいましたらぜひ教えて下さい。