Skip to content

物理・衝突判定・タイルマップ

エンジンはノードの AABB 衝突判定に加えて、より高度な物理演算ユーティリティ、タイルマップシステム、パスファインディングを提供します。これらはすべてエンジン非依存の純粋関数モジュールとして設計されており、単体でもテストしやすくなっています。

Physics(物理ユーティリティ)

typescript
import { Physics } from '@uzuhq/engine-2d';

ベクトル演算

2D ベクトルの基本演算です。移動、反射、回転などに使います。

typescript
const v = Physics.vec2(3, 4);
const n = Physics.normalize(v); // { x: 0.6, y: 0.8 }
const d = Physics.distance(posA, posB); // 2 点間の距離
const r = Physics.reflect(velocity, wallNormal); // 壁に反射
関数説明
vec2(x, y)ベクトル生成
add(a, b) / sub(a, b)加算 / 減算
scale(v, s)スカラー倍
length(v) / lengthSq(v)長さ / 長さの二乗
normalize(v)正規化(零ベクトル時は {0,0}
dot(a, b) / cross2D(a, b)内積 / 2D 外積
reflect(v, normal)法線による反射
rotate(v, angle)ラジアン回転
distance(a, b) / distanceSq(a, b)距離 / 距離の二乗
lerp(a, b, t)線形補間

TIP

距離の比較だけなら distanceSq() を使うと平方根の計算を省略できて高速です。

typescript
// 50px 以内かチェック(sqrt 不要)
if (Physics.distanceSq(a, b) < 50 * 50) { ... }

衝突判定

円同士、円 vs 矩形、円 vs 線分、円 vs 回転矩形の判定ができます。すべて CollisionResult { hit, normal, penetration, point } を返します。

typescript
const result = Physics.circleVsCircle({ x: 10, y: 10, radius: 5 }, { x: 15, y: 10, radius: 5 });

if (result.hit) {
  // めり込み解消
  const newPos = Physics.separateCircle(Physics.vec2(10, 10), result.normal, result.penetration);
  // 反射(弾性係数 0.8)
  const newVel = Physics.elasticBounce(velocity, result.normal, 0.8);
}
関数説明
circleVsCircle(a, b)円 vs 円
circleVsLine(circle, line)円 vs 線分
circleVsRect(circle, rect)円 vs AABB
circleVsRotatedRect(circle, rr)円 vs 回転矩形

衝突応答

関数説明
elasticBounce(velocity, normal, restitution)弾性反射ベクトルを計算
separateCircle(position, normal, penetration)めり込みを解消した位置を返す

Overlap(形状判定 + レイキャスト)

typescript
import { Overlap } from '@uzuhq/engine-2d';

Physics よりも広範な形状の組み合わせをサポートします。レイキャストや視線判定も含みます。

形状判定

typescript
// 点が矩形に含まれるか
Overlap.pointInRect({ x: 100, y: 100 }, { x: 50, y: 50, width: 200, height: 200 });

// 円と矩形の衝突
Overlap.circleVsRect({ x: 10, y: 10, radius: 5 }, { x: 0, y: 0, width: 20, height: 20 });

// 凸多角形同士の衝突(SAT アルゴリズム)
Overlap.polygonVsPolygon(
  {
    vertices: [
      { x: 0, y: 0 },
      { x: 10, y: 0 },
      { x: 5, y: 10 },
    ],
  },
  {
    vertices: [
      { x: 5, y: 5 },
      { x: 15, y: 5 },
      { x: 10, y: 15 },
    ],
  },
);
関数説明
pointInRect(p, rect)点 vs AABB
pointInCircle(p, circle)点 vs 円
pointInPolygon(p, poly)点 vs 凸多角形
rectVsRect(a, b)AABB vs AABB
circleVsCircle(a, b)円 vs 円
circleVsRect(circle, rect)円 vs AABB
rectVsCircle(rect, circle)AABB vs 円(引数逆順)
circleVsPolygon(circle, poly)円 vs 凸多角形
polygonVsPolygon(a, b)凸多角形 vs 凸多角形

レイキャスト

レイ(半直線)が特定の形状やタイルマップに当たるかを判定します。弾道の計算や視線判定に使えます。

typescript
// レイ vs 矩形
const hit = Overlap.rayVsRect(
  { origin: { x: 0, y: 5 }, direction: { x: 1, y: 0 } },
  { x: 10, y: 0, width: 10, height: 10 },
);
// → { hit: true, t: 10, point: { x: 10, y: 5 }, normal: { x: -1, y: 0 } }

// レイ vs タイルマップ(DDA アルゴリズム)
const grid = {
  cols: 30,
  rows: 30,
  tileSize: 20,
  isSolid: (c: number, r: number) => tilemap.isSolid(c, r),
};
const tilemapHit = Overlap.rayVsGrid(
  { origin: { x: 50, y: 50 }, direction: { x: 1, y: 0 } },
  grid,
  200, // 最大距離
);

// 2 点間の視線が通るかチェック
const canSee = Overlap.hasLineOfSight({ x: 50, y: 50 }, { x: 200, y: 100 }, grid);
関数説明
rayVsRect(ray, rect, maxDist?)レイ vs AABB
rayVsCircle(ray, circle, maxDist?)レイ vs 円
rayVsLine(ray, line, maxDist?)レイ vs 線分
rayVsGrid(ray, grid, maxDist?)レイ vs タイルマップ(DDA)
hasLineOfSight(from, to, grid)2 点間の視線判定

タイルマップ

グリッドベースのマップを効率的に描画・管理するシステムです。

単層タイルマップ

typescript
import { createTilemap } from '@uzuhq/engine-2d';

const tilemap = createTilemap({
  cols: 20, rows: 15, tileSize: 32,
  tiles: [
    [1, 1, 0, 0, ...], // [row][col] → tileId
    // ...
  ],
  defs: {
    0: { color: '#333', solid: false },  // 地面
    1: { color: '#8b5e3c', solid: true }, // 壁
  },
});

// rawBelow 内でカメラ連動描画
engine.rawBelow((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.render(ctx, cam.x, cam.y, engine.width, engine.height);
});
メソッド説明
render(ctx, camX, camY, viewW, viewH)画面に見える範囲だけ描画
isSolid(col, row)そのタイルが壁かどうか
getTile(col, row) / setTile(col, row, id)タイル ID の取得・変更
worldToTile(wx, wy)ワールド座標 → タイル座標
tileToWorld(col, row)タイル座標 → ワールド座標(タイル中心)

多層タイルマップ(パララックス対応)

遠景・メイン・前景のように複数レイヤーを重ねて、パララックス(視差)スクロールを実現できます。

typescript
import { createLayeredTilemap } from '@uzuhq/engine-2d';

const tilemap = createLayeredTilemap({
  cols: 75,
  rows: 19,
  tileSize: 32,
  layers: [
    { tiles: bgTiles, scrollFactor: 0.3, alpha: 0.6 }, // 遠景(ゆっくり)
    { tiles: mainTiles, scrollFactor: 1.0 }, // メイン
    { tiles: fgTiles, scrollFactor: 1.2, alpha: 0.7 }, // 前景(速め)
  ],
  defs: {/* ... */},
});

// 全レイヤー一括描画
engine.rawBelow((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.render(ctx, cam.x, cam.y, engine.width, engine.height);
});

前景をノードの上に描きたい場合は、レイヤーを個別に描画します。

typescript
// 背景 + メイン(ノードの下)
engine.rawBelow((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.renderLayer(ctx, 0, cam.x, cam.y, engine.width, engine.height);
  tilemap.renderLayer(ctx, 1, cam.x, cam.y, engine.width, engine.height);
});

// 前景(ノードの上)
engine.raw((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.renderLayer(ctx, 2, cam.x, cam.y, engine.width, engine.height);
});
レイヤー設定デフォルト説明
tilesnumber[][]必須[row][col] → tileId
scrollFactornumber1.0パララックス係数(0.5=半速、2.0=倍速)
alphanumber1.0レイヤーの透明度

スプライトアニメーション

スプライトシートからフレームを切り出してアニメーションさせるユーティリティです。

typescript
import { sheetFrames, createAnimator } from '@uzuhq/engine-2d';

// スプライトシートの定義
const sheet = { frameWidth: 16, frameHeight: 16, columns: 8 };

// 各アニメーションのフレーム座標を定義
const idle = sheetFrames(sheet, [
  [0, 0],
  [1, 0],
  [2, 0],
  [3, 0],
]);
const run = sheetFrames(sheet, [
  [0, 1],
  [1, 1],
  [2, 1],
  [3, 1],
]);

// アニメーターを作成
const anim = createAnimator({
  idle: { frames: idle, frameDuration: 0.2, loop: true },
  run: { frames: run, frameDuration: 0.1, loop: true },
});

// 再生するクリップを切り替え
anim.play('run');

// ゲームループでフレームを更新
engine.onUpdate((dt) => {
  anim.update(dt);
  const rect = anim.frame(); // 現在のフレーム矩形
  engine.update('player', { sourceRect: rect });
});
メソッド説明
anim.play(name)アニメーションクリップを切り替え
anim.update(dt)フレームを進める(秒単位の dt)
anim.frame()現在のフレーム矩形を取得
anim.current現在のクリップ名(読み取り専用)

パスファインディング

グリッドベースの A* アルゴリズムによる経路探索です。バイナリヒープで高速化されています。

typescript
import { createPathfinder } from '@uzuhq/engine-2d';

const pf = createPathfinder({
  cols: 30,
  rows: 30,
  isWalkable: (c, r) => !tilemap.isSolid(c, r),
  diagonal: false, // true にすると 8 方向移動(角切り防止付き)
});

経路探索

typescript
const result = pf.findPath(startCol, startRow, goalCol, goalRow);
if (result.found) {
  for (const { col, row } of result.path) {
    // この座標に沿って移動
  }
}

移動範囲の取得

タクティクス RPG の移動範囲表示に使えます。指定コスト以内で到達可能なすべてのマスを返します。

typescript
const reachable = pf.getReachable(unitCol, unitRow, 5);
for (const { col, row, cost } of reachable) {
  // cost 5 以内で到達可能なマスをハイライト
  highlightTile(col, row);
}

設定

プロパティデフォルト説明
cols, rowsnumber必須グリッドサイズ
isWalkable(col, row) => boolean必須通行可能かの判定関数
diagonalbooleanfalse8 方向移動を許可
cost(fromCol, fromRow, toCol, toRow) => number1 / sqrt(2)カスタムコスト関数