テーマ
物理・衝突判定・タイルマップ
エンジンはノードの 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);
});| レイヤー設定 | 型 | デフォルト | 説明 |
|---|---|---|---|
tiles | number[][] | 必須 | [row][col] → tileId |
scrollFactor | number | 1.0 | パララックス係数(0.5=半速、2.0=倍速) |
alpha | number | 1.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, rows | number | 必須 | グリッドサイズ |
isWalkable | (col, row) => boolean | 必須 | 通行可能かの判定関数 |
diagonal | boolean | false | 8 方向移動を許可 |
cost | (fromCol, fromRow, toCol, toRow) => number | 1 / sqrt(2) | カスタムコスト関数 |