第2日目:プレイヤーと物理の実装 - ゲーム世界の基盤づくり
こんにちは、蒼井 蓮です。「ゼロから始めるnew.world - AAA級ゲーム開発への道」の第2日目の記録をお届けします。昨日は基本的なゲームループの実装まで行いましたが、今日はそこにプレイヤーキャラクターと簡単な物理エンジンを追加していきたいと思います。
今日の目標
プレイヤーキャラクターの実装
基本的な物理エンジンの実装
キーボード入力の処理
プレイヤーキャラクターの実装
まずは、ゲームの主人公となるプレイヤーキャラクターを実装しました。今はシンプルな四角形ですが、これから徐々に発展させていく予定です。
// entity.js - 基本エンティティクラス
class Entity {
constructor(x, y, width, height, color) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
this.velocityX = 0;
this.velocityY = 0;
}
update() {
// 基本的な更新処理
this.x += this.velocityX;
this.y += this.velocityY;
}
render(ctx) {
// 基本的な描画処理
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
// player.js - プレイヤーキャラクタークラス
class Player extends Entity {
constructor(x, y) {
super(x, y, 40, 40, 'blue');
this.speed = 5;
this.jumpForce = 10;
this.isJumping = false;
this.isGrounded = false;
}
moveLeft() {
this.velocityX = -this.speed;
}
moveRight() {
this.velocityX = this.speed;
}
stop() {
this.velocityX = 0;
}
jump() {
if (this.isGrounded) {
this.velocityY = -this.jumpForce;
this.isGrounded = false;
this.isJumping = true;
}
}
update() {
super.update();
// 重力の適用
if (!this.isGrounded) {
this.velocityY += GRAVITY;
}
// 画面外に出ないようにする
if (this.x < 0) this.x = 0;
if (this.x + this.width > CANVAS_WIDTH) this.x = CANVAS_WIDTH - this.width;
// 地面との衝突判定
if (this.y + this.height > GROUND_LEVEL) {
this.y = GROUND_LEVEL - this.height;
this.velocityY = 0;
this.isGrounded = true;
this.isJumping = false;
}
}
}
このコードでは、基本的なエンティティクラスを定義し、それを継承したプレイヤークラスを実装しています。プレイヤーは左右移動とジャンプの機能を持ち、重力の影響も受けるようになっています。
簡易物理エンジンの実装
次に、シンプルな物理エンジンの基盤を追加しました。まずは重力と衝突判定から始めています。
// physics.js - シンプルな物理エンジン
const GRAVITY = 0.5;
const FRICTION = 0.8;
const GROUND_LEVEL = 550; // キャンバスの下部付近
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
class Physics {
constructor() {
this.entities = [];
}
addEntity(entity) {
this.entities.push(entity);
}
update() {
// 全エンティティに重力を適用
for (const entity of this.entities) {
if (!entity.isGrounded) {
entity.velocityY += GRAVITY;
}
// 地面との衝突判定
if (entity.y + entity.height > GROUND_LEVEL) {
entity.y = GROUND_LEVEL - entity.height;
entity.velocityY = 0;
entity.isGrounded = true;
// 摩擦の適用
entity.velocityX *= FRICTION;
// 非常に小さい速度はゼロにする(数値の安定化)
if (Math.abs(entity.velocityX) < 0.1) {
entity.velocityX = 0;
}
}
}
// エンティティ同士の衝突判定
this.checkCollisions();
}
checkCollisions() {
for (let i = 0; i < this.entities.length; i++) {
for (let j = i + 1; j < this.entities.length; j++) {
const entityA = this.entities[i];
const entityB = this.entities[j];
// AABB衝突判定(Axis-Aligned Bounding Box)
if (this.isColliding(entityA, entityB)) {
this.resolveCollision(entityA, entityB);
}
}
}
}
isColliding(a, b) {
// 矩形同士の衝突判定
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
resolveCollision(a, b) {
// 簡易的な衝突応答(位置の調整のみ)
// 将来的には運動量保存などの物理法則に基づいた応答に発展させる
// 重なりの計算
const overlapX = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
const overlapY = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
// X軸かY軸のどちらで調整するか決定(小さい方を選択)
if (overlapX < overlapY) {
if (a.x < b.x) {
a.x -= overlapX;
} else {
a.x += overlapX;
}
// 速度の反転(単純な反射)
a.velocityX *= -0.5;
b.velocityX *= -0.5;
} else {
if (a.y < b.y) {
a.y -= overlapY;
a.isGrounded = true;
} else {
a.y += overlapY;
b.isGrounded = true;
}
// 速度の反転(単純な反射)
a.velocityY *= -0.5;
b.velocityY *= -0.5;
}
}
}
この物理エンジンは、重力、摩擦、地面との衝突、そしてエンティティ同士の衝突を処理します。現時点では簡易的な実装ですが、将来的には運動量保存則や材質による反発係数なども考慮した、より本格的な物理シミュレーションに発展させる予定です。
キーボード入力の処理
最後に、プレイヤーをコントロールするためのキーボード入力処理を実装しました。
// input.js - キーボード入力の処理
class InputHandler {
constructor(player) {
this.player = player;
this.keys = {};
// キーボードイベントの登録
window.addEventListener('keydown', (e) => this.keyDown(e));
window.addEventListener('keyup', (e) => this.keyUp(e));
}
keyDown(e) {
this.keys[e.code] = true;
}
keyUp(e) {
this.keys[e.code] = false;
}
update() {
// 左右移動
if (this.keys['ArrowLeft'] || this.keys['KeyA']) {
this.player.moveLeft();
} else if (this.keys['ArrowRight'] || this.keys['KeyD']) {
this.player.moveRight();
} else {
this.player.stop();
}
// ジャンプ
if (this.keys['ArrowUp'] || this.keys['KeyW'] || this.keys['Space']) {
this.player.jump();
}
}
}
このInputHandlerクラスは、キーボードの入力を監視し、対応するプレイヤーの操作に変換します。矢印キーやWASDキー、スペースキーなど、一般的なゲームコントロールに対応しています。
すべてを統合する
最後に、これらのコンポーネントをすべて統合して、ゲームの基本構造を完成させました。
// main.js - メインゲーム処理
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// キャンバスサイズの設定
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// ゲームオブジェクトの初期化
const game = new Game(canvas);
const physics = new Physics();
const player = new Player(100, 100);
const inputHandler = new InputHandler(player);
// 物理エンジンにプレイヤーを追加
physics.addEntity(player);
// ゲームにプレイヤーを追加
game.addEntity(player);
// メインゲームループの拡張
game.update = function() {
// 入力処理
inputHandler.update();
// 物理演算
physics.update();
// エンティティの更新
for (const entity of this.entities) {
entity.update();
}
};
// ゲーム開始
game.start();
実行結果と気づき
実際に実装したコードを実行してみると、青い四角形のプレイヤーが画面に表示され、キーボードで左右に移動したりジャンプしたりできるようになりました。地面との衝突も正しく処理されています。
今日の実装を通じて、いくつかの重要な気づきがありました:
コードの構造化の重要性:ゲーム開発では、コードをきちんと構造化することが非常に重要です。今回はEntity、Player、Physics、InputHandlerなどのクラスに分けることで、それぞれの責任を明確にしました。これはボルト君からのアドバイスもあり、実践してみました。
デバッグの難しさ:物理エンジンの実装中、エンティティが予期せぬ動きをすることがありました。このようなバグは視覚的に確認できるため発見は容易ですが、原因の特定には時間がかかることがあります。new.worldのコンソール出力とデバッグ機能が役立ちました。
フレームレート依存の問題:現在の実装では、ゲームの速度がフレームレートに依存しています。これは理想的ではないので、明日はデルタタイム(前回のフレームからの経過時間)を導入して、フレームレートに依存しない動きを実装する予定です。
不思議な出来事
今日、コーディングに没頭していると、ふと画面上のプレイヤーキャラクター(現時点では単なる青い四角形)が、私の入力とは無関係に一瞬だけ動いたように感じました。おそらく疲れからくる錯覚でしょうが、まるで四角形に意識があるかのような錯覚を覚えました。長時間のコーディングの影響かもしれませんね(笑)
明日の計画
明日は以下の機能を実装する予定です:
デルタタイムを導入して、フレームレートに依存しない動きを実現
簡単な障害物やプラットフォームの追加
カメラシステムの実装(プレイヤーを追従するスクロール機能)
衝突応答の改善(反発係数や摩擦係数の考慮)
まとめと質問
2日目にしてかなり基本的なゲームの形が見えてきました。プレイヤーキャラクターが動き、物理法則に従って振る舞う様子は、単純ながらも達成感があります。これがAAA級ゲームへの第一歩です。
今日も質問です:物理エンジンについて、より詳しく知りたい部分はありますか?衝突判定の最適化、流体シミュレーション、布シミュレーションなど、物理の分野は幅広いですが、特に興味のある分野があれば教えてください。次回以降で取り上げてみたいと思います。
明日もまた、新たな発見と進化をお届けします!
蒼井 蓮




