🤩

Phaser3 Arcade物理エンジンの基礎

2024/05/28に公開

主な改定履歴

  • 2024/08/20 ソースも含め一部微修正
  • 2024/05/28 新規公開

はじめに

この記事では、Phaser3内蔵の物理エンジンArcadeを利用し初歩的なゲームを作成します。
物理エンジンを使ったコードの書き方と、その流れが理解しやすいようなるべく細かく見ていきます。

この記事の前提条件

  • Phaser3の基礎的な扱い方を知っていること。
  • TypeScriptが分かること。

作成するゲームの内容

  • 矢印キーでプレイヤーを操作できます。
  • プレイヤーが黄色い星に接触すると星が消えます。
  • 星が画面上からすべて消えた時、プレイヤーが赤いボタンに接触すると星が降ってきます。
  • プレイヤーがグレーの障害物に接触するとダメージを受けます(演出のみ)。

この記事のソースはCodePenで確認できます。

物理エンジンの種類

Phaser3で扱える物理エンジンはArcadeMatterの2種類があります。

  • ArcadePhaser3の標準的な物理エンジンです。
    Arcadeは加速や回転など物理エンジンの標準的な機能が使用でき、動作が軽量でかつ高速なのが特徴。
    ただし、使用できるボディ(物理演算に使用するオブジェクト)には制限があります。

  • Matterはサードパーティの物理エンジンライブラリです。より複雑な図形やリアルな物理法則が再現できる一方、処理の負荷がArcadeより大きいです。

当記事ではArcadeを中心に解説します。

ボディについて

ボディとは物理エンジンをつかって物理演算を行う際に使用するオブジェクトをいいます。
ボディはゲームオブジェクトと同じように形状を持っており、
ゲームオブジェクトの形状とは異なる形状を持たせることができます。
(つまり、ゲームオブジェクトの形状とは一致するとは限りません。)
Arcadeにおいて適用できるボディの形状は四角形と正円のみになります。

ボディには動的ボディと、静的ボディの2種類があります。
動的ボディは、画面上を動かすことができ、重力や衝突の影響を受けて移動の方向や速度が自動で計算されます。
一方、静的なボディは画面上に固定され、ゲーム内では壁や床のような役割を担います。

グループについて

同一のゲームオブジェクトが複数ある場合、個別で管理するよりまとめておいたほうが何かと都合がいいです。
Phaser3ではGroupという仕組みが用意されており、これを使用すると以下のメリットがあります。

  • 複数ある同一のゲームオブジェクトを一元的に管理できる(プロパティの一括適用など)。
  • ゲームオブジェクトの再利用が容易になる。
    (一般にオブジェクトの破棄・再作成はメモリ割り当て処理の負荷がかかるため、再利用によりそれらを回避できます。
    参考サイト

このゲームではブロックと星のゲームオブジェクトをそれぞれのグループでまとめて管理します。

ゲーム作成の全体の流れ

  1. GameConfigを設定します。この中で物理エンジンarcadeを有効にします。
  2. ゲームで必要なテクスチャ(プレイヤー、星、ブロック、障害物)を作成します。
    (このサンプルではすべてコードで記述していますが、画像を読み込ませるよう書き換えることも可能です。)
  3. プレイヤーやブロックなどのゲームオブジェクトを作成し、画面内に配置します。
    このとき、ブロックとボタンは静的ボディとし、それ以外は動的ボディとします。
    また、ブロックと星はそれぞれのグループを定義し個々の要素を追加します。
  4. 各ゲームオブジェクトが衝突したときや重なったときに、どう処理するかを定義します。
    たとえば、プレイヤーとブロックは衝突するよう設定しますが、一部のオブジェクト同士は反応しないようにします。
  5. プレイヤーと他のオブジェクトが重なったときの処理を定義します。
  6. キー操作の設定をし、プレイヤーが画面上で動くよう処理をします。

コードの解説

まずGameConfigにおいて、下記のコードのようにarcadeを指定します。
gravity: { x: 0, y: 300 }は、Y方向の重力(物体の加速度 (ピクセル/秒))を意味します。
debug: trueにすることで、衝突検知の範囲を画面に表示します。

Phaser API Documentation | Phaser.Types.Core.GameConfig

const config: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 960,
    height: 640,
    backgroundColor: '#000080',
    pixelArt: false,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
    },
    physics: {
        default: "arcade", // ここでarcadeを指定します。
        arcade: {
            gravity: { x: 0, y: 300 }, // y:重力
            debug: true, // true にすることで衝突検知の範囲を画面に表示します。
        },
    },
    scene: [Phaser3Demo01]
};

ステージデータの作成

ここから画面上にオブジェクトを配置し、ステージを作成していきます。

このゲームでは登場するオブジェクトの可動範囲は画面内のみで、画面からは出ないものとします。
ただし、星が降ってくる演出のため画面外の上部100pxは可動範囲とします。
this.physics.world.setBounds()で範囲を設定します。これをワールド境界(World bounds)といいます。
第1,2引数が左上のX,yポジション、第3,4引数が横幅、縦幅となります。

// 画面(キャンバス)サイズを取得
const { width: cW, height: cH } = this.game.canvas;
// オブジェクトの可能範囲を設定。範囲外は壁・床の役割となる。
this.physics.world.setBounds(0, -100, cW, cH + 100);

テクスチャの作成

このサンプルではコード内でテクスチャを生成してますが、各自でファイル用意すればpreload()メソッドで画像を読み込むことも可能です。(ソースのコメント参照)

プレイヤー

this.physics.add.sprite()playerテクスチャからプレイヤーのオブジェクトを作成します。this.add.*ではなく、this.physics.add.*なので注意してください。
setBounce()メソッドで弾力、つまりどれくらい弾むかを設定します。1のとき100%の距離で跳ね返り、0.5のときには50%で跳ね返ります。
各種プロパティについては、後半で詳しく紹介します。

ゲームオブジェクトを配置した際のボディの形状は、ゲームオブジェクトの形状と同一じで、形は四角形になります。
これを変更するには、setBodySizeメソッドで四角形の大きさを指定します。
円形にする場合はsetCircleメソッドを使用し、半径を指定します。
さらに、setOffsetメソッドで中心点からのオフセット(ズレ)を変更することもできます。
(今回のサンプルでは、初期値のままで変更不要なのでコメントアウトしてあります。)

// プレイヤー
this.player = this.physics.add.sprite(80, 400, 'player').setBounce(0.1);
// プレイヤーのボディを四角形で指定する
//this.player.setBodySize(40, 40).setOffset(0, 0);

setCollideWorldBounds(true)メソッドを呼び、プレイヤーの移動範囲をワールド境界に設定します。これをしないと画面の外に落ちていきます。

// プレイヤーの移動範囲を画面の中だけとする。
this.player.setCollideWorldBounds(true);

ボタン

this.add.rectangle()で通常のゲームオブジェクトを作ります。
それをthis.physics.add.existing()メソッドで物理オブジェクトとして扱えるようにします。
第2引数は静的かどうかのフラグとなり、trueの場合は静的ボディとなります。

this.button = this.add.rectangle(200, 640, 60, 15, 0xFF6060).setOrigin(0.5, 1);
this.physics.add.existing(this.button, true);

ブロックグループ

まず、通常のゲームオブジェクトを作っておきます。
this.physics.add.staticGroup()で静的ボディのグループを作り、作成済みのゲームオブジェクトを引数に指定することで、
それらを静的ボディの要素としてグループに追加します。
なお、後から要素を追加する場合はグループのインスタンスに対しaddまたはaddMultipleメソッドを使います。

ここでは学習のためcreateメソッドを使用してテクスチャから要素を生成、追加する方法も記載しています。

// ブロックグループ (1)ゲームオブジェクト(横長の四角形)から作るケース
const rect1 = this.add.rectangle(350, 330, 150, 30, 0xC7915A);
const rect2 = this.add.rectangle(700, 430, 150, 30, 0xC7915A);
const rect3 = this.add.rectangle(450, 530, 150, 30, 0xC7915A);
this.blockGroup = this.physics.add.staticGroup([rect1, rect2]); // groupの生成と要素の追加を同時に行う場合
this.blockGroup.add(rect3); // 要素を追加する場合はaddまたはaddMultipleメソッドを使う

// ブロックグループ (2)テクスチャ(正方形)から作るケース
this.blockGroup.create(600, 200, 'block');
this.blockGroup.create(400, 100, 'block');

障害物

プレイヤーの時と同じように、テクスチャから物理オブジェクトを作成します。
ここでのポイントはbodyプロパティからsetAllowGravity(false)メソッドを呼び、重力の影響を無効にしていることです。

setCircleメソッドでボディの形状を半径20の円形に、setVelocityメソッド等で速度と回転速度を設定します。

this.obstacle = this.physics.add.sprite(600, 510, 'obstacle')
    .setCircle(20).setBounce(1).setVelocity(300, 200).setAngularVelocity(350);
this.obstacle.body.setAllowGravity(false); // 重力の影響を無効化
this.obstacle.setCollideWorldBounds(true); // 画面の外に出ないようにする

星グループ

ブロックグループの時と同様にグループを作成します。
ブロックは静的ボディなのでstaticGroup()でしたが、今回は動的ボディなのでgroup()となります。
コードを見るとわかるように、イテレータを使って各要素に対しプロパティを適用します。
なお、オブジェクトは作成した時点で有効になっているため、ここでは一旦無効にします。
星を降らせる処理については後で説明します。

// 星グループ (1) spriteのオブジェクトを作成してからgroupに追加するケース
const star1 = this.physics.add.sprite(0, 0, 'star');
const star2 = this.physics.add.sprite(0, 0, 'star');
const star3 = this.physics.add.sprite(0, 0, 'star');
const star4 = this.physics.add.sprite(0, 0, 'star');
this.starGroup = this.physics.add.group([star1, star2]); // groupの生成と要素の追加を同時に行う場合
this.starGroup.addMultiple([star3, star4]); // 要素を追加する場合はaddまたはaddMultipleメソッドを使う
// groupの中の要素をイテレータで一個ずつ処理する
this.starGroup.children.iterate(s => {
    // 星のインスタンス
    const star = s as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    star.setCollideWorldBounds(true); // 画面の外に出ないようにする
    star.setCircle(20);
    star.disableBody(true, true); // 初回では一旦無効にする
    return true;
});
// 星を降らせる処理
this.dropStars();

星グループ その2

学習のために、group作成時にコンフィグを渡してオブジェクトを作る方法も紹介します。
同一のオブジェクトを一括で作成できます。

// 星グループ (2) group作成時にコンフィグを渡してオブジェクトを作るケース
// ※こちらを有効にする場合は星グループ (1)のコードをコメントアウトしてください
this.starGroup = this.physics.add.group({
    collideWorldBounds: true,
    key: 'star',
    active: true,
    visible: true,
    frameQuantity: 4,
    setXY: { x: 200, y: -50, stepX: 150, stepY: 50 },
} as Phaser.Types.Physics.Arcade.PhysicsGroupConfig | Phaser.Types.GameObjects.Group.GroupCreateConfig);

Phaser API Documentation | Phaser.Types.Physics.Arcade.PhysicsGroupConfig
Phaser API Documentation | Phaser.Types.GameObjects.Group.GroupCreateConfig

衝突検知

これまでに作成したオブジェクト同士またはグループが衝突するよう設定します。
コードにあるようにthis.physics.add.collider()メソッドの第1~2引数にゲームオブジェクト等を指定します。
ここで指定した要素はゲーム画面上でぶつかった際に跳ね返るようになります。
一方で、設定してないオブジェクトについては衝突せず、ゲーム画面上ではすり抜けます。

// 衝突検知:プレイヤーとブロック
this.physics.add.collider(this.player, this.blockGroup);
// 衝突検知:星とブロック
this.physics.add.collider(this.blockGroup, this.starGroup);
// 衝突検知:星同士
this.physics.add.collider(this.starGroup, this.starGroup);

重なり検知

オブジェクト同士が重なったときの処理をthis.physics.add.overlap()メソッドを使って設定します。
衝突検知のときと同様に第1、2引数にゲームオブジェクト等を指定し、第3引数に処理を行うアロー関数式を記述します。
このゲームではプレイヤーがボタン・星・障害物のいずれかに重なったときの処理を行っています。

アロー関数式の仮引数に入るインスタンスは、第1、2引数に指定したゲームオブジェクトそのものになります。
グループの場合は、グループの中の該当ゲームオブジェクトになります。

ボタンとの重なりについては、星を降らす処理を呼び出しています。

// 重なり検知:プレイヤーとボタン
this.physics.add.overlap(this.player, this.button, () => this.dropStars());

星との重なりについては、s.disableBody(true, true)を呼び出し、ゲームオブジェクトを無効にしています。
destroyにしないのは後でオブジェクトを再利用するためです。

// 重なり検知:プレイヤーと星
this.physics.add.overlap(this.player, this.starGroup, (p, s) => {
    // 星のインスタンス
    const star = s as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    star.disableBody(true, true);
});

障害物との重なりについては、プレイヤーを青くしてダメージ受けた演出を行います。
さらにタイマー機能で0.5秒後に色が戻るよう設定します。

// 重なり検知:プレイヤーと障害物
this.physics.add.overlap(this.player, this.obstacle, (p, o) => {
    // プレイヤーのインスタンス
    const player = p as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    if (player.isTinted === false) {
        player.setTint(0x0000FF); // ダメージを受ける演出
        this.time.addEvent({ delay: 500, callback: () => player.clearTint() }); // 0.5秒後に戻る
    }
});

キー入力設定

以下のコードで矢印キー等からの入力を捕捉するように設定します。

// キー入力設定
this.cursor = this.input.keyboard.createCursorKeys();

update()メソッド内では、画面描画のステップごとにキー入力状態をチェックし、入力ボタンに合わせてプレイヤーに速度を付けます。
ジャンプ操作については、プレイヤーがワールド境界または静的ボディを持つゲームオブジェクトに接地しているときのみ可能にします。

// プレイヤーの移動速度を0に
this.player.setVelocityX(0);

// 左右のキーに合わせて移動
if (this.cursor.left.isDown) {
    this.player.setVelocityX(-200);
}
else if (this.cursor.right.isDown) {
    this.player.setVelocityX(200);
}

// ジャンプ(プレイヤーが地面に接地しているときのみジャンプ可能)
if ((this.cursor.space.isDown || this.cursor.up.isDown) && this.player.body.onFloor()) {
    this.player.setVelocityY(-300);
}

Phaser API Documentation | Phaser.Input.Keyboard.KeyboardPlugin
Phaser API Documentation | Phaser.Input.Keyboard.Key

星を降らせる処理

countActive(true)メソッドを使用し、画面上で有効な星の数をカウントします。
0だった場合、イテレータで全要素に対しenableBody()メソッドで再度有効にします。そのときに表示の位置はリセットします。
横方向の速度については配列からランダムで決定しています。

// 星を降らせる処理
dropStars() {
    if (this.starGroup.countActive(true) === 0) {
        this.starGroup.children.iterate(s => {
            // 星のインスタンス
            const star = s as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
            star.enableBody(true, Phaser.Math.Between(200, 940), -50, true, true);
            star.setBounce(0.8);
            star.setVelocityX(Phaser.Utils.Array.GetRandom([-200, -100, 100, 200]));
            return true;
        });
    }
}

ボディに設定できる物理演算に関するメソッド一覧

ボディに設定できる物理演算に関係するプロパティとそのsetメソッドの一覧です。
メソッドはPhaser.Physics.Arcade.Spriteクラスに、プロパティはPhaser.Physics.Arcade.Bodyクラスにあります。
ゲーム内の例でいえば、プレイヤーの速度を設定するにはthis.player.setVelocity()メソッドを使用し、取得するにはthis.player.body.velocityプロパティを参照します。

なおArcadeでは、ゲームオブジェクトに角度をつけても、見た目の変化はありますが、ボディには影響しません。
つまり、四角形のボディを斜めに配置することはできません。

また、ゲームオブジェクトを回転させても(角度の速度をつけても)見た目の変化のみで、
円形のオブジェクトをタイヤのように機能させることはできません。他の角度に関するプロパティも同様です。

*1印は、setVelocityに対しsetVelocityXsetVelocityYがあるというように、X,Yごとのメソッドがあるものを示します。

日本語名 プロパティ メソッド名 補足説明
加速度 acceleration setAcceleration (number, [number]) *1
角度の速度 angularVelocity setAngularVelocity (number) (上記の説明参照)
角度の加速度 angularAcceleration setAngularAcceleration (number)
角度の抗力 angularDrag setAngularDrag (number)
弾力 bounce setBounce (number, [number]) *1
抗力 drag setDrag (number, [number]) *1
摩擦 friction setFriction (number, [number]) *1 Arcade Physics では、摩擦は「動かない」ボディから乗っているボディへの動きの転送の特殊なケースです。
重力 gravity setGravity (number, [number]) *1
インムーバブル immovable setImmovable (boolean) このボディが他のボディとの衝突で離れていくかどうかを設定します。
true(初期値)の場合、移動可能なボディに衝突した時、反発しません。
質量 mass setMass (number)
プッシュ可能 pushable setPushable (boolean) このボディを別のボディによってプッシュできるかどうかを設定します。
速度 velocity setVelocity (number, [number]) *1
最大速度 maxVelocity setMaxVelocity (number, [number]) *1 ボディの最大速度を設定します。

Phaser API Documentation | Phaser.Physics.Arcade.Sprite
Phaser API Documentation | Phaser.Physics.Arcade.Body

Discussion