phaserでゲーム開発 第四回
phaserでゲーム開発シリーズ、第四回。
前回の記事はこちら。
今回はプレイヤーにアニメーションをつけてみます。
全体像
まずは、今回のコードの全文です。
/**
* ゲームのメインシーンを表すクラス
*
* @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);
// プレイヤーのアニメーションを登録する
// - それぞれのキー(down/left/right/up)に対応した歩行アニメーションを定義
// - frames: スプライトシートの何番フレームを使うか(連番指定)
// - frameRate: 1秒間に何枚切り替えるか(数値が大きいほど速く動く)
// - repeat: -1 にすると無限ループで再生
this.anims.create({
key: 'player-walk-down',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 2 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'player-walk-left',
frames: this.anims.generateFrameNumbers('player', { start: 3, end: 5 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'player-walk-right',
frames: this.anims.generateFrameNumbers('player', { start: 6, end: 8 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'player-walk-up',
frames: this.anims.generateFrameNumbers('player', { start: 9, end: 11 }),
frameRate: 8,
repeat: -1,
});
// プレイヤーが「止まっているとき」に表示する静止フレームを定義
// - 各方向の歩行アニメーションの中央フレームを採用することで、自然に見える
this.idleFrame = {
down: 1, // 下向き(0〜2フレームの中央)
left: 4, // 左向き(3〜5フレームの中央)
right: 7, // 右向き(6〜8フレームの中央)
up: 10, // 上向き(9〜11フレームの中央)
};
// プレイヤーが最後に向いていた方向を記録する変数
// - 停止時やジャンプ中など、次の見た目を決めるために利用する
this.lastFacing = 'right';
}
/**
* 毎フレーム呼ばれる更新処理
* - プレイヤー操作や物理演算などを記述する
*
* @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);
// 地面にいる場合のみ「歩くアニメーション」を再生
if (onFloor) {
this.#player.anims.play('player-walk-left', true);
}
this.lastFacing = 'left';
} else if (this.#cursors.right.isDown) {
// →キーを押している間は右へ移動
this.#player.setVelocityX(this.#runSpeed);
// 地面にいる場合のみ「歩くアニメーション」を再生
if (onFloor) {
this.#player.anims.play('player-walk-right', true);
}
this.lastFacing = 'right';
} else {
// 左右キーを押していないときは静止
this.#player.setVelocityX(0);
// 地面に立っているなら歩行アニメーションを止めて「待機用の静止フレーム」を表示
if (onFloor) {
this.#player.anims.stop();
const idle = this.idleFrame[this.lastFacing] ?? this.idleFrame.right;
this.#player.setFrame(idle);
}
}
// === ジャンプ ===
// ↑キーが押され、かつ接地しているときのみジャンプ可能
if (this.#cursors.up.isDown && onFloor) {
this.#player.setVelocityY(-this.#jumpSpeed);
}
// === 空中の見た目 ===
// 宙に浮いている間は歩行アニメーションを止め、最後に向いていた方向の静止フレームを表示
if (!onFloor) {
this.#player.anims.stop();
const idle = this.idleFrame[this.lastFacing] ?? this.idleFrame.right;
this.#player.setFrame(idle);
}
}
}
createPlayer
アニメーションの登録
プレイヤーキャラクターを動かすときには「歩くアニメーション」と「止まっているときの見た目」の両方が必要です。
Phaser では this.anims.create() を使って歩行アニメーションを登録し、さらに停止時に表示する静止フレームを決めることで自然な動きを表現できます。
ここでは「下・左・右・上」に対応する歩行アニメーションを4種類登録しています。
key: アニメーションの名前。あとで anims.play('player-walk-right') のように呼び出します。
frames: どのフレーム番号を使うか。ここではスプライトシートの範囲を指定しています。
例){ start: 0, end: 2 } → フレーム 0,1,2 を使用
frameRate: 1秒間に何枚のフレームを切り替えるか。数値が大きいほど速く動きます。
repeat: ループ回数。-1 にすると無限ループになります。
これにより、キー入力に応じて「上下左右の歩行アニメーション」を切り替えられるようになります。
静止フレームの設定
キャラクターが止まったときには、歩行アニメーションを再生し続けるわけにはいきません。
そのため、各方向の「待機用のフレーム」を別途定義しておきます。
コード例:
this.idleFrame = {
down: 1, // 下向き(0〜2フレームの中央)
left: 4, // 左向き(3〜5フレームの中央)
right: 7, // 右向き(6〜8フレームの中央)
up: 10, // 上向き(9〜11フレームの中央)
};
歩行アニメーションが3枚構成の場合、真ん中のフレームを「自然な立ち姿」として使うと違和感が少なくなります。
最後に向いていた方向を記録する
停止時にどちらを向いているかを決めるために「最後に押された方向キー」を覚えておきます。
コード例:
this.lastFacing = 'right';
これで、例えば「右に移動してキーを離した場合は右を向いたまま立ち止まる」といった自然な動作を実現できます。
update
この update() は、見た目(アニメーション)を次の三つの状態で切り替えています。
接地して左右に歩いているとき → 歩行アニメーションを再生
接地していて左右キーを離したとき → 歩行アニメーションを止めて静止フレーム表示
宙にいるとき(ジャンプ中・落下中) → 歩行アニメーションを止めて静止フレーム表示
それぞれの意図を詳しく解説します。
歩行アニメーションの再生(接地中のみ)
左右キーが押されている間、this.#player.anims.play('player-walk-left' | 'player-walk-right', true) を呼びます。
第2引数の true は「すでに同じアニメが再生中なら再スタートしない(ignoreIfPlaying)」という意味です。これによりキーを押しっぱなしでもフレームが頭に戻らず、滑らかに歩き続けます。
onFloor 条件を付けているのは、空中で足がバタつくのを避けるため(多くの2Dアクションで自然に見える定石)。
待機時の静止フレーム(接地中に左右キーを離したら)
まず this.#player.anims.stop() で歩行アニメーションを停止します。
次に this.#player.setFrame(idle) で「立ち姿」の1枚絵に切り替えます。
アニメを止めるだけだと“歩行の途中のコマ”で止まって不自然になりがちなので、あえて静止フレームを指定して見た目を整えています。
どの静止フレームを出すかは this.lastFacing(最後に向いていた方向)と this.idleFrame(方向ごとの立ち姿フレーム番号)で決めます。
例:最後に右へ歩いていたなら right 用の静止フレーム(7)を表示。
空中の見た目(ジャンプ中・落下中)
!onFloor の間は常に歩行アニメーションを止め、静止フレームに固定します。
ここではジャンプ専用フレームは使っていません(シンプルさ優先)。将来的に「ジャンプ用1枚絵」や「落下用1枚絵」を追加したければ、if (!onFloor) { setFrame(jumpFrame) } のように差し替え可能です。
なお、ジャンプの発動自体は速度設定(setVelocityY)で行っており、アニメーションとは分離しています。見た目は「空中=静止フレーム固定」という設計です。
** 向きの記録 lastFacing の役割**
左右の入力処理のたびに this.lastFacing = 'left' | 'right' を更新しています。
これにより、キーを離れた瞬間や空中にいる間でも「どちらを向いて止まるべきか」を判断できます。停止時やジャンプ中に不自然な方向へ向くのを防ぐ、見た目の一貫性の仕組みです。
現状
アニメーションがついて華やかになりましたね。
お疲れ様でした。
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion