慣性と速度——ものが動き続ける理由
新しいシリーズへようこそ。前シリーズではビジュアルコーディングの基礎——ループ・三角関数・アニメーション——を学びました。今度は一歩踏み込んで、物理シミュレーションの世界に入ります。
物理シミュレーションとは何か。一言で言えば、「数学で世界のルールを再現する」ことです。重力・バネ・衝突——現実で当たり前のように起きる現象を、コードで書き起こすことで、動くボール、揺れるロープ、跳ね返る矩形を作れるようになります。
このシリーズを通して、ゲームエンジンや物理エンジン(Box2D, Matter.js)の中で何が起きているのかが見えてくるはずです。まずは最もシンプルな概念——慣性と速度から始めましょう。
物理シミュレーションの基本構造
物理シミュレーションは、次の3ステップを毎フレーム繰り返す構造になっています。
1. 状態を更新する(位置・速度を変える)
2. 画面をクリアする
3. 現在の状態で描画する
→ 次のフレームへ
「状態(state)」とは、シミュレーションの世界にある物体の情報です。最もシンプルな物体は位置と速度の4変数で表せます。
var x = 100; // X座標
var y = 150; // Y座標
var vx = 80; // X方向の速度(ピクセル/秒)
var vy = 30; // Y方向の速度(ピクセル/秒)
位置は「今どこにいるか」、速度は「どの方向にどれだけ速く動いているか」を表します。
deltaTime(dt)の重要性
アニメーションの更新には requestAnimationFrame を使いますが、フレームレートは端末によってまちまちです。60fpsのPCもあれば120fpsのモニターも、30fpsまでしか出ない状況もあります。
速度を「1フレームあたりのピクセル」で書いてしまうと、端末が違うだけで動きの速さが変わってしまいます。これを避けるためにdeltaTime(dt)を使います。
var lastTime = 0;
function loop(timestamp) {
var dt = (timestamp - lastTime) / 1000; // ミリ秒→秒に変換
lastTime = timestamp;
// 速度はピクセル/秒で定義する
x += vx * dt;
y += vy * dt;
requestAnimationFrame(loop);
}
dt を掛けることで、「1秒間に何ピクセル動くか」が一定になります。60fpsでも30fpsでも、1秒後には同じ位置にいます。これは物理シミュレーションの基礎中の基礎です。
更新則: x += vx * dt
速度を使った位置の更新式を「オイラー積分」と呼びます(詳しくはepisode 8で扱います)。
x_new = x + vx * dt
y_new = y + vy * dt
これは微積分の「位置は速度を積分したもの」という考え方を離散化したものです。dtを無限小にすれば完全に正確になりますが、コンピューターで動かす以上は有限のdtを使うしかありません。それでも十分小さければ(60fpsなら約0.016秒)、実用上は十分な精度が得られます。
摩擦なしで永遠に動くボール
現実では、物体は空気抵抗や摩擦で徐々に遅くなります。しかしコードに何も書かなければ、速度は永遠に変わりません。これが慣性の法則(ニュートンの第一法則)です。
外力が働かない限り、静止しているものは静止し続け、動いているものは等速直線運動を続ける。
まずは摩擦なし——慣性だけで動くボールを作ってみます。壁に当たったら跳ね返る処理も入れます。
var x = 30, y = H / 2;
var vx = 200, vy = 0;
var r = 18;
var lastTime = null;
function loop(ts) {
if (!lastTime) lastTime = ts;
var dt = Math.min((ts - lastTime) / 1000, 0.05);
lastTime = ts;
x += vx * dt;
y += vy * dt;
if (x - r < 0) { x = r; vx = Math.abs(vx); }
if (x + r > W) { x = W - r; vx = -Math.abs(vx); }
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = '#00e5ff';
ctx.fill();
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(0, y - 1, W, 2);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 横方向だけに速度があるので、水平に等速で動き続けます。壁に当たると速度の符号が反転して跳ね返ります。
複数のボールが各方向に等速移動
複数の物体を配列で管理するのが物理シミュレーションの基本パターンです。各ボールはそれぞれ独立した状態変数を持ちます。
var balls = [
{ x: 50, y: 80, vx: 150, vy: 80 },
{ x: 200, y: 120, vx: -90, vy: 110 },
{ x: 350, y: 60, vx: 70, vy: -130 },
];
更新ループでは balls.forEach を使って全ボールをまとめて処理します。
var balls = [];
for (var i = 0; i < 8; i++) {
var angle = (i / 8) * Math.PI * 2;
balls.push({
x: W / 2,
y: H / 2,
vx: Math.cos(angle) * 120,
vy: Math.sin(angle) * 120,
hue: i * 45
});
}
var lastTime = null;
function loop(ts) {
if (!lastTime) lastTime = ts;
var dt = Math.min((ts - lastTime) / 1000, 0.05);
lastTime = ts;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
balls.forEach(function(b) {
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.x < 10) { b.x = 10; b.vx = Math.abs(b.vx); }
if (b.x > W - 10) { b.x = W - 10; b.vx = -Math.abs(b.vx); }
if (b.y < 10) { b.y = 10; b.vy = Math.abs(b.vy); }
if (b.y > H - 10) { b.y = H - 10; b.vy = -Math.abs(b.vy); }
ctx.beginPath();
ctx.arc(b.x, b.y, 10, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + b.hue + ',85%,60%)';
ctx.fill();
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 8方向に等速で飛び出し、壁で跳ね返り続けます。どのボールも速さは変わりません——これが慣性です。
デバッグ表示: 速度ベクトルの可視化
物理シミュレーションを作るとき、「今の速度はどの方向にどれだけか」を視覚的に確認できると非常に便利です。速度ベクトルを矢印として描く習慣を身につけましょう。
// 速度ベクトルを矢印として描く
function drawVelocityArrow(x, y, vx, vy) {
var scale = 0.2; // 速度の表示スケール
var ex = x + vx * scale;
var ey = y + vy * scale;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(ex, ey);
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 2;
ctx.stroke();
// 矢印の頭
var angle = Math.atan2(ey - y, ex - x);
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - 8 * Math.cos(angle - 0.4), ey - 8 * Math.sin(angle - 0.4));
ctx.lineTo(ex - 8 * Math.cos(angle + 0.4), ey - 8 * Math.sin(angle + 0.4));
ctx.closePath();
ctx.fillStyle = '#ffff00';
ctx.fill();
}
var balls = [];
for (var i = 0; i < 5; i++) {
var angle = (i / 5) * Math.PI * 2;
var speed = 80 + i * 30;
balls.push({
x: W / 2 + Math.cos(angle) * 80,
y: H / 2 + Math.sin(angle) * 60,
vx: Math.cos(angle + 0.5) * speed,
vy: Math.sin(angle + 0.5) * speed,
hue: i * 72
});
}
var lastTime = null;
function drawArrow(x, y, vx, vy) {
var scale = 0.25;
var ex = x + vx * scale;
var ey = y + vy * scale;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(ex, ey);
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 2;
ctx.stroke();
var angle = Math.atan2(ey - y, ex - x);
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - 8 * Math.cos(angle - 0.4), ey - 8 * Math.sin(angle - 0.4));
ctx.lineTo(ex - 8 * Math.cos(angle + 0.4), ey - 8 * Math.sin(angle + 0.4));
ctx.closePath();
ctx.fillStyle = '#ffff00';
ctx.fill();
}
function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
balls.forEach(function(b) {
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.x < 14) { b.x = 14; b.vx = Math.abs(b.vx); }
if (b.x > W - 14) { b.x = W - 14; b.vx = -Math.abs(b.vx); }
if (b.y < 14) { b.y = 14; b.vy = Math.abs(b.vy); }
if (b.y > H - 14) { b.y = H - 14; b.vy = -Math.abs(b.vy); }
ctx.beginPath();
ctx.arc(b.x, b.y, 14, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + b.hue + ',80%,55%)';
ctx.fill();
drawArrow(b.x, b.y, b.vx, b.vy);
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 黄色の矢印が速度ベクトルです。矢印の向きが進行方向、長さが速さに比例しています。壁で跳ね返った瞬間に矢印の向きが変わるのが確認できます。
まとめ
この回でやったこと:
- 物理シミュレーションの基本構造(更新 → クリア → 描画)を理解した
- 状態変数
x,y,vx,vyで物体を表現した dt(deltaTime)を使ってフレームレート非依存の更新則x += vx * dtを実装した- 複数の物体を配列で管理するパターンを習得した
- 速度ベクトルを矢印で可視化するデバッグ手法を学んだ
次回は「力と加速度」を実装します。ニュートンの第二法則 F = ma をコードで書けるようになると、重力・空気抵抗・バネといったあらゆる力が扱えるようになります。