↑ページトップへ
表示調整
閉じる
挿絵表示切替ボタン
▼配色
▼行間
▼文字サイズ
▼メニューバー
×閉じる

ブックマークに追加しました

設定
0/400
設定を保存しました
エラーが発生しました
※文字以内
ブックマークを解除しました。

エラーが発生しました。

エラーの原因がわからない場合はヘルプセンターをご確認ください。

ブックマーク機能を使うにはログインしてください。
PR
3/10

第3日目:時間の流れとゲームの世界 - カメラとデルタタイムの実装

こんにちは、蒼井 蓮です。「ゼロから始めるnew.world - AAA級ゲーム開発への道」の第3日目の記録をお届けします。今日はデルタタイムの導入、障害物の追加、そしてカメラシステムの実装に挑戦します。


今日の一日

今朝は珍しく早起きできました。窓から差し込む朝日を浴びながら、コーヒーを一杯。開発の計画を頭の中で整理しつつ、朝食を取りました。私の小さなアパートの作業スペースは決して広くありませんが、ここで生まれるアイデアに制限はありません。


昨晩は少し遅くまでコーディングしていたせいか、ややクタクタでしたが、新しいコードを書く期待感で体が勝手に目覚めてしまいました。近所の猫が窓辺で鳴いているのに気づき、少し遊んであげると、いつものように私の膝の上でくつろぎ始めました。プログラミングの時は必ず傍にいる、非公式の「デバッグアシスタント」です。


今日の目標

デルタタイムの導入

障害物とプラットフォームの実装

カメラシステムの構築

コリジョン応答の改善

デルタタイムの導入

まずは、フレームレートに依存しない動きを実現するために、デルタタイムを導入しました。これにより、どのデバイスでも一貫した速度でゲームが動作するようになります。


// game.js - デルタタイム導入

class Game {

constructor(canvasId) {

this.canvas = document.getElementById(canvasId);

this.ctx = this.canvas.getContext('2d');

this.entities = [];

this.isRunning = false;

this.lastFrameTime = 0;

this.deltaTime = 0;

}


start() {

this.isRunning = true;

this.lastFrameTime = performance.now();

this.gameLoop();

}


gameLoop(currentTime) {

if (!this.isRunning) return;


// デルタタイムの計算 (秒単位)

this.deltaTime = (currentTime - this.lastFrameTime) / 1000;

this.lastFrameTime = currentTime;


// フレームレートが極端に低い場合の対策

// (例えばタブが非アクティブの場合など)

if (this.deltaTime > 0.1) this.deltaTime = 0.1;


// クリア

this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);


// 更新と描画

this.update();

this.render();


// 次のフレーム

requestAnimationFrame((time) => this.gameLoop(time));

}


update() {

// 物理演算と入力処理は別ファイルで実装


// エンティティの更新

for (const entity of this.entities) {

entity.update(this.deltaTime);

}

}


render() {

// エンティティの描画

for (const entity of this.entities) {

entity.render(this.ctx);

}

}


addEntity(entity) {

this.entities.push(entity);

}

}

次に、エンティティの更新処理もデルタタイムを考慮したものに変更しました。


// 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(deltaTime) {

// デルタタイムを考慮した位置の更新

this.x += this.velocityX * deltaTime * 60; // 60FPSを基準とした調整

this.y += this.velocityY * deltaTime * 60;

}


render(ctx) {

ctx.fillStyle = this.color;

ctx.fillRect(this.x, this.y, this.width, this.height);

}

}

物理エンジンも同様にデルタタイムを考慮するように修正しました。


// physics.js - デルタタイム対応版

class Physics {

// ... 前回のコードから継続 ...


update(deltaTime) {

const scaledGravity = GRAVITY * deltaTime * 60;


// 全エンティティに重力を適用

for (const entity of this.entities) {

if (!entity.isGrounded) {

entity.velocityY += scaledGravity;

}


// ... 以下略 ...

}


// エンティティ同士の衝突判定

this.checkCollisions();

}


// ... 以下略 ...

}

コーヒーを一杯飲み干した後、実際にコードを走らせてデルタタイムの効果を確認しました。フレームレートを意図的に変動させても、キャラクターの動きが一貫していることを確認できました。これは特に、様々なデバイスでゲームを動作させる場合に重要です。


障害物とプラットフォームの実装

午前中の残りの時間は、ゲームに障害物とプラットフォームを追加することに費やしました。これにより、シンプルなキャラクターが動くだけのデモから、実際にプレイ可能なゲームへと一歩近づきます。


// platform.js - プラットフォームクラス

class Platform extends Entity {

constructor(x, y, width, height) {

super(x, y, width, height, 'green');

this.isStatic = true; // 物理演算の影響を受けない

}


update(deltaTime) {

// 静的オブジェクトは位置の更新が不要

// ただし、動くプラットフォームを実装する場合は

// ここにカスタムの動きのロジックを追加

}

}


// obstacle.js - 障害物クラス

class Obstacle extends Entity {

constructor(x, y, width, height) {

super(x, y, width, height, 'red');

this.isStatic = true;

this.isDamaging = true; // プレイヤーにダメージを与える

}


update(deltaTime) {

// 静的障害物のため更新不要

}

}

これらのオブジェクトをゲームに追加し、物理エンジンでの衝突判定も対応させました。


// main.js - プラットフォームと障害物の追加

// ... 前回のコードから継続 ...


// プラットフォームの追加

const platforms = [

new Platform(100, 400, 200, 20),

new Platform(400, 350, 150, 20),

new Platform(600, 300, 150, 20),

new Platform(300, 250, 100, 20),

new Platform(500, 200, 100, 20),

];


// 障害物の追加

const obstacles = [

new Obstacle(350, 530, 100, 20),

new Obstacle(550, 280, 30, 20),

];


// ゲームと物理エンジンにオブジェクトを追加

for (const platform of platforms) {

game.addEntity(platform);

physics.addEntity(platform);

}


for (const obstacle of obstacles) {

game.addEntity(obstacle);

physics.addEntity(obstacle);

}

昼食のためにキッチンへ向かう前に、ゲームをテストしました。プレイヤーがプラットフォームの上に乗り、ジャンプして別のプラットフォームに移動できるようになりました。障害物に触れるとコンソールに警告メッセージが表示されるようにしました(後でダメージ処理に発展させる予定)。


昼食は簡単なサンドイッチで済ませました。開発に夢中になると、つい食事のことを忘れがちです。「デバッグアシスタント」の猫も私の足元で昼寝をしながら、時々目を開けて私の様子を窺っていました。


カメラシステムの実装

午後からは、プレイヤーを追従するカメラシステムの実装に取り組みました。これにより、より広いゲーム世界を探索できるようになります。


// camera.js - カメラシステム

class Camera {

constructor(width, height, worldWidth, worldHeight) {

this.x = 0;

this.y = 0;

this.width = width;

this.height = height;

this.worldWidth = worldWidth;

this.worldHeight = worldHeight;

this.target = null; // 追従するターゲット

this.smoothFactor = 0.1; // カメラの追従スムーズさ (0-1)

}


setTarget(entity) {

this.target = entity;

}


update(deltaTime) {

if (!this.target) return;


// ターゲットが中央に来るようにカメラ位置を計算

const targetX = this.target.x + this.target.width / 2 - this.width / 2;

const targetY = this.target.y + this.target.height / 2 - this.height / 2;


// スムーズな追従

this.x += (targetX - this.x) * this.smoothFactor * deltaTime * 60;

this.y += (targetY - this.y) * this.smoothFactor * deltaTime * 60;


// カメラが世界の境界を超えないようにする

this.x = Math.max(0, Math.min(this.x, this.worldWidth - this.width));

this.y = Math.max(0, Math.min(this.y, this.worldHeight - this.height));

}


// 世界座標をカメラ相対座標に変換

worldToScreen(x, y) {

return {

x: x - this.x,

y: y - this.y

};

}

}

次に、ゲームのレンダリング処理をカメラを考慮したものに更新しました。


// game.js - カメラを考慮したレンダリング

class Game {

// ... 前回のコードから継続 ...


constructor(canvasId) {

// ... 以前のコード ...

this.camera = new Camera(

canvas.width,

canvas.height,

WORLD_WIDTH,

WORLD_HEIGHT

);

}


setCamera(camera) {

this.camera = camera;

}


setCameraTarget(entity) {

this.camera.setTarget(entity);

}


update() {

// カメラの更新

this.camera.update(this.deltaTime);


// ... 以前のコード ...

}


render() {

// 背景の描画

this.ctx.fillStyle = 'skyblue';

this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);


// 地面の描画

this.ctx.fillStyle = 'sienna';

const groundPos = this.camera.worldToScreen(0, GROUND_LEVEL);

this.ctx.fillRect(0, groundPos.y, this.canvas.width, this.canvas.height - groundPos.y);


// エンティティの描画(カメラ相対座標に変換)

this.ctx.save();

this.ctx.translate(-this.camera.x, -this.camera.y);


for (const entity of this.entities) {

entity.render(this.ctx);

}


this.ctx.restore();


// UI要素の描画(カメラに影響されない)

this.drawUI();

}


drawUI() {

// 今後ここにスコア、体力ゲージなどのUI要素を描画

this.ctx.fillStyle = 'white';

this.ctx.font = '20px Arial';

this.ctx.fillText(`FPS: ${Math.round(1 / this.deltaTime)}`, 10, 30);

}

}

カメラシステムを実装した後、窓の外を眺めると、夕日が沈み始めていました。一日がこんなに早く過ぎるとは思いませんでした。デスクに戻り、作業を続けました。


コリジョン応答の改善

最後に、コリジョン応答を改善して、より自然な衝突と反発を実現しました。


// physics.js - 改良版コリジョン応答

class Physics {

// ... 前回のコードから継続 ...


resolveCollision(a, b) {

// 衝突が検出された場合の応答


// 両方が動的オブジェクトでない場合の処理

if (a.isStatic && b.isStatic) return;


// 静的オブジェクトと動的オブジェクトの衝突

let staticObj, dynamicObj;

if (a.isStatic) {

staticObj = a;

dynamicObj = b;

} else if (b.isStatic) {

staticObj = b;

dynamicObj = a;

}


if (staticObj && dynamicObj) {

// 静的オブジェクトと動的オブジェクトの衝突処理

this.resolveStaticCollision(staticObj, dynamicObj);

return;

}


// 両方が動的オブジェクトの場合の処理

this.resolveDynamicCollision(a, b);

}


resolveStaticCollision(staticObj, dynamicObj) {

// 静的オブジェクトと動的オブジェクトの衝突応答


// 重なりの計算

const overlapX = Math.min(staticObj.x + staticObj.width, dynamicObj.x + dynamicObj.width)

- Math.max(staticObj.x, dynamicObj.x);

const overlapY = Math.min(staticObj.y + staticObj.height, dynamicObj.y + dynamicObj.height)

- Math.max(staticObj.y, dynamicObj.y);


// X軸かY軸のどちらで調整するか決定(小さい方を選択)

if (overlapX < overlapY) {

// X軸方向の調整

if (dynamicObj.x < staticObj.x) {

// 動的オブジェクトが左から衝突

dynamicObj.x = staticObj.x - dynamicObj.width;

} else {

// 動的オブジェクトが右から衝突

dynamicObj.x = staticObj.x + staticObj.width;

}


// X方向の速度を反転

dynamicObj.velocityX *= -0.3; // 反発係数

} else {

// Y軸方向の調整

if (dynamicObj.y < staticObj.y) {

// 動的オブジェクトが上から衝突

dynamicObj.y = staticObj.y - dynamicObj.height;

dynamicObj.isGrounded = true;

dynamicObj.velocityY = 0;

} else {

// 動的オブジェクトが下から衝突

dynamicObj.y = staticObj.y + staticObj.height;

dynamicObj.velocityY *= -0.3; // 反発係数

}

}


// 障害物の特殊処理

if (staticObj.isDamaging) {

console.log("プレイヤーがダメージを受けました!"); // 後でダメージ処理に置き換え

}

}


resolveDynamicCollision(a, b) {

// 2つの動的オブジェクト間の衝突応答(運動量保存)

// ... 運動量保存則に基づいた衝突応答のコード ...

// 今回は簡略化のため、詳細は省略

}

}

夜も更けてきた頃、最後のコーディングを終え、ゲームを実行してみました。プレイヤーがプラットフォームの上をスムーズに移動し、カメラが追従する様子は、小さいながらも大きな進歩を感じさせるものでした。


不思議な出来事 - 再び

今日もまた、不思議な出来事がありました。深夜の作業中、画面上のプレイヤーキャラクターが一瞬だけ私の入力とは無関係に動いたように感じたのです。昨日も同じような現象があったので、単なる疲労からくる錯覚ではないかもしれません。


念のために、入力処理のコードをチェックしましたが、特に問題は見つかりませんでした。おそらくWebブラウザの何らかの不具合か、単に私の目の錯覚でしょう。とはいえ、少し気になるので明日も注意してみようと思います。


夜遅くになって猫が私の作業を邪魔し始めたので、今日はここまでにすることにしました。体調管理も開発の一部ですからね。


明日の計画

明日は以下の機能を実装する予定です:


敵キャラクターの追加と基本的なAI

プレイヤーの体力システムとダメージ処理

簡単なアニメーション機能

音響効果の実装

まとめと日常の振り返り

3日目にして、ゲームらしい形が見えてきました。フレームレートに依存しない動き、プラットフォームのある世界、そしてカメラシステムの実装により、単なるデモから本格的なゲームへの第一歩を踏み出せたと感じています。


今日は開発に没頭するあまり、食事がサンドイッチだけになってしまいました。明日はもう少しバランスの取れた食事を心がけようと思います。開発は楽しいですが、健康あってこそですからね。


また、友人から受けていた別のプロジェクトの相談メールにも返信しなければなりません。new.worldでの開発に夢中になっていましたが、フリーランスとしての他の仕事もきちんとこなさなければいけませんね。


明日もまた、new.worldでの冒険を続けます。開発の進捗と共に、このブログも成長していければと思います。


それでは、おやすみなさい。


蒼井 蓮

作品に感心したり、次の展開を待ち望んでいる方は、ブックマークや評価をしていただけると幸いです。


作品に魅力を感じなかった方も、お手数ですが評価でご感想をお聞かせください。

評価をするにはログインしてください。
ブックマークに追加
ブックマーク機能を使うにはログインしてください。
― 新着の感想 ―
このエピソードに感想はまだ書かれていません。
感想一覧
+注意+

特に記載なき場合、掲載されている作品はすべてフィクションであり実在の人物・団体等とは一切関係ありません。
特に記載なき場合、掲載されている作品の著作権は作者にあります(一部作品除く)。
作者以外の方による作品の引用を超える無断転載は禁止しており、行った場合、著作権法の違反となります。