pixi-reactに触ってみての注意点や感想とか
初めに
pixi-reactのパッケージは@pixi/react
と@inlet/react-pixi
が混在しているが、前者の方を使うようにすること
- @inlet~ のほうはpixijs公式に取り込まれる前の個人プロジェクトだった頃のものと思われる、
_https://reactpixi.org/ ではまだこちらの方をインストールするよう指定されるので注意(2024-01-17更新)ドメイン失効?で消えてた、念のためリンク回避 - 特にv7未満のパッケージはreact 18に対応していないが、@inlet~のほうはv6.8.0までしかない
- (pixiにreactを組み合わせたプロジェクトはreact-pixi-fiberなど他にもあるが、特に理由が無ければ@pixi/reactで事足りる?)
検証モジュールのバージョン
- pixi.js v7
- react v18
- @pixi/react v7
検証サンドボックス
気を付けたほうがいいポイントなど
拡張コンポーネントでのprops引継ぎを忘れない
既存コンポーネントを拡張する際は以下のようにする。
import { Sprite } from "@pixi/react";
const RedBunny = (props) => {
return <Sprite image={"./うさぎ画像.png"} tint={0xff0000} {...props} />;
};
この時{...props}
のところを忘れないこと。
そうしないと(childrenも含め)拡張元のSpriteへpropsが引き継がれない
GraphicsはPixiComponentで定義するのが良さげ
Graphicsを使ったコンポーネントは一旦PixiComponentで定義するとスマート
// Good
import { PixiComponent } from "@pixi/react";
import { Graphics as PixiGraphics } from "pixi.js";
export const CircleSprite = PixiComponent("CircleSprite", {
create: () => {
const g = new PixiGraphics();
g.lineStyle(2, 0xffffff)
.beginFill(0xff0000, 1)
.drawCircle(0, 0, 7, 14)
.endFill();
return g;
}
});
というのも以下のようにdraw propsで直接定義することもできるが、処理が激重になったため
(draw処理が事あるごとに走るから?)
// NG!
return (
<Graphics
preventRedraw={true}
draw={(g) => {
g.lineStyle(2, 0xffffff)
.beginFill(0x35cc5a, 1)
.drawStar(0, 0, 7, 14)
.endFill();
}}
x={x}
y={y}
angle={angle}
>
{props.children}
</Graphics>
);
参考リンク
Graphics処理についてはuseCallbackのhookを使ってdraw処理を定義するのも良いらしい
const draw = useCallback(g => {
g.beginFill(0x0033cc, 1)
g.lineStyle(4,0xff0000,1)
g.drawRect(250, 150, 150, 120)
g.endFill()
},[]);
<Graphics draw={draw} />
更新処理のあれこれ
useTickが基本だが…
コンポーネントの毎フレーム状態変更はuseTickが便利
(UnityなどでいうところのOnUpdate的な処理が可能)
const [angle, setAngle] = useState(0);
useTick((delta) => {
setAngle(angle + delta);
});
※deltaパラメータは複雑なので割愛するが通常は1
が入る
ただ状態更新(useTickの実行)タイミングはデフォルトではPIXI.Ticker(=requestAnimationFrame)に依存していることに留意(例えば高Hzモニターでは秒間144回useTickが走ることもあり得る?)
Tickerの設定
PIXI.Tickerには最大FPSを制限する機能があるが、基本的にTickerはデフォルト(制限なし)のまま使っている?のでuseTickの実行頻度が端末によって変わることを考慮する必要がある。
(特に何も設定してない場合、素のPIXI.Applicationクラスに付随するTickerをそのまま使われ、このTicker(Ticker.shared)はほぼデフォルト値のまま)
options
propを経由して自分で設定したTickerを使うことも可能
const myTicker = new Ticker();
myTicker.maxFPS = 15;
// 省略
<Stage
options={{
ticker: myTicker
}}
>/* 省略 */
</Stage>
独自のループ処理を組み込んでみる
描画(renderer.renderの実行)と状態更新(useTickの実行)ループのタイミングは独自の処理に変更することもできる
以下はとりあえずの方法(一例であり、要改善)
- <Stage> propの
raf
をfalseにする(必要に応じてrenderOnComponentChange
もオフ) - refやuseEffectを駆使して初回呼び出しだけループ開始する処理を実行する
const loopFrameinterval = 1000 / 15; // 約15FPSにする
export default function App() {
// 独自ループ処理仕込み:初回呼び出しだけ
const appRef = useRef<import("pixi.js").Application>();
{
const once = useRef(true);
useEffect(() => {
if (!once.current) return;
const loop = () => {
if (appRef.current) {
const app = appRef.current;
app.ticker.update(); // 更新処理
app.renderer.render(app.stage); // 描画処理
}
setTimeout(loop, loopFrameinterval); // 次のループを仕込む
};
loop(); // ループ開始
once.current = false; // 次回呼び出し以降にループ開始処理を行わないようにする
}, []);
}
<Stage
onMount={(app) => {
// app参照を保持
appRef.current = app;
}}
raf={false}
renderOnComponentChange={false}
>/* 省略 */
</Stage>
}
ゲームで更新頻度は60回に固定したいときなどに有効
その他
ちなみに「コンポーネントの状態が変更したときだけ再レンダリングしたい」というだけならraf propをfalse、renderOnComponentChangeをtrueにするだけで良い
良いと思ったところ・その他など(WIP)
useApp Hookを使うとrenderer参照やスクリーンサイズを取ってくる的なことが自然にできてよい
const { renderer, screen } = useApp();
これはRenderTextureのレンダリングにも便利
その他
- 元のpixijsクラスをimportするときは
PixiXxx
とリネームすると良い(公式もそうしている)