アーキテクチャ概要
PlutoEngine のパフォーマンスは、CPUキャッシュ構造、JavaScriptエンジンの内部動作(V8 JIT / GC)、そしてGPUハードウェアの特性を考慮した**データ指向アーキテクチャ(Data-Oriented Architecture)**に基づいています。
flowchart TD
subgraph PlutoEngine ["PlutoEngine Runtime"]
Loop["Game Loop (RAF)"]
Time["TimeManager (Fixed Timestep 60Hz)"]
Input["InputManager (Latched Events)"]
Scene["Active Scene"]
end
subgraph Memory ["Memory Subsystem (Zero-Alloc)"]
Arena["InstanceBufferArena (SoA)"]
FreeList["FreeList (Int32Array)"]
Arrays["posX, posY, scale, facing, tint (Float32Array / Uint32Array)"]
end
subgraph GPU ["Graphics Device"]
Sync["Write-Through Sync (writeBuffer)"]
Pipeline["WebGPU Hardware Instancing"]
WebGPU["HTML5 Canvas Display"]
end
Loop --> Time
Time --> Scene
Input --> Scene
Scene --> Arena
Arena --> FreeList
Arena --> Arrays
Arrays --> Sync
Sync --> Pipeline
Pipeline --> WebGPU1. InstanceBufferArena: SoA メモリアリーナ
従来のオブジェクト指向(Array of Structures: AoS)では、エンティティごとに独立したオブジェクトがヒープ領域に割り当てられます:
// ❌ 従来の AoS (Array of Structures)
// メモリ上にオブジェクトが断片化し、GCの対象になる
interface Entity {
x: number; // 8 bytes
y: number; // 8 bytes
scale: number; // 8 bytes
facing: number; // 8 bytes
tint: number; // 8 bytes
}
const entities: Entity[] = []; // ポインタ配列によるキャッシュミスPlutoEngine では、すべてのデータがフラットな型付き配列(Structure of Arrays: SoA)として事前確保されます:
// PlutoEngine の SoA (Structure of Arrays)
export class InstanceBufferArena {
public readonly capacity: number;
public readonly posX: Float32Array;
public readonly posY: Float32Array;
public readonly scale: Float32Array;
public readonly facing: Float32Array;
public readonly tint: Uint32Array;
public readonly active: Uint8Array;
// ...
}なぜ SoA なのか?
- CPUキャッシュラインの最大活用: 現代のCPUは、メモリからデータを読み込む際に 64 バイト単位(キャッシュライン)で一括フェッチします。全エンティティの X 座標を走査するとき、連続した
Float32Arrayであれば、1 回のフェッチで 16 個分の座標(4 bytes × 16 = 64 bytes)がキャッシュに載り、メモリアクセスの待機時間がゼロになります。 - SIMD / JIT最適化: 配列内の連続アクセスは、ブラウザのJITコンパイラによって自動ベクトル化(SIMD命令)されやすくなります。
2. フリーリストによる $O(1)$ メモリ管理
エンティティの生成(allocate)と破棄(free)において、JavaScriptのヒープ割り当ては一切発生しません。Int32Array で実装されたスタック形式の**フリーリスト(Free List)**によって、すべて $O(1)$ の極小コストで処理されます。
// フリーリストの動作原理
public allocate(): number {
if (this.freeListHead >= this.capacity) return -1; // 枯渇時は -1
const id = this.freeList[this.freeListHead++];
this.active[id] = 1;
// デフォルト値の初期化
this.posX[id] = 0;
this.posY[id] = 0;
return id;
}
public free(id: number): void {
this.active[id] = 0;
this.freeList[--this.freeListHead] = id; // 即座に再利用スロットへ返却
}敵が倒れたときに free(id) を呼ぶと、ID番号がフリーリストの先頭に戻ります。次のフレームで新しい敵が出現した際には、その空きスロットが即座に再利用されます。
3. フライウェイト・ハンドル (Flyweight Pattern)
開発者が純粋な配列操作(arena.posX[id])ばかりを書く必要がないよう、直感的な Sprite ハンドルが用意されています。
export class Sprite {
public readonly id: number;
private readonly _arena: InstanceBufferArena;
constructor(id: number, arena: InstanceBufferArena) {
this.id = id;
this._arena = arena;
}
public get x(): number { return this._arena.posX[this.id]; }
public set x(val: number) { this._arena.posX[this.id] = val; }
public destroy(): void {
this._arena.free(this.id);
}
}Sprite はヒープ上の巨大なプロトタイプチェーンを持たず、実質的に「ただの整数ID」と「アリーナ参照」だけを保持します。
4. 固定タイムステップと可変フレームレート
ゲームループは、物理挙動の安定性と描画の滑らかさを両立するため、**アキュムレータ方式の固定タイムステップ(Fixed Timestep with Accumulator)**を採用しています。
[ RequestAnimationFrame (可変 dt: 144Hz, 60Hz, etc.) ]
│
▼
TimeManager.step(now)
│
┌───────────┴───────────┐
▼ ▼
accumulator += dt accumulator < fixedDt (1/60s)?
│ │
│ (残余分を消費) ▼
└──-> while(accumulator >= fixedDt)
scene.fixedUpdate(1/60)
│
▼
scene.update(dt)
│
▼
scene.render()fixedUpdate(1/60): 物理シミュレーション(XPBD)、衝突判定、AI意思決定など、決定論的(Deterministic)な精度が求められる処理を行います。update(dt): カメラの追従、Tweenの補間、入力処理など、ディスプレイのリフレッシュレート(144Hzなど)に同期させたい処理を行います。
5. ハードウェア・インスタンシング描画パイプライン
数十万体のスプライトを描画する際、スプライトごとに描画コマンドを発行すると GPU ドライバや IPC が過負荷で停止します。 PlutoEngine では、**単一のクアッドメッシュ(4頂点)**に対し、アリーナの座標・スケール配列を頂点アトリビュートとしてバインドし、WebGPU の drawIndexed(6, count) を 1 回だけ呼び出します。
// 1回のAPIコールで最大30万個のインスタンスを一括描画
device.setupInstancedAttributes(gpuBuffers, renderCount);
device.drawInstanced(renderCount);InstanceBufferArena の変更は、パッキングループを介さず GPU に直接ライトスルー(Write-Through)で同期されます。CPU側でのコピー処理や関数呼び出しを完全に排除し、Dirtyとマークされたバッファ領域のみを直接 writeBuffer でGPUへ転送することで、ブラウザのIPC過負荷クラッシュを防ぎ、30万体でも安定して動作します。
6. ゼロアロケーションを維持するための鉄則
PlutoEngine で最高のパフォーマンスを引き出すための開発ルールです:
- ループ内での
new禁止:new Vector2()やnew Sprite()などのインスタンス生成は初期化時に行い、ゲームループ内では行わない。 - オブジェクト・配列リテラルの禁止:
{ x, y }や[a, b]を引数や戻り値に使わず、スカラー値(数値)をそのまま渡す。 - クロージャの禁止: 毎フレーム
arr.forEach((item) => ...)やsetTimeoutを生成しない。シンプルなforループを使用する。 - イベントリスナーの登録解除: シーンのシャットダウン時にはリソースやリスナーを適切に解放する。