進捗:
03/8

PhaserJS入門
スプライト移動の基本

今度は動くゲームを作ろう!クリックでキャラクターを移動させる

🎮 サンプルビルドの動作確認

実際の動作を見てみよう

この章で作成するサンプルプログラムの動作を確認できます。画面をクリック/タップすると、プレイヤーキャラクターがその場所に向かって滑らかに移動します。

画面サイズ: 800 × 600 | 背景: 青空 | プレイヤー: 移動可能 | 敵: 固定

新しいタブで開く

🎯 この章で学べること

基本概念

  • • スプライトの移動システム
  • • クリック/タップイベントの処理
  • • 速度と時間の関係
  • • 滑らかな移動の実装

実装技術

  • • PhaserJSの入力システム
  • • update()メソッドの活用
  • • 数学的計算による移動
  • • パフォーマンス最適化

🚀 移動システムの基本概念

ゲーム開発における移動の重要性

ゲームの世界では、キャラクターやオブジェクトが動くことで世界が生き生きとします。 移動システムは、アクションゲーム、RPG、パズルゲームなど、ほとんどのゲームジャンルで必要となる基本的な機能です。

1. 入力処理

プレイヤーの操作(クリック、タップ、キー入力)を検知し、移動の目標地点を決定します。

2. 移動計算

現在位置から目標地点までの距離と方向を計算し、適切な速度で移動させます。

3. 画面更新

毎フレームごとに位置を更新し、滑らかなアニメーションを実現します。

🔧 実装手順

Step 1: 移動に必要な変数を追加

02章のコードに、移動に必要な変数を追加します。速度と目標地点の座標を管理する変数が必要です。

// 速度(px/秒) private speedX: number = 200; private speedY: number = 200; // 目標地点 private targetX: number = 0; private targetY: number = 0;

💡 ポイント

  • • 速度は「ピクセル/秒」で指定(200px/秒 = 1秒で200ピクセル移動)
  • • X方向とY方向で異なる速度を設定可能
  • • 目標地点は初期値として現在位置を設定

Step 2: クリック/タップイベントの処理

プレイヤーが画面をクリック/タップした時に、その座標を目標地点として設定します。

// 画面タップ(クリック)で目標地点を更新 this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { this.targetX = pointer.worldX; this.targetY = pointer.worldY; });

🔍 詳細解説

  • this.input.on('pointerdown', ...) でマウスクリック/タッチを検知
  • pointer.worldXpointer.worldY でクリック位置の座標を取得
  • • 座標はワールド座標系(カメラの位置を考慮した座標)

📋 create関数の完全なコード

Step1とStep2の、完全なcreate関数のコードです。 既存の画像配置処理に加えて、クリック/タップイベントの処理が追加されています。

create () { // background, player, enemy 配置 this.add.image(400, 300, 'background'); this.player = this.add.image(300, 300, 'player'); this.add.image(500, 300, 'enemy'); // 初期の目標地点は現在位置 this.targetX = this.player.x; this.targetY = this.player.y; // 画面タップ(クリック)で目標地点を更新 this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { this.targetX = pointer.worldX; this.targetY = pointer.worldY; }); }

既存の処理(02章から継承)

  • • 背景画像の配置(中央)
  • • プレイヤー画像の配置(左側)
  • • 敵画像の配置(右側)
  • • プレイヤーオブジェクトの参照保存

新規追加された処理

  • • 目標地点の初期化
  • • クリック/タップイベントリスナーの設定
  • • 座標取得と目標地点の更新
  • • 移動システムの準備

💡 重要なポイント

  • • 既存の画像配置処理はそのまま維持
  • • プレイヤーオブジェクトの参照(this.player)が重要
  • • 初期目標地点は現在位置に設定
  • • イベントリスナーはcreate関数内で設定

Step 3: update()メソッドでの移動処理

毎フレーム実行されるupdate()メソッドで、プレイヤーの位置を目標地点に向かって移動させます。

update (_time: number, delta: number) { if (!this.player) { return; } // deltaはmsなので秒に変換 const dt = delta / 1000; // X方向の移動量を計算(目標に向かって進み、行き過ぎないようクランプ) const dx = this.targetX - this.player.x; if (dx !== 0) { const stepX = Math.sign(dx) * this.speedX * dt; this.player.x += Math.abs(stepX) > Math.abs(dx) ? dx : stepX; } // Y方向の移動量を計算(同様にクランプ) const dy = this.targetY - this.player.y; if (dy !== 0) { const stepY = Math.sign(dy) * this.speedY * dt; this.player.y += Math.abs(stepY) > Math.abs(dy) ? dy : stepY; } }

⚡ パフォーマンスのポイント

  • delta は前フレームからの経過時間(ミリ秒)
  • • 秒に変換してから速度計算を行うことで、フレームレートに依存しない一定の速度を実現
  • • 目標地点を超えないようクランプ処理で正確な位置に到達
  • • クランプ処理がないと目標地点を通り過ぎてしまう可能性がある

🔍 deltaとは何か?なぜ重要なのか?

deltaの基本概念

deltaは、前のフレームから現在のフレームまでの経過時間をミリ秒(ms)で表した値です。

例:60FPSの場合

delta ≈ 16.67ms(1000ms ÷ 60)

1フレームあたり約16.67ミリ秒

この値は、ゲームエンジンが自動的に計算して提供してくれます。

なぜdeltaが必要なのか?

デバイスによってフレームレートが異なるため、固定値での移動では速度が変わってしまいます。

❌ 固定値での移動(問題あり)

this.player.x += 5; // 毎フレーム5px移動

60FPS: 300px/秒、30FPS: 150px/秒

✅ delta使用での移動(正解)

this.player.x += speed * (delta/1000);

常に一定の速度(例:200px/秒)

📱 デバイスごとのリフレッシュレートの違い

現代のゲーム開発では、様々なデバイスで動作する必要があります。各デバイスのリフレッシュレートは大きく異なります:

🖥️ デスクトップPC

  • • 60Hz(60FPS): 一般的
  • • 120Hz(120FPS): ゲーミングモニター
  • • 144Hz(144FPS): 高級ゲーミング
  • • 240Hz(240FPS): プロゲーミング

📱 モバイルデバイス

  • • 30Hz(30FPS): 古いスマートフォン
  • • 60Hz(60FPS): 一般的なスマートフォン
  • • 90Hz(90FPS): 中級スマートフォン
  • • 120Hz(120FPS): 高級スマートフォン

🎮 ゲーム機・その他

  • • 30Hz(30FPS): 古いゲーム機
  • • 60Hz(60FPS): 現代のゲーム機
  • • 可変: パフォーマンスに応じて変化
  • • 省電力モード: 30Hz以下

🚨 固定値での移動が問題となる理由

もしdeltaを使わずに固定値で移動させた場合、以下のような問題が発生します:

低速デバイス(30FPS)

毎フレーム5px移動

1秒間: 30 × 5 = 150px

結果: 非常に遅い移動

高速デバイス(120FPS)

毎フレーム5px移動

1秒間: 120 × 5 = 600px

結果: 非常に速い移動

✅ delta使用による解決

deltaを使用することで、どのデバイスでも一定の速度を実現できます:

// 速度200px/秒で移動 const speed = 200; // px/秒 const dt = delta / 1000; // 秒に変換 const movement = speed * dt; // このフレームでの移動量 // 30FPSの場合: movement = 200 × (33.33/1000) = 6.67px // 60FPSの場合: movement = 200 × (16.67/1000) = 3.33px // 120FPSの場合: movement = 200 × (8.33/1000) = 1.67px

結果として、どのデバイスでも1秒間に200px移動する一定の速度が保たれます。

🚨 実際のゲームで発生した問題事例

これは理論上の問題ではなく、実際のゲームで深刻なバグとして発生した問題です。 特に家庭用ゲーム機からPCへ移植された際に、フレームレート依存の処理が原因で様々な問題が起きています。

🎮 DARK SOULS II(ダークソウル2)

問題:武器の耐久度減少がフレームレートに依存

  • • 30FPS/60FPS: 正常動作
  • • 120FPS以上: 耐久度が2-3倍速で減少
  • • 結果: 高スペックPCで武器が異常に早く壊れる
  • • 影響: ゲーム難易度の意図しない上昇

フレーム単位で耐久度処理を行ったため、高フレームレート環境で深刻な問題が発生

🤖 ニーア オートマタ

問題:回避アクションとゲームスピードの異常

  • • 60FPS超: 回避の無敵時間が短縮
  • • 高フレームレート: ゲームスピードが高速化
  • • 結果: ゲームバランスの崩壊
  • • 影響: プレイヤビリティの大幅な低下

物理演算とゲーム内時間がフレームレートに依存していた

🎯 メタルギアソリッドV

問題:物理演算とヘリの挙動異常

  • • 120FPS: 敵兵の吹き飛び挙動が異常
  • • 高フレームレート: オブジェクト物理が荒ぶる
  • • ヘリ着陸: 不自然に高速化
  • • 結果: ゲーム体験の質の低下

🧟 デッドライジング3

問題:ゲーム全体のスピード異常

  • • 60FPS超: ゲームスピードが高速化
  • • ゾンビの動き: 異常に速くなる
  • • 音声ズレ: 口の動きと音声が合わない
  • • 結果: ゲームプレイの支障

💡 これらの問題の共通点

  • フレーム単位での処理: 毎フレーム固定値を加算/減算
  • 時間の概念がない: deltaを使用していない
  • デバイス性能差の影響: 高スペックPCで問題が顕在化
  • ゲーム体験の破綻: 意図しない難易度上昇やバランス崩壊

🚀 deltaを使うことがなぜ重要なのか?

🎯

一貫性

どのデバイスでも同じゲーム体験を提供

🔧

保守性

フレームレート変更時の修正が不要

💪

将来性

新しいデバイスにも対応可能

📚 学習者へのメッセージ

これらの実例を見ると、deltaを使うことは単なる「良い習慣」ではなく、 プロフェッショナルなゲーム開発者として必須の技術であることが分かります。 今から正しい習慣を身につけることで、将来のゲーム開発でこのような問題を防ぐことができます。

⚠️ クランプ処理の重要性

🔍 クランプ(Clamp)とは何か?

クランプ処理とは、ある値を指定された範囲内に制限する処理のことです。 ゲーム開発では、移動量が残り距離を超えないように制限する際によく使用されます。

📚 クランプの基本概念

クランプ処理は以下のような形で実装されます:

// 基本的なクランプ処理

const clampedValue = Math.min(max, Math.max(min, value));

// または

const clampedValue = value > max ? max : (value < min ? min : value);

これにより、値が指定範囲内に収まることが保証されます。

🎯 移動でのクランプ処理

私たちのコードでは、移動量が残り距離を超えないようにクランプしています:

// 移動でのクランプ処理

const stepX = Math.sign(dx) * this.speedX * dt;

const clampedStepX = Math.abs(stepX) > Math.abs(dx) ? dx : stepX;

// 移動量が残り距離を超えないよう制限

これにより、目標地点を通り過ぎることがなくなります。

💡 クランプ処理の利点

  • 正確性: 目標地点にピッタリ到達できる
  • 安定性: 振動や行き過ぎが発生しない
  • 予測可能性: 移動結果が予測可能になる
  • パフォーマンス: 不要な計算処理を回避できる

クランプ処理を削除すると何が起こるか?

先ほどのコードで使用していたクランプ処理(Math.abs(stepX) > Math.abs(dx) ? dx : stepX)を削除すると、 プレイヤーが目標地点を通り過ぎてしまう問題が発生します。

問題のあるコード例

以下のようにクランプ処理を削除した場合の動作を確認してみましょう:

// X方向の移動量を計算(クランプ処理なし) const dx = this.targetX - this.player.x; if (dx !== 0) { const stepX = Math.sign(dx) * this.speedX * dt; // this.player.x += Math.abs(stepX) > Math.abs(dx) ? dx : stepX; this.player.x += stepX; // ← クランプ処理なし } // Y方向の移動量を計算(クランプ処理なし) const dy = this.targetY - this.player.y; if (dy !== 0) { const stepY = Math.sign(dy) * this.speedY * dt; // this.player.y += Math.abs(stepY) > Math.abs(dy) ? dy : stepY; this.player.y += stepY; // ← クランプ処理なし }

🚨 発生する問題

  • • プレイヤーが目標地点を通り過ぎてしまう
  • • 目標地点の周りで行ったり来たりする「振動」が発生
  • • 正確な位置に到達できない
  • • ゲームの操作性が悪くなる

クランプ処理あり(推奨)

this.player.x += Math.abs(stepX) > Math.abs(dx) ? dx : stepX;

動作:目標地点に正確に到達し、通り過ぎることがない

✅ 滑らかで正確な移動

✅ 目標地点で停止

✅ 振動なし

クランプ処理なし(問題あり)

this.player.x += stepX;

動作:目標地点を通り過ぎ、振動が発生する

❌ 目標地点を通り過ぎる

❌ 振動が発生

❌ 正確な位置に到達できない

なぜ振動が発生するのか?

クランプ処理がない場合、以下のような流れで振動が発生します:

🚨 クランプ処理なしの実際の動作

以下のサンプルでは、クランプ処理を削除した状態で動作します。プレイヤーが目標地点を通り過ぎて振動する様子を確認できます:

画面サイズ: 800 × 600 | 背景: 青空 | プレイヤー: 移動可能(クランプ処理なし) | 敵: 固定

新しいタブで開く

🔍 観察ポイント
  • • プレイヤーが目標地点を通り過ぎる様子
  • • 目標地点の周りで行ったり来たりする振動
  • • 正確な位置に到達できない問題
  1. 1. 目標地点に近づく:プレイヤーが目標地点に向かって移動
  2. 2. 通り過ぎる:1フレームで目標地点を通り過ぎてしまう
  3. 3. 方向転換:次のフレームで逆方向に移動開始
  4. 4. 再び通り過ぎる:今度は反対側を通り過ぎる
  5. 5. 振動の継続:この動作が繰り返される

具体例:速度200px/秒、60FPSの場合、1フレームで約3.3px移動します。 目標地点まで1pxの距離がある場合でも、3.3px移動してしまうため、必ず通り過ぎてしまいます。

クランプ処理の仕組み

クランプ処理は、移動量が残り距離を超えないように制限する処理です:

// クランプ処理の詳細 const stepX = Math.sign(dx) * this.speedX * dt; // 計算された移動量 const clampedStepX = Math.abs(stepX) > Math.abs(dx) ? dx : stepX; // クランプ処理 // 例:目標まで5px、1フレームで8px移動する場合 // stepX = 8px, dx = 5px // Math.abs(8) > Math.abs(5) → true // 結果:clampedStepX = 5px(残り距離分のみ移動)

💡 クランプ処理の利点

  • 正確性:目標地点にピッタリ到達
  • 安定性:振動や行き過ぎが発生しない
  • 操作性:プレイヤーの意図通りの動作
  • パフォーマンス:不要な計算処理を回避

📝 完全なコード

import { Scene } from 'phaser'; export class PutSprite extends Scene { // プレイヤー本体 private player!: Phaser.GameObjects.Image; // 速度(px/秒) private speedX: number = 200; private speedY: number = 200; // 目標地点 private targetX: number = 0; private targetY: number = 0; constructor () { super('PutSprite'); } preload () { // background, player, enemy をプリロード this.load.image('player', 'assets/player.png'); this.load.image('enemy', 'assets/enemy.png'); this.load.image('background', 'assets/background.png'); } create () { // background, player, enemy 配置 this.add.image(400, 300, 'background'); this.player = this.add.image(300, 300, 'player'); this.add.image(500, 300, 'enemy'); // 初期の目標地点は現在位置 this.targetX = this.player.x; this.targetY = this.player.y; // 画面タップ(クリック)で目標地点を更新 this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { this.targetX = pointer.worldX; this.targetY = pointer.worldY; }); } update (_time: number, delta: number) { if (!this.player) { return; } // deltaはmsなので秒に変換 const dt = delta / 1000; // X方向の移動量を計算(目標に向かって進み、行き過ぎないようクランプ) const dx = this.targetX - this.player.x; if (dx !== 0) { const stepX = Math.sign(dx) * this.speedX * dt; this.player.x += Math.abs(stepX) > Math.abs(dx) ? dx : stepX; } // Y方向の移動量を計算(同様にクランプ) const dy = this.targetY - this.player.y; if (dy !== 0) { const stepY = Math.sign(dy) * this.speedY * dt; this.player.y += Math.abs(stepY) > Math.abs(dy) ? dy : stepY; } } }

⚠️ 重要なポイント

  • • 02章のコードから、移動に必要な変数とメソッドを追加
  • • 既存の画像読み込みと配置処理はそのまま維持
  • • 移動処理はupdate()メソッド内で毎フレーム実行

🔬 動作の仕組みを詳しく解説

1. イベントループ

ゲームエンジンは以下の流れで動作します:

  1. 1. 入力処理(クリック/タップ検知)
  2. 2. ゲームロジック更新(update実行)
  3. 3. 画面描画(自動実行)
  4. 4. 1に戻る(60FPSなら約16.7ms間隔)

2. 移動計算の詳細

移動量の計算式:

移動量 = 速度 × 時間

stepX = speedX × (delta / 1000)

新しい位置 = 現在位置 + 移動量

なぜ滑らかに動くのか?

1秒間に60回(60FPS)実行されるupdate()メソッドで、毎回少しずつ位置を更新することで、 人間の目には滑らかな動きとして見えます。これは映画やアニメと同じ原理です。

例:速度200px/秒の場合、60FPSでは1フレームあたり約3.3px移動
200 ÷ 60 ≈ 3.3px/フレーム

🎨 カスタマイズのヒント

速度の調整

// より速く移動 private speedX: number = 400; private speedY: number = 400; // より遅く移動 private speedX: number = 100; private speedY: number = 100;

数値を変更することで、キャラクターの移動速度を簡単に調整できます。

移動の制限

// 画面外に出ないよう制限 this.player.x = Math.max(0, Math.min(800, this.player.x)); this.player.y = Math.max(0, Math.min(600, this.player.y));

プレイヤーが画面外に出ないよう、座標に制限を設けることができます。

移動アニメーションの追加

// 移動中は回転させる if (dx !== 0 || dy !== 0) { this.player.rotation += 0.1; }

移動中に回転やスケール変更などのアニメーションを追加することで、 より動的で魅力的なゲームを作ることができます。

❗ よくある問題と解決方法

問題1: キャラクターが目標地点を通り過ぎる

原因:移動量の計算でクランプ処理が不適切

解決方法:現在のコードでは既に対応済み。以下の部分で制御しています:

this.player.x += Math.abs(stepX) > Math.abs(dx) ? dx : stepX;

問題2: 移動が滑らかでない

原因:フレームレートが低い、または移動量の計算が不適切

解決方法:delta時間を使用した計算を確認し、適切な速度設定を行う

問題3: クリックが反応しない

原因:イベントリスナーの設定ミス、または座標系の問題

解決方法:以下を確認してください:

  • • イベントリスナーが正しく設定されているか
  • • 座標系(worldX/worldY)が適切か
  • • 他の要素がクリックイベントを妨げていないか

🚀 次のステップ

基本的な移動システムが完成しました!

この章で学んだ移動システムは、多くのゲームの基礎となります。 次章では、この移動システムを活用して、より実践的なゲーム(モグラたたき)を作成していきます。

次章で学べること

  • • ゲームループの実装
  • • スコアシステム
  • • タイマー機能
  • • ゲーム状態の管理

応用発展

  • • 複数キャラクターの移動
  • • パスファインディング
  • • 物理演算の追加
  • • パーティクルエフェクト

📚 まとめ

🎯

学んだこと

  • • スプライト移動の基本概念
  • • クリック/タップイベントの処理
  • • 滑らかな移動の実装
  • • パフォーマンス最適化
🔧

実装技術

  • • PhaserJSの入力システム
  • • update()メソッドの活用
  • • 時間ベースの移動計算
  • • 座標系の理解
🚀

次のステップ

  • • ゲームループの実装
  • • より複雑なゲーム制作
  • • 物理演算の追加
  • • エフェクトの実装

お疲れさまでした!基本的な移動システムが完成しました。

次章では、この移動システムを活用して実際のゲームを作成していきましょう!