npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uzuhq/engine-2d

v1.1.1

Published

UZU 2D Canvas Game Engine

Readme

@uzuhq/engine-2d

UZU 上で動く 2D ゲームのための Canvas ベースゲームエンジン。レスポンシブ画面サイズ対応、DPI 自動対応。

npm install @uzuhq/engine-2d

プロジェクトの雛形は @uzuhq/code-cliuzu create-2d-game で作成できます。マルチプレイ通信・サウンドには @uzuhq/code-sdk を併用してください。

画面サイズ

重要: ゲームコードで画面サイズをハードコードしてはいけない。必ず engine.width / engine.height を使う。

エンジンはレスポンシブモードで動作し、画面サイズは端末によって変わる:

  • engine.width: 現在の論理幅(portrait モードでは固定値 390)
  • engine.height: 現在の論理高さ(端末の画面比率により 640〜844 で変動)
// ✅ 正しい — engine.width / engine.height を使う
const centerX = engine.width / 2;
const groundY = engine.height - 60;
engine.add('btn', {
  type: 'rect',
  x: 30,
  y: engine.height - 80,
  width: engine.width - 60,
  height: 50,
});

// ❌ 間違い — 数値をハードコードしない
const centerX = 195; // NG
const groundY = 784; // NG

エンジン設定(開発者が指定)

// デフォルト: 縦画面(何も指定しなくてもこの設定になる)
createEngine({ canvas });

// 明示的に縦画面
createEngine({ canvas, orientation: 'portrait', baseSize: 390, maxLength: 844 });

// 横画面
createEngine({ canvas, orientation: 'landscape', baseSize: 390, maxLength: 844 });

// 固定サイズ(レスポンシブ無効、互換用)
createEngine({ canvas, width: 600, height: 600 });

| 設定 | 型 | デフォルト | 説明 | | ------------- | --------------------------- | ------------ | --------------------------------------------- | | orientation | 'portrait' \| 'landscape' | 'portrait' | 画面の向き | | baseSize | number | 390 | 固定軸のサイズ(portrait=幅、landscape=高さ) | | maxLength | number | 844 | 可変軸の上限 |

レイアウトの指針

| 要素 | 配置方法 | | -------------------- | --------------------------------------------- | | 上端固定(スコア等) | y: 20 | | 下端固定(ボタン等) | y: engine.height - 80 | | 中央 | x: engine.width / 2, y: engine.height / 2 | | 全幅ボタン | x: 30, width: engine.width - 60 | | 画面外から生成 | y: engine.height + 50(下端外) |


Quick Start — 最小ゲーム

export function startMyGame(engine: Engine) {
  const W = engine.width;
  const H = engine.height;
  let score = 0;

  engine.addMany([
    {
      id: 'score',
      type: 'text',
      x: W / 2,
      y: 30,
      text: '0',
      font: 'bold 24px sans-serif',
      fill: '#fff',
      align: 'center',
    },
    {
      id: 'target',
      type: 'circle',
      x: W / 2,
      y: H / 2,
      radius: 30,
      fill: '#ef4444',
      onClick: () => {
        score++;
        engine.update('score', { text: String(score) });
        engine.animate(
          'target',
          {
            x: 50 + Math.random() * (W - 100),
            y: 100 + Math.random() * (H - 200),
          },
          { duration: 200, easing: 'easeOutBack' },
        );
      },
    },
  ]);
}

パターン: Engine を受け取る → engine.width/engine.height で座標計算 → scene() でノード配置 → イベントで状態更新 → update() / animate() でUI反映。 全ゲームがこの構造に従う。


初期化

createEngine(config: EngineConfig): Engine

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

// 最小構成(assets 省略可)
const engine = createEngine({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
});
await engine.ready; // 初期化完了を待つ(assets の有無に関わらず必須)
// 画像アセット付き
const engine = createEngine({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: 600,
  height: 600,
  background: '#1a1a2e',
  assets: {
    images: { player: '/sprites/player.png' },
  },
});
await engine.ready; // アセットロード完了を待つ

EngineConfig:

| プロパティ | 型 | デフォルト | 説明 | | --------------- | ------------------------ | ------------- | ------------------------- | | canvas | HTMLCanvasElement | 必須 | 描画先 canvas | | width | number | canvas.width | デザイン幅 | | height | number | canvas.height | デザイン高さ | | background | string | '#000' | 背景色 | | assets.images | Record<string, string> | — | 初期ロード画像 {key: url} |


ノード管理

ゲームオブジェクトは「ノード」として管理。6種類: rect, circle, text, sprite, line, nineslice。

engine.add(id, config)

ノードを追加。

engine.add('player', { type: 'rect', x: 100, y: 200, width: 30, height: 30, fill: '#3b82f6' });
engine.add('enemy', { type: 'circle', x: 300, y: 100, radius: 20, fill: '#ef4444' });
engine.add('label', {
  type: 'text',
  x: 300,
  y: 50,
  text: 'Score: 0',
  font: 'bold 16px sans-serif',
  fill: '#fff',
  align: 'center',
});
// リッチテキスト(インラインカラー)
engine.add('dialog', {
  type: 'text',
  x: 50,
  y: 400,
  text: '{c:#7c3aed}長老{/c}「ようこそ」',
  font: '16px sans-serif',
  fill: '#fff',
  richText: true,
});
engine.add('ship', {
  type: 'sprite',
  x: 100,
  y: 100,
  image: 'player',
  width: 32,
  height: 32,
  anchor: { x: 0.5, y: 0.5 },
});
// line: (x, y) から (x2, y2) へ線を引く。x2/y2 は設計座標上の絶対位置。
engine.add('wall', { type: 'line', x: 0, y: 300, x2: 600, y2: 300, stroke: '#fff', lineWidth: 2 });
// 斜め線: (100, 50) → (200, 150)
engine.add('diag', { type: 'line', x: 100, y: 50, x2: 200, y2: 150, stroke: '#ff0', lineWidth: 2 });

engine.update(id, props)

ノードのプロパティを更新。

engine.update('player', { x: 150, y: 250 });
engine.update('label', { text: 'Score: 100' });

engine.remove(id)

ノードを削除(子ノードも再帰的に削除)。

engine.get(id): NodeConfig & { id } | null

ノードの現在の状態を取得。

engine.clear()

全ノード・コールバック・タイマーをクリア。シーン遷移時に自動で呼ばれる。

engine.addMany(nodes)

ノード配列を一括追加。

engine.addMany([
  { id: 'bg', type: 'rect', x: 0, y: 0, width: 600, height: 600, fill: '#000' },
  { id: 'title', type: 'text', x: 300, y: 100, text: 'Hello', fill: '#fff', align: 'center' },
]);

engine.addMany(nodes)

scene() と同じ。追加時にクリアしない。

共通ノードプロパティ(NodeBase):

| プロパティ | 型 | デフォルト | 説明 | | ------------------ | -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ | | x, y | number | 0 | 位置 | | alpha | number | 1 | 透明度 (0-1) | | scaleX, scaleY | number | 1 | 拡大率 | | rotation | number | 0 | 回転 (ラジアン) | | flipX | boolean | false | 水平反転 | | flipY | boolean | false | 垂直反転 | | tint | string | — | カラーティント(CSS color, e.g. '#ff0000') | | tintAmount | number | 0.5 | ティント強度 (0-1)。animate で補間可能 | | visible | boolean | true | 表示/非表示 | | tag | string | — | タグ(グループ化用) | | clipRect | {x,y,width,height} | — | クリッピング領域(ローカル座標)。子ノードにも適用 | | parent | string | — | 親ノードID | | onClick | () => void | — | クリック/タップ時 | | onDown | () => void | — | ポインタダウン時 | | onUp | () => void | — | ポインタアップ時 | | blockInput | boolean | false | true にすると onClick 等が未設定でもヒットテスト対象になり、背面ノードへのイベント貫通を阻止する。モーダルオーバーレイ等に使用 |

TextConfig 追加プロパティ:

| プロパティ | 型 | デフォルト | 説明 | | ---------- | -------------------- | ---------- | ------------------------------------------------------------------- | | baseline | CanvasTextBaseline | 'top' | 垂直方向のテキスト基準線。'middle' でボタン等の垂直中央揃えが可能 | | richText | boolean | false | インラインカラーマークアップを有効化 |

テキスト垂直中央揃えの例:

// ボタン中央にテキストを配置
const btnX = 50,
  btnY = 100,
  btnW = 200,
  btnH = 48;
engine.add('btn-bg', {
  type: 'rect',
  x: btnX,
  y: btnY,
  width: btnW,
  height: btnH,
  fill: '#3b82f6',
  radius: 8,
});
engine.add('btn-text', {
  type: 'text',
  x: btnX + btnW / 2,
  y: btnY + btnH / 2,
  text: '決定',
  font: 'bold 16px sans-serif',
  fill: '#fff',
  align: 'center',
  baseline: 'middle',
});

リッチテキスト書式: {c:#ff0000}赤いテキスト{/c} — 指定部分のみ色を変更。ネスト不可。

engine.add('dialog', {
  type: 'text',
  x: 50,
  y: 400,
  text: '{c:#7c3aed}長老{/c}「{c:#3b82f6}クリスタル{/c}を集めよ」',
  font: '16px sans-serif',
  fill: '#fff',
  richText: true,
  align: 'center', // center/right も対応
});

スプライト円形クリップの使用例:

// プロフィールアイコンを円形に切り抜く
engine.add('avatar', {
  type: 'sprite',
  x: 50,
  y: 50,
  image: 'avatar',
  width: 80,
  height: 80,
  clipCircle: { radius: 40 }, // 中心から半径40pxで円形クリップ
});

flipX / flipY の使用例:

// キャラの向き変更(左向き)
engine.update('hero', { flipX: true });

// 親ノードに flipX → 子ノードも反転される
engine.add('player', {
  type: 'rect',
  x: 100,
  y: 200,
  width: 0,
  height: 0,
  fill: 'transparent',
  flipX: true,
});
engine.add('player-body', {
  type: 'rect',
  x: -16,
  y: 0,
  width: 32,
  height: 40,
  fill: '#3b82f6',
  parent: 'player',
});

tint の使用例:

// ダメージ赤点滅
engine.update('hero', { tint: '#ff0000', tintAmount: 0.5 });

// 敵の色バリエーション(同じスプライトで色違い)
engine.add('enemy-fire', {
  type: 'sprite',
  image: 'slime',
  x: 100,
  y: 100,
  width: 32,
  height: 32,
  tint: '#ff4400',
  tintAmount: 0.4,
});
engine.add('enemy-ice', {
  type: 'sprite',
  image: 'slime',
  x: 200,
  y: 100,
  width: 32,
  height: 32,
  tint: '#0088ff',
  tintAmount: 0.4,
});

// tintAmount を animate でフェードアウト
engine.update('hero', { tint: '#ff0000', tintAmount: 0.6 });
await engine.animate('hero', { tintAmount: 0 }, { duration: 400 });

NineSlice(UIパネル):

RPGウィンドウ風の装飾パネル。Canvas描画モード(デフォルト)と画像ベースモードを持つ。

// Canvas描画モード(画像不要)
engine.add('panel', {
  type: 'nineslice',
  x: 20,
  y: 50,
  width: 300,
  height: 200,
  fill: '#0f1729',
  borderColor: '#4a90d9',
  borderWidth: 2,
  borderStyle: 'double', // 'solid' | 'double' | 'groove'
  innerColor: '#2a5aa0',
  innerPadding: 4,
  cornerRadius: 8,
});

// 画像ベースモード(スプライトシートの9分割描画)
engine.add('frame', {
  type: 'nineslice',
  x: 10,
  y: 10,
  width: 400,
  height: 300,
  image: 'ui-frame',
  sliceSize: 16, // 均一スライスサイズ
});

| プロパティ | 型 | デフォルト | 説明 | | ----------------- | -------- | ----------- | ----------------------------------- | | width, height | number | 必須 | パネルサイズ | | fill | string | — | 背景色 | | borderColor | string | — | 外枠色 | | borderWidth | number | 2 | 枠線太さ | | cornerRadius | number | 0 | 角丸 | | borderStyle | string | 'solid' | 'solid' / 'double' / 'groove' | | innerColor | string | borderColor | 内枠色(double/groove 用) | | innerPadding | number | 4 | 外枠と内枠の間隔 | | image | string | — | 画像キー(設定時は画像9分割モード) | | sliceSize | number | img.width/3 | 均一スライスサイズ |

clipRect(クリッピング):

ノードとその子を矩形領域内に描画制限する。スクロールコンテナの内部で使用。

engine.add('viewport', {
  type: 'rect',
  x: 50,
  y: 50,
  width: 0,
  height: 0,
  clipRect: { x: 0, y: 0, width: 200, height: 300 }, // ローカル座標
});
// viewport の子ノードは 200x300 の領域内のみ描画される
engine.add('content', { type: 'text', x: 10, y: 10, text: 'Clipped!', parent: 'viewport' });

シーン管理

engine.defineScene(name, setup)

シーンを登録。

engine.defineScene('game', () => {
  engine.addMany([...]);
  engine.onUpdate((dt) => { /* game loop */ });
});

engine.goTo(name, transition?, data?): Promise<void>

シーン遷移。clear()setup() を実行。第3引数でデータを渡せる。

engine.goTo('game'); // フェード(デフォルト 300ms)
engine.goTo('menu', { type: 'none' }); // 即時遷移
engine.goTo('battle', { type: 'fade', duration: 500 }); // カスタムフェード
// データを次のシーンに渡す
engine.goTo('boss', { type: 'fade' }, { level: 3, crystals: 5 });

engine.currentScene: string | null

現在のシーン名(読み取り専用)。

engine.sceneData: any

goTo() の第3引数で渡されたデータ(読み取り専用)。遷移先シーンの setup 関数内で参照する。

engine.defineScene('boss', () => {
  const data = engine.sceneData; // { level: 3, crystals: 5 }
  const crystals = data?.crystals ?? 0;
});

グローバル状態

シーンをまたいで値を保持する Key-Value ストア。clear() ではクリアされず、destroy() でクリア。

engine.setGlobal(key, value)

値を保存。

engine.setGlobal('talkedToElder', true);
engine.setGlobal('inventory', ['sword', 'shield']);

engine.getGlobal<T>(key, defaultValue?): T

値を取得。存在しない場合は defaultValue を返す。

const talked = engine.getGlobal<boolean>('talkedToElder', false);
const items = engine.getGlobal<string[]>('inventory', []);

アニメーション

engine.animate(id, props, config): Promise<void>

ノードプロパティをアニメーション。x, y, alpha, scaleX, scaleY, rotation, tintAmount が対象。

await engine.animate('player', { x: 300, y: 100 }, { duration: 500, easing: 'easeOutQuad' });
await engine.animate('enemy', { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300 });
// ダメージ赤点滅 → フェードアウト
engine.update('player', { tint: '#ff0000', tintAmount: 0.6 });
await engine.animate('player', { tintAmount: 0 }, { duration: 400 });

AnimConfig:

| プロパティ | 型 | デフォルト | 説明 | | ---------- | ------------ | --------------- | ------------- | | duration | number | 必須 | 持続時間 (ms) | | easing | EasingName | 'easeOutQuad' | イージング | | delay | number | 0 | 開始遅延 (ms) |

EasingName: 'linear' 'easeInQuad' 'easeOutQuad' 'easeInOutQuad' 'easeInCubic' 'easeOutCubic' 'easeInOutCubic' 'easeOutBack' 'easeOutElastic' 'easeOutBounce'

engine.timeline(steps): Promise<void>

宣言的なアニメーションシーケンス。

await engine.timeline([
  { type: 'animate', id: 'enemy', props: { x: 100 }, config: { duration: 200 } },
  { type: 'wait', ms: 300 },
  { type: 'call', fn: () => console.log('done') },
  {
    type: 'parallel',
    steps: [
      { type: 'animate', id: 'a', props: { alpha: 0 }, config: { duration: 200 } },
      { type: 'animate', id: 'b', props: { alpha: 0 }, config: { duration: 200 } },
    ],
  },
]);

engine.sequence(...fns): Promise<void>

関数を順番に実行。

await engine.sequence(
  () => engine.animate('a', { x: 100 }, { duration: 200 }),
  () => engine.animate('b', { x: 200 }, { duration: 200 }),
);

engine.parallel(...fns): Promise<void>

関数を同時に実行。

await engine.parallel(
  () => engine.animate('a', { alpha: 0 }, { duration: 200 }),
  () => engine.animate('b', { alpha: 0 }, { duration: 200 }),
);

engine.wait(ms): Promise<void>

指定ミリ秒待つ(ゲームループ連動)。


入力

engine.on(event, handler): unsubscribe

イベント購読。戻り値は解除関数。

const unsub = engine.on('tap', (e) => {
  console.log(`Tapped at (${e.x}, ${e.y}), node: ${e.id}`);
});
unsub(); // 解除

イベント一覧:

| イベント | ペイロード | 説明 | | ------------- | ------------------------------------------------------------ | --------------- | | tap | { id, x, y } | タップ/クリック | | keydown | { key, code, repeat } | キー押下 | | keyup | { key, code } | キー離し | | pointermove | PointerDragEvent | ポインタ移動 | | dragstart | PointerDragEvent | ドラッグ開始 | | drag | PointerDragEvent | ドラッグ中 | | dragend | PointerDragEvent | ドラッグ終了 | | swipe | { direction, distance, speed, startX, startY, endX, endY } | スワイプ |

engine.isKeyDown(code): boolean

キーが押されているかチェック。

if (engine.isKeyDown('ArrowLeft')) {
  /* ... */
}

ノードの onClick / onDown / onUp

ノード定義時に直接ハンドラを設定。

engine.add('btn', { type: 'rect', ..., onClick: () => console.log('clicked!') });

ゲームループ

engine.onUpdate(callback): unsubscribe

毎フレーム呼ばれるコールバック。dt は秒単位。

const unsub = engine.onUpdate((dt) => {
  // dt ≈ 0.016 (60fps)
  playerX += speed * dt;
  engine.update('player', { x: playerX });
});

衝突判定

矩形ベースの AABB 判定。

engine.overlap(idA, idB): boolean

2ノード間の衝突判定。

engine.overlapAny(id, tag): boolean

ノードとタグ付きノード群のいずれかに衝突しているか。

engine.overlapAll(id, tag): string[]

衝突している全ノードIDを返す。


カメラ

engine.setCamera(config)

カメラを設定。

engine.setCamera({ x: 100, y: 50 });
engine.setCamera({ followId: 'player', followSmooth: 0.1 });
// ズーム + 境界制限
engine.setCamera({
  followId: 'player',
  followSmooth: 0.12,
  zoom: 1.5,
  bounds: { minX: 0, minY: 0, maxX: 1200, maxY: 1200 },
});

CameraConfig:

| プロパティ | 型 | デフォルト | 説明 | | -------------- | ----------------------- | ---------- | ----------------------------------------------- | | x | number | 0 | カメラ X 位置 | | y | number | 0 | カメラ Y 位置 | | followId | string | - | 追従するノード ID | | followSmooth | number | 0.1 | 追従の滑らかさ (0-1) | | zoom | number | 1 | ズーム倍率 (>1 = ズームイン, <1 = ズームアウト) | | bounds | {minX,minY,maxX,maxY} | - | カメラ移動の境界制限 |

engine.updateCamera(props)

カメラを部分更新。

engine.updateCamera({ zoom: 2.0 });

engine.getCameraOffset(): Vec2

現在のカメラオフセット。

engine.getCameraZoom(): number

現在のズーム倍率。

engine.animateCamera(props, config): Promise<void>

カメラプロパティをアニメーション。ズーム演出に便利。

await engine.animateCamera({ zoom: 1.5 }, { duration: 300, easing: 'easeOutQuad' });
await engine.animateCamera({ zoom: 1.0 }, { duration: 200 });

engine.isInView(id): boolean

ノードが画面内にあるか(ズーム対応)。


画面効果

engine.shake(intensity?, duration?)

画面シェイク。被弾・爆発の演出に。

engine.shake(); // デフォルト: 強度8, 300ms
engine.shake(12, 500); // 強め、500ms

| パラメータ | 型 | デフォルト | 説明 | | ----------- | -------- | ---------- | ----------------- | | intensity | number | 8 | 揺れの大きさ (px) | | duration | number | 300 | 持続時間 (ms) |

engine.flash(color?, duration?)

画面フラッシュ。撃破・ダメージの演出に。

engine.flash(); // デフォルト: 白, 200ms
engine.flash('#ef4444', 150); // 赤フラッシュ
engine.flash('#f97316', 300); // オレンジ(大爆発)

| パラメータ | 型 | デフォルト | 説明 | | ---------- | -------- | ---------- | ------------- | | color | string | '#fff' | フラッシュ色 | | duration | number | 200 | 持続時間 (ms) |


アセット管理

engine.loadImage(key, url): Promise<void>

画像を読み込んでキャッシュ。

await engine.loadImage('enemy', '/sprites/enemy.png');

engine.image(key): HTMLImageElement | null

キャッシュされた画像を取得。

engine.preload(manifest, onProgress?): Promise<void>

複数画像をバッチロード。進捗コールバック付き。

await engine.preload(
  { player: '/sprites/player.png', enemy: '/sprites/enemy.png', bg: '/sprites/bg.png' },
  (loaded, total) => console.log(`${loaded}/${total}`),
);

engine.ready: Promise<void>

初期アセット(EngineConfig.assets)のロード完了を待つ。


パーティクル

engine.emitParticles(config)

パーティクルを発生させる。

engine.emitParticles({
  x: 300,
  y: 200,
  count: 15,
  colors: ['#f97316', '#ef4444', '#fbbf24'],
  speed: 150,
  lifetime: 500,
  size: 4,
  direction: -Math.PI / 2, // 上方向
  spread: Math.PI * 0.5, // 90度の扇形
  gravity: 200, // 下方向の重力
});

ParticleConfig:

| プロパティ | 型 | デフォルト | 説明 | | ----------- | ---------- | ------------- | -------------- | | x, y | number | 必須 | 発生位置 | | count | number | 12 | パーティクル数 | | speed | number | 150 | 初速 (px/s) | | lifetime | number | 600 | 寿命 (ms) | | size | number | 4 | サイズ (px) | | colors | string[] | ['#fff'] | ランダム色配列 | | spread | number | Math.PI * 2 | 拡散角度 (rad) | | direction | number | 0 | 中心角度 (rad) | | gravity | number | 0 | 重力 (px/s²) |


タイマー

ゲームループ連動。シーン遷移時に自動クリア。

engine.setTimeout(fn, ms): number

const id = engine.setTimeout(() => console.log('boom'), 1000);

engine.setInterval(fn, ms): number

const id = engine.setInterval(() => spawnEnemy(), 2000);

engine.clearTimer(id)

タイマーを解除。


テキスト計測

engine.measureText(text, font?): { width, height }

const { width } = engine.measureText('Hello World', 'bold 16px sans-serif');

親子ノード

子ノードは親の座標系を継承(位置・回転・拡縮・透明度)。

engine.setParent(childId, parentId | null)

engine.add('body', { type: 'circle', x: 100, y: 100, radius: 30, fill: '#22c55e' });
engine.add('eye', { type: 'circle', x: 10, y: -5, radius: 5, fill: '#fff', parent: 'body' });

engine.getChildren(parentId): string[]

engine.getParent(childId): string | null

engine.getWorldPos(id): Vec2

ワールド座標を計算(親の座標を加算)。


タグ・ノード検索

engine.findByTag(tag): string[]

engine.removeByTag(tag)

engine.updateByTag(tag, props)

engine.add('e1', { type: 'circle', ..., tag: 'enemy' });
engine.add('e2', { type: 'circle', ..., tag: 'enemy' });
engine.updateByTag('enemy', { fill: '#ef4444' });
engine.removeByTag('enemy');

engine.findAll(predicate): string[]

engine.removeAll(predicate)


Z 順序

engine.sendToFront(id)

engine.sendToBack(id)


ノードプール

大量の同種ノードを効率的に再利用。

engine.createPool(config): NodePool

const pool = engine.createPool({
  tag: 'bullet',
  create: () => ({ type: 'rect', x: 0, y: 0, width: 4, height: 10, fill: '#ff0' }),
  initialSize: 20,
});
const id = pool.acquire(); // 取得(visible: true)
pool.release(id); // 返却(visible: false)
pool.releaseAll(); // 全返却

描画フック

Canvas の ctx に直接描画。

engine.raw(fn): () => void

全ノード描画後(スクリーン空間)。HUD 等。複数登録可。戻り値で解除。

engine.rawBelow(fn): () => void

ノード描画前(ワールド空間、カメラ影響あり)。タイルマップ背景等。複数登録可。

const unsub = engine.rawBelow((ctx, w, h) => {
  ctx.fillStyle = '#333';
  ctx.fillRect(0, 0, w, h);
});
unsub(); // 解除

engine.addRaw(id, fn) / engine.removeRaw(id)

ノードと同じ drawOrder に参加する raw 描画。sendToFront/sendToBack で描画順を制御可能。 raw() と異なり、ノードの間に挟むことができるため、モーダルの下に raw 描画を配置できる。

// 宇宙船アニメーション(ノードと同じ描画順で管理)
engine.addRaw('spaceship', (ctx, w, h) => {
  // カスタム描画...
});
engine.sendToBack('spaceship'); // 最背面に移動
// モーダルを追加してもこの描画はモーダルの下になる
engine.removeRaw('spaceship'); // 削除

engine.simulateKeyDown(code) / engine.simulateKeyUp(code)

合成キー入力。仮想ゲームパッド等から isKeyDown() を透過的に動かす。

engine.simulateKeyDown('ArrowLeft'); // isKeyDown('ArrowLeft') が true になる
engine.simulateKeyUp('ArrowLeft');

engine.canvas / engine.designWidth / engine.designHeight

読み取り専用プロパティ。Canvas 要素とデザイン解像度。


UIヘルパー

import { createHpBar, createTextWindow, createMenu, createVirtualGamepad, createScrollContainer } from './engine/ui'

createHpBar(engine, config): HpBarController

HP/MPバー。3ノード構成(背景 + 塗り + ラベル)。

const hpBar = createHpBar(engine, {
  id: 'hp',
  x: 20,
  y: 400,
  width: 200,
  height: 18,
  label: 'HP',
  labelFont: 'bold 11px sans-serif',
  colors: { high: '#22c55e', mid: '#eab308', low: '#ef4444' },
});
hpBar.update(75, 100); // current, max

createTextWindow(engine, config): TextWindowController

タイプライター演出付きメッセージ表示。

const tw = createTextWindow(engine, {
  id: 'msg',
  x: 20,
  y: 300,
  width: 560,
  height: 80,
  font: 'bold 16px sans-serif',
  charDelay: 25,
});
await tw.show('勇者のこうげき!');
tw.hide();

createMenu(engine, config): MenuController

選択メニュー。キーボード(↑↓+Enter)とタップ対応。

const menu = createMenu(engine, {
  id: 'cmd',
  x: 20,
  y: 440,
  width: 270,
  font: 'bold 18px sans-serif',
});
const index = await menu.open(['たたかう', 'まほう', 'にげる']);
menu.close();

createVirtualGamepad(engine, config?): VirtualGamepadController

モバイル用仮想ゲームパッド。D-pad + A/B ボタン。engine.raw() でスクリーン空間に描画。 押下中は engine.simulateKeyDown() で合成キーを注入するため、既存の isKeyDown() ベースのゲームコードがそのまま動く。

const gamepad = createVirtualGamepad(engine, {
  buttons: ['a', 'b'],
  keyMap: { a: 'KeyZ', b: 'KeyX' },
});
// autoHide: true にすると非タッチデバイスで自動非表示
// gamepad.destroy() でリスナー解除

VirtualGamepadConfig:

| プロパティ | 型 | デフォルト | 説明 | | ---------- | -------------------------- | ----------- | ------------------------ | | dpadMode | '4way'\|'8way' | '8way' | D-pad 方向モード | | buttons | ('a'\|'b')[] | ['a','b'] | 表示するアクションボタン | | alpha | number | 0.35 | 透明度 | | autoHide | boolean | false | 非タッチデバイスで非表示 | | keyMap | {up,down,left,right,a,b} | Arrow+Z/X | キーマッピング |

VirtualGamepadController:

  • isPressed(name) — 方向/ボタン状態取得
  • direction(){x, y} 正規化ベクトル
  • show() / hide() / visible
  • destroy() — リスナー解除

createScrollContainer(engine, config): ScrollContainerController

スクロール可能なコンテナ。内部で clipRect を使用して描画領域を制限。 ドラッグ操作でスクロール。スクロールバー表示。

const scroll = createScrollContainer(engine, {
  id: 'list',
  x: 20,
  y: 100,
  width: 260,
  height: 340,
  contentHeight: 800, // コンテンツ全体の高さ
});
// アイテムを追加(自動的にコンテナの子になる)
scroll.addItem('item-0', { type: 'text', x: 10, y: 10, text: 'Item 1', fill: '#fff' });
scroll.addItem('item-1', { type: 'text', x: 10, y: 50, text: 'Item 2', fill: '#fff' });
scroll.setScrollY(100); // プログラム的スクロール
scroll.setContentHeight(400); // コンテンツ高さ変更
scroll.clear(); // 全アイテム削除
scroll.destroy(); // コンテナ破棄

ScrollContainerConfig:

| プロパティ | 型 | デフォルト | 説明 | | ----------------- | -------- | --------------------- | ------------------------ | | id | string | 必須 | コンテナID | | x, y | number | 必須 | 位置 | | width, height | number | 必須 | 表示領域サイズ | | contentHeight | number | 必須 | スクロール可能な全体高さ | | bgColor | string | 'rgba(0,0,20,0.85)' | 背景色 | | borderColor | string | '#4a5568' | 枠線色 | | scrollbarWidth | number | 6 | スクロールバー幅 | | scrollbarColor | string | '#64748b' | スクロールバー色 |


物理ユーティリティ

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

エンジン非依存の純粋関数モジュール。ベクトル演算・衝突判定・衝突応答を提供。

ベクトル演算(Vec2)

const v = Physics.vec2(3, 4);
const n = Physics.normalize(v); // { x: 0.6, y: 0.8 }
const d = Physics.distance(a, b); // 2点間の距離
const r = Physics.reflect(vel, normal); // 反射ベクトル

| 関数 | 説明 | | ------------------------------------- | -------------------------------- | | vec2(x, y) | Vec2 生成 | | 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) | 線形補間 |

衝突判定

すべて CollisionResult { hit, normal, penetration, point } を返す。

const res = Physics.circleVsCircle({ x: 10, y: 10, radius: 5 }, { x: 15, y: 10, radius: 5 });
if (res.hit) {
  const pos = Physics.separateCircle(Physics.vec2(10, 10), res.normal, res.penetration);
  const vel = Physics.elasticBounce(Physics.vec2(vx, vy), res.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) | めり込み解消後の位置 |


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

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

エンジン非依存の純粋ユーティリティ。スプライトシートのフレーム座標計算とアニメーション再生管理。

sheetFrame(config, col, row): FrameRect

スプライトシートの指定位置から1フレームの矩形を計算。

sheetFrames(config, coords): FrameRect[]

複数の [col, row] ペアから一括計算。

const sheet: SpriteSheetConfig = { 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],
]);

createAnimator(clips): AnimatorController

名前付きアニメーションクリップを管理。エンジン不要 — onUpdate 内で手動更新。

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(); // 現在のフレーム FrameRect
  engine.update('player', { sourceRect: rect });
});

AnimatorController:

| メソッド/プロパティ | 説明 | | ------------------- | -------------------------------- | | play(name) | クリップ切り替え(同名なら無視) | | update(dt) | フレーム進行(秒単位の dt) | | frame() | 現在の FrameRect | | current | 現在のクリップ名(readonly) |


タイルマップ

import { createTilemap, createLayeredTilemap } from '@uzuhq/engine-2d'

createTilemap(config): Tilemap

単層タイルマップ。

const tilemap = createTilemap({
  cols: 20, rows: 15, tileSize: 32,
  tiles: [[1,1,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, 600, 600);
});
tilemap.isSolid(col, row);     // タイル衝突判定
tilemap.getTile(col, row);     // タイルID取得
tilemap.setTile(col, row, id); // タイル変更
tilemap.worldToTile(wx, wy);   // ワールド座標→タイル座標
tilemap.tileToWorld(col, row); // タイル座標→ワールド座標(タイル中心)

createLayeredTilemap(config): LayeredTilemap

多層タイルマップ + パララックススクロール対応。

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, 600, 600);
});

// レイヤー個別描画(前景をノードの上に描く場合)
engine.rawBelow((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.renderLayer(ctx, 0, cam.x, cam.y, 600, 600); // BG層
  tilemap.renderLayer(ctx, 1, cam.x, cam.y, 600, 600); // メイン層
});
engine.raw((ctx) => {
  const cam = engine.getCameraOffset();
  tilemap.renderLayer(ctx, 2, cam.x, cam.y, 600, 600); // 前景層(ノードの上)
});

LayerConfig:

| プロパティ | 型 | デフォルト | 説明 | | -------------- | ------------ | ---------- | --------------------------------------- | | tiles | number[][] | 必須 | [row][col] → tileId | | scrollFactor | number | 1.0 | パララックス係数(0.5=半速、2.0=2倍速) | | alpha | number | 1.0 | レイヤー透明度 |

LayeredTilemap メソッド:

| メソッド | 説明 | | --------------------------------------------------- | ------------------------------------- | | getTile(layer, col, row) | 特定レイヤーのタイルID取得 | | setTile(layer, col, row, id) | 特定レイヤーのタイル変更 | | isSolid(col, row) | 全レイヤー横断でソリッド判定(OR) | | render(ctx, camX, camY, viewW, viewH) | 全レイヤー描画 | | renderLayer(ctx, layer, camX, camY, viewW, viewH) | 特定レイヤーのみ描画 | | worldToTile(wx, wy) | ワールド座標→タイル座標 | | tileToWorld(col, row) | タイル座標→ワールド座標(タイル中心) | | layerCount | レイヤー数(readonly) |


Overlap(衝突判定 + レイキャスト)

import { Overlap } from '@uzuhq/engine-2d'Overlap.pointInRect(...) のように使用。

エンジン非依存の純粋関数モジュール。形状判定・レイキャスト・視線判定を提供。

形状判定

import { Overlap } from '../../engine';

Overlap.pointInRect({ x: 100, y: 100 }, { x: 50, y: 50, width: 200, height: 200 }); // true
Overlap.circleVsRect({ x: 10, y: 10, radius: 5 }, { x: 0, y: 0, width: 20, height: 20 }); // true
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 },
    ],
  },
); // true

関数一覧:

| 関数 | 説明 | | ------------------------------- | ------------------------------------- | | 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 円(circleVsRect の引数逆順) | | circleVsPolygon(circle, poly) | 円 vs 凸多角形 | | polygonVsPolygon(a, b) | 凸多角形 vs 凸多角形(SAT) |

レイキャスト

import { Overlap } from '../../engine';
import type { GridConfig } from '../../engine';

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

// レイ vs タイルマップ(DDA)
const grid: GridConfig = {
  cols: 30,
  rows: 30,
  tileSize: 20,
  isSolid: (c, r) => tilemap.isSolid(c, r),
};
const tilemapHit = Overlap.rayVsGrid(
  { origin: { x: 50, y: 50 }, direction: { x: 1, y: 0 } },
  grid,
  200, // maxDist
);
// tilemapHit.tileCol, tilemapHit.tileRow で当たったタイル座標

// 2点間の視線判定
Overlap.hasLineOfSight({ x: 50, y: 50 }, { x: 200, y: 100 }, grid); // true/false

レイキャスト関数:

| 関数 | 説明 | | ------------------------------------ | ---------------------------------------------------------------- | | rayVsRect(ray, rect, maxDist?) | レイ vs AABB | | rayVsCircle(ray, circle, maxDist?) | レイ vs 円 | | rayVsLine(ray, line, maxDist?) | レイ vs 線分 | | rayVsGrid(ray, grid, maxDist?) | レイ vs タイルマップ(DDA アルゴリズム)。TilemapRayHit を返す | | hasLineOfSight(from, to, grid) | 2点間の視線が通るか(内部で rayVsGrid 使用) |


Pathfinding(パスファインディング)

import { createPathfinder } from './engine/pathfinding'

グリッドベースの A* パスファインディング。バイナリヒープで高速。

基本使用

import { createPathfinder } from '../../engine/pathfinding';

const pf = createPathfinder({
  cols: 30,
  rows: 30,
  isWalkable: (c, r) => !tilemap.isSolid(c, r),
  diagonal: false, // 4方向移動(デフォルト)
});

// パス検索
const result = pf.findPath(startCol, startRow, goalCol, goalRow);
if (result.found) {
  for (const { col, row } of result.path) {
    // col, row に沿って移動
  }
}

// 移動範囲取得(タクティクス用)
const reachable = pf.getReachable(unitCol, unitRow, 5);
for (const { col, row, cost } of reachable) {
  // cost 以内で到達可能なマス
}

PathfinderConfig:

| プロパティ | 型 | デフォルト | 説明 | | ------------ | -------------------------------------------- | ---------- | --------------------------------- | | cols | number | 必須 | グリッド列数 | | rows | number | 必須 | グリッド行数 | | isWalkable | (col, row) => boolean | 必須 | 通行可能判定 | | diagonal | boolean | false | 8方向移動を許可(角切り防止付き) | | cost | (fromCol, fromRow, toCol, toRow) => number | 1 / √2 | カスタムコスト関数 |

Pathfinder メソッド:

| メソッド | 説明 | | ------------------------------------------------ | ------------------------------------------------------------ | | findPath(startCol, startRow, goalCol, goalRow) | A* でパス検索。PathResult { found, path, explored } を返す | | getReachable(startCol, startRow, maxCost) | Dijkstra フラッドフィル。コスト内の到達可能タイル一覧 |


レイアウトヘルパー

import { layoutRow, layoutColumn } from '@uzuhq/engine-2d'

ノード配列の x/y を自動計算する純粋関数。エンジン非依存。

layoutRow(opts, children): NodeConfig[]

子ノードを横一列に配置。

const cards = layoutRow({ x: 50, y: 300, gap: 8 }, [
  { id: 'c1', type: 'sprite', image: 'card1', width: 48, height: 64 },
  { id: 'c2', type: 'sprite', image: 'card2', width: 48, height: 64 },
]);
// → c1.x = 50, c2.x = 106

| オプション | 型 | 説明 | | ---------- | --------- | -------------------------------- | | x | number | 行の左端 x | | y | number | 行の y | | gap | number? | 子ノード間の間隔(デフォルト 0) | | centerIn | number? | 指定幅の中央揃え |

layoutColumn(opts, children): NodeConfig[]

子ノードを縦一列に配置。オプションは layoutRow と同様(centerIn は高さ方向)。

const buttons = layoutColumn({ x: 100, y: 50, gap: 10 }, [
  { id: 'b1', type: 'rect', width: 200, height: 40, fill: '#333' },
  { id: 'b2', type: 'rect', width: 200, height: 40, fill: '#333' },
]);
// → b1.y = 50, b2.y = 100

エンジンの責務外

以下はエンジンが扱わない領域。SDK やホスト環境(Flutter 等)の責務として、ゲームコードから直接利用する。

| 機能 | 担当 | 使い方 | | ------------------------ | ---- | --------------------------------------------------------------- | | サウンド(SE・BGM) | SDK | import { playSound, playBgm, stopBgm } from '@uzuhq/code-sdk' | | マルチプレイ(通信) | SDK | import { onRoom, init } from '@uzuhq/code-sdk' | | セーブ/ロード | 未定 | SDK またはホスト環境で提供予定 |

設計原則: SDK が既に提供している機能をエンジンで再ラップしない。ゲームコードが直接 SDK を呼ぶ。

License

MIT