📖

phaserでゲーム開発 第三回

に公開

phaserでゲーム開発シリーズ、第三回。
今回は画面に足場を配置してみたいと思います。
前回の記事はこちら。
https://zenn.dev/k_tabuchi/articles/521622aefc1151

今回はプレイヤーを配置してみます。

まずは、今回のコードの全文です。

全体像

GameScene
/**
 * ゲームのメインシーンを表すクラス
 * 
 * @extends Phaser.Scene
 */
export class GameScene extends Phaser.Scene {

    /** レイヤー */
    #layer;

    /** 移動速度 */
    #runSpeed = 100;

    /** ジャンプ力 */
    #jumpSpeed = 300;

    /** プレイヤー */
    #player;

    /** 入力キー */
    #cursors;



    /**
     * シーンのコンストラクタ
     * - シーンキーを "GameScene" として登録する
     */
    constructor() {
        super("GameScene");
    }

    /**
     * アセットの事前読み込みを行う
     * - 画像や音声などをロードする
     * 
     * @override
     */
    preload() {
        this.load.setBaseURL('/assets/');

        this.load.image('sky', 'img/bg7.jpg');

        this.load.spritesheet('ground', 'img/map_chip/base.png', {
            frameWidth: 16,
            frameHeight: 16
        });

        this.load.spritesheet('player', 'img/character/pipo-charachip001.png', {
            frameWidth: 32,
            frameHeight: 32,
        });
    }

    /**
     * シーン初期化時に呼ばれる
     * 
     * @override
     */
    create() {
        this.createBackground();
        this.createPlatforms();
        this.createPlayer();
    }

    /**
     * 背景を作成してゲーム全体に配置する
     */
    createBackground() {
        const bg = this.add.image(0, 0, 'sky').setOrigin(0, 0);
        bg.setDisplaySize(this.sys.game.config.width, this.sys.game.config.height);
    }

    /**
     * 足場(タイルマップ)を作成して衝突判定を設定する
     */
    createPlatforms() {
        const mapData = [];

        // 1~36行目までは空白
        for (let y = 0; y < 36; y++) {
            mapData[y] = new Array(50).fill(-1);
        }

        // 足場ブロックを配置
        mapData[2].fill(368, 0, 50);
        mapData[15].fill(368, 10, 30);
        mapData[22].fill(368, 20, 30);
        mapData[22].fill(368, 40, 70);
        mapData[30].fill(368, 20, 50);

        // 最下段(36行目)は全面にブロックを敷く
        mapData[36] = new Array(50).fill(368);

        // タイルマップを生成
        const map = this.make.tilemap({
            data: mapData,
            tileWidth: 16,
            tileHeight: 16
        });

        const tiles = map.addTilesetImage('ground');
        this.#layer = map.createLayer(0, tiles, 0, 0);

        // -1 以外を衝突対象に設定
        this.#layer.setCollisionByExclusion([-1]);
    }

    /**
     * プレイヤーキャラクターを生成し、物理演算や入力処理を設定する
     *
     * - 矢印キー入力を取得
     * - プレイヤースプライトを物理演算付きで生成
     * - 当たり判定サイズを調整(足元のみ反応するように設定)
     * - タイルマップとの衝突処理を登録
     *
     * @private
     */
    createPlayer() {
        /** 矢印キー入力オブジェクト */
        this.#cursors = this.input.keyboard.createCursorKeys();

        /** プレイヤースプライトを生成(初期位置は X:40, Y:400) */
        this.#player = this.physics.add.sprite(40, 400, 'player', 0)
            .setCollideWorldBounds(true); // 画面外に出ないよう制約

        // 当たり判定サイズを調整(キャラ画像32x32に対して足元16x16だけ反応させる)
        this.#player.body.setSize(16, 16).setOffset(8, 16);

        // プレイヤーと足場レイヤーの衝突を有効化
        this.physics.add.collider(this.#player, this.#layer);
    }

    /**
     * 毎フレーム呼ばれる更新処理
     * - プレイヤー操作や物理演算などを記述する
     * 
     * @param {number} [time] 経過時間(ミリ秒)
     * @param {number} [delta] 前フレームからの経過時間(ミリ秒)
     * @override
     */
    update(time, delta) {
        // プレイヤーの下が「床(タイルなどの静的地形)」または「他の物体」に接しているかどうか
        const onFloor = this.#player.body.blocked.down || this.#player.body.touching.down;

        // 左右移動
        if (this.#cursors.left.isDown) {
            this.#player.setVelocityX(-this.#runSpeed);
        } else if (this.#cursors.right.isDown) {
            this.#player.setVelocityX(this.#runSpeed);
        } else {
            this.#player.setVelocityX(0);
        }

        // ジャンプ
        if (this.#cursors.up.isDown && onFloor) {
            this.#player.setVelocityY(-this.#jumpSpeed);
        }
    }
}

本題に入る前に、ここの書き方は少し前回から変更しました。
これは、フィールド変数を定義しています。
javascriptは動的にフィールド変数を追加できるのでべつにわざわざ定義しなくても、いきなりthis.layerのように書いても動きます。
ただ、それだとphaserの提供するフィールドを使っているのか自分で定義したもんか見分けがつかなくなるため、あえて明示的に宣言しています。
また、変数名の頭にある「#」はprivate変数であることを表しています。

抜粋
export class GameScene extends Phaser.Scene {

    /** レイヤー */
    #layer;

    /** 移動速度 */
    #runSpeed = 100;

    /** ジャンプ力 */
    #jumpSpeed = 300;

    /** プレイヤー */
    #player;

    /** 入力キー */
    #cursors;

preload

では本題ですが、まずpreloadに追加されたこれ。
これは画像を読み込んで、画像の分割単位をしています。
これは前回やりましたね。

抜粋
        this.load.spritesheet('player', 'img/character/pipo-charachip001.png', {
            frameWidth: 32,
            frameHeight: 32,
        });

キャラクター画像はこんなものを使っています。

1つの絵が32pxの正方形でそれが並んでいるわけですね。
次回のアニメーションで生きてきます。

createPlayerメソッド (createから切り出し)

次はこれです。
以下の createPlayer() メソッドでは、キーボード入力の取得・プレイヤースプライトの生成・当たり判定の調整・足場との衝突処理をまとめています。

抜粋
    /**
     * プレイヤーキャラクターを生成し、物理演算や入力処理を設定する
     *
     * - 矢印キー入力を取得
     * - プレイヤースプライトを物理演算付きで生成
     * - 当たり判定サイズを調整(足元のみ反応するように設定)
     * - タイルマップとの衝突処理を登録
     *
     * @private
     */
    createPlayer() {
        /** 矢印キー入力オブジェクト */
        this.#cursors = this.input.keyboard.createCursorKeys();

        /** プレイヤースプライトを生成(初期位置は X:40, Y:400) */
        this.#player = this.physics.add.sprite(40, 400, 'player', 0)
            .setCollideWorldBounds(true); // 画面外に出ないよう制約

        // 当たり判定サイズを調整(キャラ画像32x32に対して足元16x16だけ反応させる)
        this.#player.body.setSize(16, 16).setOffset(8, 16);

        // プレイヤーと足場レイヤーの衝突を有効化
        this.physics.add.collider(this.#player, this.#layer);
    }

キー入力の取得
this.input.keyboard.createCursorKeys() を呼び出すことで、矢印キー(←↑→↓)をまとめて管理するオブジェクトを生成します。
このオブジェクトは後で update() メソッドの中で移動やジャンプ処理に利用します。

プレイヤースプライトの生成
this.physics.add.sprite() を使い、物理演算に対応したスプライトを生成します。

引数の (40, 400, 'player', 0) はそれぞれ X座標・Y座標・テクスチャキー・フレーム番号 を意味します。

.setCollideWorldBounds(true) によって、画面外に出ないよう制約をかけています。

当たり判定の調整
プレイヤー画像は 32x32 ピクセルですが、実際に「床に接地する部分」はキャラの足元だけにしたいケースが多いです。

.setSize(16, 16) で判定範囲を 16x16 に縮小

.setOffset(8, 16) で画像の下半分に判定を寄せる
これによって「頭がブロックにめり込んでもジャンプ可能」といった自然な挙動を実現できます。

足場との衝突設定
this.physics.add.collider(this.#player, this.#layer) によって、プレイヤーと足場タイルの衝突を有効にしています。
これを設定しないと、プレイヤーは床をすり抜けてしまいます。

updateメソッド

次はこれです。

Phaser では update() メソッドが毎フレーム呼び出され、キャラクターの操作や物理演算を反映します。
ここではプレイヤーの左右移動とジャンプを処理しています。

抜粋
    update(time, delta) {
        // プレイヤーの下が「床(タイルなどの静的地形)」または「他の物体」に接しているかどうか
        const onFloor = this.#player.body.blocked.down || this.#player.body.touching.down;

        // 左右移動
        if (this.#cursors.left.isDown) {
            this.#player.setVelocityX(-this.#runSpeed);
        } else if (this.#cursors.right.isDown) {
            this.#player.setVelocityX(this.#runSpeed);
        } else {
            this.#player.setVelocityX(0);
        }

        // ジャンプ
        if (this.#cursors.up.isDown && onFloor) {
            this.#player.setVelocityY(-this.#jumpSpeed);
        }
    }

接地判定
this.#player.body.blocked.down または this.#player.body.touching.down を確認し、
プレイヤーが床やオブジェクトに接地しているかどうかを判定します。

blocked.down … タイルマップなど静的な床に接している状態

touching.down … 他の動くオブジェクトに接している状態
どちらかが true なら「地面の上にいる」とみなします。

左右移動

←キーが押されていれば setVelocityX(-this.#runSpeed) で左方向へ移動

→キーが押されていれば setVelocityX(this.#runSpeed) で右方向へ移動

どちらも押されていなければ setVelocityX(0) で停止
ここでは #runSpeed を使い、速度を数値で制御しています。

ジャンプ処理

↑キーが押されていて、かつ「接地している場合のみ」ジャンプ可能

setVelocityY(-this.#jumpSpeed) で上方向へ初速を与えます
これにより「空中での多段ジャンプを防ぐ」ことができます。

現状

これでプレイヤーが登場して、キーボードで動かせるようになりました。
一気にゲームっぽくなりましたね!

株式会社ONE WEDGE

【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
https://onewedge.co.jp

Discussion