本文へスキップ

PixiJS からの移行 ​

spryte は PixiJS のドロップイン置き換えではありません。PixiJS は表示オブジェクトのツリーを保持しますが、spryte は毎フレーム頼まれたものを描くだけで、何も保持しません。移行はたいていコードを削る作業になりますが、考え方は変わるので、このガイドでは2つの道筋を扱います。

  • ホットパスだけを移す。 PixiJS のゲームはそのままに、重い部分(弾、パーティクル、群衆)を上に重ねた canvas で spryte に描かせます。まずはここから始めてください。
  • レンダラー全体を移す。 ステージを描画関数に置き換えます。ほとんどのものが毎フレーム動くゲームなら、その価値があります。

考え方の違い ​

PixiJS(リテインド)spryte(イミディエイト)
Sprite を一度作ってステージに追加し、時間とともにプロパティを変える。毎フレーム、見えるものすべてについて draw.sprite(image, x, y, options) を呼ぶ。
ステージが何を描くかを覚えている。ゲームの状態が唯一の情報源。同期すべきものはない。
不要になったオブジェクトを削除または破棄する。描くのをやめる。
描画順はツリーと zIndex で決まる。描画順は呼び出し順。

利点は 2dbench が計測しているとおりです。オブジェクトごとの管理処理がないので、スプライト1つのコストは、その数値を書き込むコストだけです。その代わり、シーングラフがやってくれていたこと(ヒットテスト、イベント、入れ子)は自分で担うことになります。

コードの比較 ​

PixiJS 8:

ts
import { Application, Assets, Sprite } from "pixi.js";

const app = new Application();
await app.init({ width: 320, height: 180, background: "#111" });
document.body.append(app.canvas);

const texture = await Assets.load("/bunny.png");
const bunnies = [];
for (let i = 0; i < 1000; i++) {
  const s = new Sprite(texture);
  s.anchor.set(0.5);
  s.x = Math.random() * 320;
  s.y = Math.random() * 180;
  app.stage.addChild(s);
  bunnies.push(s);
}

app.ticker.add(() => {
  for (const s of bunnies) s.rotation += 0.05;
});

spryte:

ts
import { createRenderer } from "spryte";

const canvas = document.querySelector("canvas")!;
canvas.width = 320;
canvas.height = 180;
const draw = createRenderer(canvas);
draw.resize(canvas.width, canvas.height);

const bunny = new Image();
bunny.src = "/bunny.png";
await bunny.decode();

// 状態はただのデータ。描くまで spryte からは見えない。
const bunnies = Array.from({ length: 1000 }, () => ({ x: Math.random() * 320, y: Math.random() * 180, r: 0 }));
const look = { rotation: 0 }; // 使い回すオプションオブジェクト:描画でアロケーションは起きない

function frame() {
  draw.begin("#111", 320, 180, 1);
  for (const b of bunnies) {
    b.r += 0.05;
    look.rotation = b.r;
    draw.sprite(bunny, b.x, b.y, look); // デフォルトで中心揃え、anchor 0.5 と同じ
  }
  draw.end();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

対応表 ​

PixiJS 8spryte
new Application()、app.init({ ... })createRenderer(canvas, { smooth })。canvas のバッキングサイズが変わったら draw.resize(w, h)
app.ticker.add(fn)自前の requestAnimationFrame ループ、またはエンジンのループ(spryte にティッカーはありません)
backgrounddraw.begin(clear, width, height, scale) の第1引数
Assets.load(url)任意の画像ソース:HTMLImageElement(decode() の後)、ImageBitmap、canvas
new Sprite(texture) + プロパティdraw.sprite(image, x, y, { rotation, scaleX, scaleY, alpha, tint, flipX, flipY })
sprite.anchor.set(0, 0)origin: [0, 0](デフォルトは中心)
texture.source.scaleMode = "nearest"createRenderer(canvas, { smooth: false })、またはスプライトごとに smooth: false
スプライトシートのフレーム(frame 付きの Texture)Frame:{ image, x, y, w, h }。呼び出しごとに矩形が変わるなら1つのオブジェクトを使い回す
ParticleContainer + Particle型付き配列で draw.sprites(frame, positions, count, options, { scale, rotation, color })
x、y、rotation、scale を持つ Containerdraw.push()、translate、rotate、scale、描画、draw.pop()
Graphics().rect(...).fill(c) / .circle(...)draw.rect(x, y, w, h, color)、draw.circle(x, y, r, color)、draw.line(...)
new Text({ text, style })draw.text(text, x, y, { font, color, align, baseline })、または BitmapFont
sprite.blendMode = "add"描画の前に draw.blend("add")、後に "normal"
矩形の maskdraw.clip(x, y, w, h, () => { ... })
RenderTexture + app.renderer.render({ container, target })draw.createTarget(w, h) と draw.into(target, () => { ... })
Filter / カスタムシェーダーdraw.createShader(source) と draw.withShader(shader, () => { ... })
zIndex、sortableChildrenデータをソートし、その順に描く
eventMode、on("pointerdown")、ヒットエリアゲームの状態に対する自前のヒットテスト(下記参照)
destroy()描くのをやめる。画像のピクセルが変わったら draw.invalidate(image)、レンダラーごと捨てるときは draw.dispose()

パーティクル:一番の効果 ​

ParticleContainer は draw.sprites の1回の呼び出しに対応します。パーティクルの状態を型付き配列で持てば、spryte はそれをそのままアップロードします。

ts
const MAX = 50_000;
const pos = new Float32Array(MAX * 2);    // シミュレーションが更新する x, y のペア
const scale = new Float32Array(MAX);
const rotation = new Float32Array(MAX);
const color = new Uint32Array(MAX);       // 0xRRGGBBAA
let live = 0;

// 毎フレーム、配列を更新した後に:
draw.sprites(spark, pos, live, {}, { scale, rotation, color });

WebGL2 では、パーティクルに対する JavaScript のループはまったくありません。ベンチマークの差の大部分はここから生まれています。

自分で書くもの ​

  • ヒットテスト。 範囲をゲームの状態に持たせ、ポインターをそれと照合します。ポインターは自分の倍率(begin() に渡すのと同じ scale)で論理単位に変換してください。
  • 入れ子。 親子のトランスフォームは、push()、translate()、子を描く、pop() です。深いシーンツリーに頼るゲームなら、その部分は PixiJS に残してください。
  • テキストのレイアウト。 draw.text は揃え位置を指定して1行を描き、measure() はそのサイズを教えてくれます。折り返しやリッチテキストは自分で実装します。
  • シェーダー以外のフィルター。 spryte にはフラグメントシェーダー、レンダーターゲット、加算ブレンドがあります。組み込みのブラーやフィルターのライブラリはありません。

PixiJS を残してホットパスを移す ​

すべてを移す必要はありません。PixiJS の canvas の上に背景が透ける2枚目の canvas を重ね、重い部分を spryte に描かせます(transparent オプションには spryte 0.2 以降が必要です)。

ts
const overlay = document.createElement("canvas");
overlay.width = app.canvas.width;
overlay.height = app.canvas.height;
overlay.style.cssText = "position:absolute;inset:0;pointer-events:none;width:100%;height:100%";
app.canvas.parentElement!.append(overlay);

const draw = createRenderer(overlay, { smooth: false, transparent: true });
draw.resize(overlay.width, overlay.height);

app.ticker.add(() => {
  draw.begin("#0000", 320, 180, overlay.width / 320); // 透明でクリア:PixiJS が透けて見える
  draw.sprites(bullet, bulletPositions, bulletCount);
  draw.end();
});

弾やパーティクルはたいてい一番上に描かれるものなので、ゲームの残りの部分はまったくそのままです。

チェックリスト ​

  1. インストール:npm install spryte。
  2. まず1つのシステムを移します。たいていはパーティクルか弾で、オーバーレイを使います。
  3. その状態を型付き配列で持ち、draw.sprites で描きます。
  4. 自分のフレーム計測で測るか、気になるデバイスで 2dbench を実行します。
  5. 効果があれば、さらに移します。

関連:spryte の概要、API リファレンス、spryte を単体で使う、draw.sprites でパーティクルと弾幕。