【Phaser3でSvelteゲームアプリを作る③】シューティングゲームの土台となる最初のプロジェクトを作成


※ 当ページには【広告/PR】を含む場合があります。
2024/07/28
【Phaser.jsでSvelteゲームアプリを作る②】Phaser3のローカル保存したアセットファイルを読み込む
合同会社タコスキングダム|TacosKingdom,LLC.

前回までで、Scratchゲームで使っていたシューティングゲームの画像をPhaserへインポートするところまで解説していました。

合同会社タコスキングダム|タコキンのPスクール
【Phaser.jsでSvelteゲームアプリを作る②】Phaser3のローカル保存したアセットファイルを読み込む

Phaser.js+Svelteゲームアプリ作成②〜Phaser3でローカルに保存した画像ファイルを読み込む

本記事からよりシューティングゲームに近い形で段階的に仕上げていきます。

今回はScratch版のゲームメイキングの記事と比較しやすいように以下の記事相当の内容で、Phaserゲームに移植していきます。

合同会社タコスキングダム|タコキンのPスクール
Scratchから始めるシューティングゲームの作り方①〜最初のプロジェクトを作成

シューティングゲーム作成を通じてScratchでプログラミングを学習していきましょう。

なお、このPhaserゲーム作成の大きな目的は、Javascriptのプログラミング学習を掲げていますが、純粋なJavascript(ES6)の使い方は詳しく解説していません。

必要であれば、このブログ等でちょこちょこ紹介している「HTML/CSS/Javascriptの学習方法」のページを参考にしてみてください。

なお、本記事の最後までコードを組んでいただくと、以下のようなアプリが動きます。


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

プレーヤーと敵と弾を配置する

まずは前回説明していた「画像を読み込むシーン」からコードを拡張するところから始めます。

前回に引き続き、プロジェクトの
scenesフォルダにシーンコードの名前をshooting-1.jsとして新規追加し、preloadメソッドにシーンに必要なアセット画像を読み込ませておきます。

            
            import Phaser from "phaser";

export default class shooting1Scene extends Phaser.Scene {
    constructor() {
        super({ key: 'shooting1' });
    }

    preload() {
        this.load.setBaseURL("src/assets");
        this.load.image('cat', 'cat.svg');
        this.load.image('ball', 'ball.svg');
        this.load.image('enemy', 'enemy1.png');
    }

}
        
なお、プレイヤーと弾と敵の画像は以下のようなものを指定しています。

合同会社タコスキングダム|タコキンのPスクール

この時点では画像を読み込んでいるだけですので、このシーン自体は何も起こりません。

アプリ本体のコードにこのシーンコードを登録しておきます。

            
            <script>
import Phaser from "phaser";
//👇追加
import shooting1Scene from "./scenes/shooting-1.js";

const game = new Phaser.Game({
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 0 },
            debug: true
        }
    },
    input: { keyboard: true },
    scene: [shooting1Scene], //👈シーンを追加
});
</script>
        

JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

キーボードからプレーヤーを動かす

まずはプレーヤーである猫をキーボードの矢印キーで動かせるようにしてみましょう。

先程の
shooting1Sceneクラスで、createupdateを以下のように追加します。

            
            //...中略

create () {
    //👇'cat'の画像を(400, 300)にスプライトとして配置する(=デフォルトの配置点は画像の中央)
    this._cat = this.add.sprite(400, 300, 'cat');
    //👇画像を60px四方にリサイズ
    this._cat.setDisplaySize(60, 60);
    //👇スプライトをシーンに表示
    this.physics.add.existing(this._cat, false);
    //👇スプライトがシーンの画面枠外に出ないようにする
    this._cat.body.setCollideWorldBounds(true);

    //👇特定のキー(矢印キーを含む)のイベントを有効にする
    this.cursors = this.input.keyboard.createCursorKeys();
}

update () {
    //👇キーが押されていない場合、スプライトの動きを止める
    this._cat.body.setVelocity(0);

    //👇↑キーが押されたら、上に移動
    if (this.cursors.up.isDown) {
        this._cat.body.setVelocityY(-300);
    }
    //👇↓キーが押されたら、下に移動
    else if (this.cursors.down.isDown) {
        this._cat.body.setVelocityY(300);
    }

    //👇←キーが押されたら、左に移動
    if (this.cursors.left.isDown) {
        this._cat.body.setVelocityX(-300);
    }
    //👇→キーが押されたら、右に移動
    else if (this.cursors.right.isDown) {
        this._cat.body.setVelocityX(300);
    }
}
        
ここらへんからPhaserらしいプログラミングが始まります。

「Phaserらしい」というのは、膨大なPhaserのクラスやメソッドの中から、自分のやりたい・作りたい機能を見つける作業を繰り返すようなプログラミングスタイルを指します。

そこにテクニックは不要で、いかにオンラインマニュアルとにらめっこして、その引き出しから取り出したパーツを配置するかという作業になります。

例えば、シーンにスプライトを追加したい場合を考えると、

            
            this._cat = this.add.sprite(400, 300, 'cat');
        
の箇所にあたります。

クラス内の
thisPhase.Sceneクラスのインスタンス自身を指し、シーン内にGameObject全般を追加するためのaddプロパティが見つかります。

この
addプロパティは、Phaser.GameObjects.GameObjectFactoryクラスのインスタンスであり、シーンにさまざまなGameObjectを登録することが出来ます。

この内、スプライトを追加するのは
spriteメソッドになります。

spriteメソッドでシーンに登録して返されたthis._catPhaser.GameObjects.Spriteになり、後は上のコードで説明したように、開発者が自由にコーディングすることが出来ます。

リファレンスを見ての通り、分厚い辞書で文字を引いている気になるように、かなりの数のクラスプロパティ・メソッドがあるため、全てを解説は出来ませんが、ここで使っているものだけで以下のようなものがあります。

            
            this._cat.setDisplaySize(60, 60);
this._cat.body.setCollideWorldBounds(true);
this._cat.body.setVelocity(0);
        
と言うことで、this.addからスプライトをシーンに登録するところまで解説しました。

シーン内のGameObjectに動きを与えるための
this.physicsや、デバイスからの入力を制御するthis.inputについても同様の考え方で利用することができます。

詳しい使い方は、ご自分でリファレンスを眺めてみてください。

参考|this.physics - Phaser.Physics.Arcade.ArcadePhysics

参考|this.input - Phaser.Input.InputPlugin


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

プレーヤーから弾を発射させる

次に猫から弾が発射できるような機能を盛り込みます。

ここでも
this.physicsthis.inputの機能を使って、弾の発射を実装することになります。

            
            //...中略
create () {

    //...中略

    //👇弾のスプライトに物理演算できるようにthis.physicsに登録する
    this._ball = this.physics.add.sprite(this._cat.x, this._cat.y, 'ball');
    //👇一旦弾のスプライトは非表示にしておく
    this._ball.disableBody(true, true);

    //...中略

    //👇キーボードのAキー+Downイベントとして弾を発射させる
    this.input.keyboard.on('keydown-A', event => {
        //👇イベント発生時に弾のスプライトを表示
        this._ball.enableBody(true, this._cat.x + 30, this._cat.y, true, true);
        //👇弾に物理的な動きを設定
        this.physics.velocityFromRotation(0, 600, this._ball.body.velocity);
    });
    //👇キーボードのAキーのイベントを有効化
    this.input.keyboard.addCapture('A');
}
        

JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

敵に動きをつける

次に敵をシーンに表示させて、画面左へ流れ込んでくるように動き(「物理演算:Physics」)を設定してみましょう。

            
            //...中略
create () {

    //...中略

    //👇敵スプライトを物理演算対象に登録して、画像サイズを0.3倍に設定
    this._enemy = this.physics.add.sprite(0, 0, 'enemy').setScale(0.3);
    //👇敵スプライトを配置/Y座標はランダム
    this._enemy.setPosition(800, Phaser.Math.Between(50, 550));
    //👇敵スプライトに動きを設定
    this._enemy.setVelocityX(-60);

    //...中略
}

update () {

    //...中略

    //👇敵が画面左端に着いたら、右端から動きを繰り返す
    if (this._enemy.getBounds().right < 0) {
        //👇敵スプライトを一旦非表示にする
        this._enemy.disableBody(true, true);
        //👇敵スプライトを右端に戻して表示する
        this._enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
        //👇敵スプライトに動きを設定
        this._enemy.setVelocityX(-60);
    }
}
        

JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

当たり判定①〜敵が弾をあたると消える

プレーヤーが打った弾に当たると敵が消滅し、別の位置から敵が発生するようにしてみます。

            
            //...中略
create () {

    //...中略

    //👇弾と敵が接触した際の処理を追加
    this.physics.add.overlap(this._ball, this._enemy, () => {
        //👇弾スプライトを非表示にする
        this._ball.disableBody(true, true);

        this._enemy.disableBody(true, true);
        this._enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
        this._enemy.setVelocityX(-60);
    }, null, this);
}
        


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

プレーヤーのライフを設定する

次に、プレーヤーが敵に当たると、プレーヤーがダメージを受けて、ライフ(残機数)が減るようにしてみます。

まずはプレーヤーのライフを別レイヤーで管理できるようなシーンを作成します。

scenesフォルダに新しくlives.jsというシーンコードを追加します。

この
lives.jsの中身には以下のような内容にします。

            
            import Phaser from "phaser";

export default class livesScene extends Phaser.Scene {
    constructor () {
        super({ key: 'lives' });
    }

    //👇シーンの変数を外部から初期化
    init (data) {
        this.lives = data.life;
    }

    preload (){
        this.load.setBaseURL("src/assets");
        this.load.image('life', 'life.svg');
    }

    create () {
        //👇ハートのスプライトを連続複写かつ物理演算にグループとして登録
        this._lifeIcon = this.physics.add.group(
            {
                key: 'life',
                repeat: this.lives - 1,
                setScale: {x: 0.2, y: 0.2},
                setXY: { x: 50, y: 50, stepX: 40 }
            }
        );

        //👇メインのゲームシーンを取得する
        const _mainGame = this.scene.get('shooting1');

        //👇「damaged」イベントを受信したときの処理を記述
        _mainGame.events.on('damaged', () => {
            this.lives--;
            if (this.lives>= 0) {
                //👇スプライトグループ内で対象のハートスプライトだけ非表示
                this._lifeIcon.children.entries[this.lives].disableBody(true, true);
            }
        }, this);
    }
}
        
このシーンをメインアプリに追加します。

            
            <script>
import Phaser from "phaser";
import shooting1Scene from "./scenes/shooting-1.js";
//👇追加
import livesScene from "./scenes/lives.js";

const game = new Phaser.Game({
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 0 },
            debug: true
        }
    },
    input: { keyboard: true },
    scene: [
        shooting1Scene,
        livesScene, //👈シーンを追加
    ],
});

//👇SceneManagerからのライフ表示シーンの初期化
game.scene.start('liveView', { life: 3 });
</script>
        
ここでは、新しく2点ほどPhaserプログラミングの使いこなしで重要となるテクニックがありました。

まずは、シーンクラス内で作成した
initメソッドの使い方です。

            
            init (data) {
    this.lives = data.life;
}
        
これはシーンの使う変数を外部から初期化する際に不可欠なテクニックで、initメソッドの第一引数(ここではdataの部分)から任意のデータを受けることができます。

初期化するデータを送るには、例えば以下のようにゲーム本体のシーンマネジャーから設定することができます。

            
            game.scene.start('liveView', { life: 3 });
        
もう一つは、シーンのthis.sceneの扱いです。

this.scene「シーンプラグイン(Phaser.Scenes.ScenePlugin)」であり、シーンの中からゲーム本体のシーンマネジャーを呼び出したいときに使うエイリアスになっています。

例えば、ライフ表示シーン(
liveView)の中から、ゲームのメインシーンにアクセスしたい場合には、

            
            const _mainGame = this.scene.get('shooting1');
        
とすることで、別のシーンと連帯することができます。

EventEmitterでシーン間相互イベントを実装する

ゲームプログラミングにおいて、複数の異なるシーンを連携させるうえで、「イベント」は必須のテクニックです。

Phaserではシーンごとに
イベントエミッター(Phaser.Events.EventEmitter)が存在し、<シーン>.eventsで利用することができます。

例えばここでは、ライフ表示シーンからメインシーンのイベント(
damaged)を受信した際の処理を、

            
            _mainGame.events.on('damaged', () => {
    this.lives--;
    if (this.lives>= 0) {
        this._lifeIcon.children.entries[this.lives].disableBody(true, true);
    }
}, this);
        
としてコールバックを登録しています。

他方、メインシーンからのイベントの送信は、メインシーンの中のイベントエミッタから、

            
            this.events.emit('damaged');
        
と利用します。


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

当たり判定②〜プレーヤーが敵にあたるとダメージを受ける

ではライフが敵に当たることで1つ減って、ゼロになった時点でゲームを終了するようにしてみましょう。

            
            //...中略

export default class shooting1Scene extends Phaser.Scene {
    //👇プレーヤーの初期ライフ数を設定
    catlife = 3;

    create () {

        //...中略

        //👇プレーヤーと敵とのコライダー(衝突オブジェクト)を設置
        this.physics.add.collider(this._cat, this._enemy, this.hitEnemy, null, this);

        //...中略
    }

    //...中略

    //👇新しいクラスメソッドを追加する
    hitEnemy(player, enemy) {
        //👇シーンイベント・「damaged」を発行
        this.events.emit('damaged');

        //👇残機が2以上の場合の処理
        if (this.catlife > 1) {
            enemy.disableBody(true, true);
            enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
            enemy.setVelocityX(-60);
            //👇プレーヤーのライフを1つ減らす
            this.catlife--;
        }
        //👇ゲームオーバー時の処理
        else {
            //👇すべての物理演算を停止(=ゲームの中断)
            this.physics.pause();
            //👇プレーヤーの色を赤に変更
            player.setTint(0xff0000);
        }
    }

//...以下略
        


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

JavaScriptとCanvasでアニメーションとゲーム制作!~描き、動かし、操作する~

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

まとめ

以上、今回はこれから数回に渡りカスタマイズしていく「シューティングゲームの土台」のプロジェクトを作成してみました。

最後まで一通りコーディングできたなら、だいぶPhaser3のゲームプログラミングの作法が理解できたかと思います。

次回以降で、もっとシューティングゲームらしい機能を付け加えたり、修正したりと、より細かいポイントを解説していく予定です。

付録〜ここまでで作成したコードの完成版

これまで小出しに説明してきたコードの全体を以下にまとめておきます。

参考にしてください。

App.svelte

Svelteアプリ本体になるブートコンポーネント。

            
            <script>
import Phaser from "phaser";
import shooting1Scene from "./scenes/shooting-1.js";
import livesScene from "./scenes/lives.js";

const game = new Phaser.Game({
    type: Phaser.CANVAS,
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 0 },
            debug: true
        }
    },
    //👇ゲーム画面サイズが自動調整されるおまじない
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    input: { keyboard: true },
    scene: [shooting1Scene, livesScene],
});

game.scene.start('liveView', { life: 3 });
</script>
        

scenes/shooting-1.js

シューティングゲームのメインシーン。

            
            import Phaser from "phaser";

export default class shooting1Scene extends Phaser.Scene {
    catlife = 3;
    constructor () {
        super({ key: 'shooting1' });
    }

    preload (){
        this.load.setBaseURL('src/assets');
        this.load.image('cat', 'cat.svg');
        this.load.image('ball', 'ball.svg');
        this.load.image('enemy', 'enemy1.png');
    }

    create () {
        this._cat = this.add.sprite(400, 300, 'cat');
        this._cat.setDisplaySize(60, 60);
        this.physics.add.existing(this._cat, false);
        this._cat.body.setCollideWorldBounds(true);

        this.cursors = this.input.keyboard.createCursorKeys();

        this._ball = this.physics.add.sprite(this._cat.x, this._cat.y, 'ball');
        this._ball.disableBody(true, true);

        this._enemy = this.physics.add.sprite(0, 0, 'enemy').setScale(0.3);
        this._enemy.setPosition(800, Phaser.Math.Between(50, 550));
        this._enemy.setVelocityX(-60);

        this.input.keyboard.on('keydown-A', event => {
            this._ball.enableBody(true, this._cat.x + 30, this._cat.y, true, true);
            this.physics.velocityFromRotation(0, 600, this._ball.body.velocity);
        });
        this.input.keyboard.addCapture('A');

        this.physics.add.overlap(this._ball, this._enemy, () => {
            this._ball.disableBody(true, true);
            this._enemy.disableBody(true, true);
            this._enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
            this._enemy.setVelocityX(-60);
        }, null, this);

        this.physics.add.collider(this._cat, this._enemy, this.hitEnemy, null, this);
    }

    update () {
        this._cat.body.setVelocity(0);

        if (this.cursors.up.isDown) {
            this._cat.body.setVelocityY(-300);
        }
        else if (this.cursors.down.isDown) {
            this._cat.body.setVelocityY(300);
        }

        if (this.cursors.left.isDown) {
            this._cat.body.setVelocityX(-300);
        }
        else if (this.cursors.right.isDown) {
            this._cat.body.setVelocityX(300);
        }

        if (this._enemy.getBounds().right < 0) {
            this._enemy.disableBody(true, true);
            this._enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
            this._enemy.setVelocityX(-60);
        }
    }

    hitEnemy (player, enemy) {
        this.events.emit('damaged');

        if (this.catlife > 1) {
            enemy.disableBody(true, true);
            enemy.enableBody(true, 800, Phaser.Math.Between(30, 550), true, true);
            enemy.setVelocityX(-60);
            this.catlife--;
        } else {
            this.physics.pause();
            player.setTint(0xff0000);
        }
    }
}
        

scenes/lives.js

プレーヤーのライフ表示を管理するシーン。

            
            import Phaser from "phaser";

export default class liveViewScene extends Phaser.Scene {
    constructor () {
        super({ key: 'liveView' });
    }

    init (data) {
        this.lives = data.life;
    }

    preload (){
        this.load.setBaseURL("src/assets");
        this.load.image('life', 'life.svg');
    }

    create () {
        this._lifeIcon = this.physics.add.group(
            {
                key: 'life',
                repeat: this.lives - 1,
                setScale: {x: 0.2, y: 0.2},
                setXY: { x: 50, y: 50, stepX: 40 }
            }
        );

        const _mainGame = this.scene.get('shooting1');

        _mainGame.events.on('damaged', () => {
            this.lives--;
            if (this.lives >= 0) {
                this._lifeIcon.children.entries[this.lives].disableBody(true, true);
            }
        }, this);
    }
}