6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React with Babylon.js での注意事項(随時更新)

Posted at

お仕事で React と Babylon.js を組で使用することになり、色々調べて得られた知見をまとめます。
サンプルコードを GitHub に置いたので、必要に応じてこちらも参照してみてください。

なお、この記事は随時更新予定です。

Babylon.js コンポーネント実装の基本

React では useRefuseEffect を使って Babylon.js のためのコンポーネントを実装するのが王道のようです。

import { useEffect, useRef } from "react";
import { Engine } from '@babylonjs/core';

const GameCanvas = () => {
  const renderCanvas = useRef(null);

  useEffect(() => {
    const engine = new Engine(renderCanvas.current);
    // ... 以下略 ...
  }, [renderCanvas]);

  return (
    <canvas ref={renderCanvas}></canvas>
  );
};

export default GameCanvas;

HTML 要素と Babylon.js の連携

HTML で描画したボタンで Babylon.js の状態を変更するような場合、React Redux の導入がほぼ必須になります。

React Redux を導入しない場合、コンポーネントの単位を大きくするか、コールバックのバケツリレーを行うか、どちらかをしなければなりません。基本的にどちらもやりたくない作業なので、素直に React Redux を導入しましょう。

React Redux を導入する際に迷うのは、canvas 要素の無駄な再描画を回避するために、どのように状態を取得するべきかという点です。つまり、コンポーネントの中で、

const something = useSelector(state => state.something);
game.setSomething(something);

のように useSelector で値を取得しても問題ないのか、あるいは、

const store = useStore();
const state = store.getState();
store.subscribe(() => {
  game.setSomething(state.something);
});

のように Store に直接コールバックを設定する必要があるのか、です。
再描画の計算コストはそれなりに大きそうなので、できるだけ再描画されない方法で管理したいですよね。

結論ですが、React では DOM に差分がある時だけ再描画を行うので、Store から取得した値をコンポーネントから返却する JSX の中で使用しない限り、再描画は発生しません。なので、基本的には前者のように useSelector を使用しても問題ありません。ただし Redux 取得した値を JSX に引き渡す必要がある場合は後者の形で実装する必要があります。

React の再描画については以下の記事(英語)が参考になります。

タブ切り替え時の canvas 要素

タブ切り替えなど、画面内の canvas 要素を出したり消したりする必要がある場合、[Babylon.js コンポーネント実装の基本](#Babylon.js コンポーネント実装の基本) で示したナイーブな実装では問題が生じることがあります。canvas を消すタイミングで生成された Engine が放棄され、再表示の際に新たな Engine が生成されてしまうためです。

canvas が非表示にされても Babylon.js の状態を維持したい場合、以下の2つの選択肢が考えられます:

  1. canvas の非表示処理を display='none' スタイルで実装する
  2. Babylon.js のオブジェクトを管理するクラスをシングルトンで実装する

お手軽なのは 1. です。react-tabsforceRenderTabPanel のように、オプションが用意されている場合も多いので、使用するパッケージの詳細を調べてみるとよいでしょう。

シングルトンで解決する場合には canvas 要素の扱いに注意が必要です。そもそも Babylon.js の Engine は canvas 要素なしにはインスタンス生成ができません。

この制約がシングルトンの実装を微妙にややこしくします。悩むのは「シングルトンの Engine をいつ生成するか?」という点です。シングルトンのインスタンス取得時に Engine を生成する場合、あらかじめ canvas を引き渡さなければならないので、

const game = GameFactory.getInstance(renderCanvas.current);

となり、getInstance のインターフェースが不便極まりないものになります。

対して initialize メソッドを追加し、Engine の生成を遅延すると、

const game = GameFactory.getInstance();
game.initialize(renderCanvas.current);

となり、表面上の利便性は上がります。しかし、上記のコードをコンポーネントに記述した場合、canvas を再描画するたびに initialize がコールされるため、initialize の実装はステートフルなものにならざるを得ません。実装イメージは以下の通りです。

let instance = null;

export const GameFactory = {
  getInstance: () => {
    if (instance === null) {
      instance = new Game();
    }
    return instance;
  }
};

class Game {
  initialize(canvas) {
    if (this.engine === null) {
      this.engine = new Engine(canvas);
      // ... 以下略 ...
    } else {
      // 2度目以降の処理をここに書く
    }
  }
}

どちらも避けたい方法ですね。

納得いかなかったので色々試行錯誤した結果、document.createElementEngine 生成のための canvas を生成し、canvas は EngineregisterView を使って登録するようにすると、ある程度キレイに実装できるようです。以下にコードを示します。

let instance = null;

export const GameFactory = {
  getInstance: () => {
    if (instance === null) {
      instance = new Game();
    }
    return instance;
  }
};

class Game {
  constructor() {
    // 架空の canvas を生成して Engine の生成に使用する
    const canvas = document.createElement('canvas');
    this.engine = new Engine(canvas);
    // ... 以下略 ...
  }

  registerView(canvas) {
    // canvas を登録する(ゴミが残らないように一度クリアしている)
    this.engine.views = [];
    this.engine.registerView(canvas);
  }
}

コンポーネント側のコードは以下のようになります。

import { useEffect, useRef } from "react";
import { GameFactory } from './3d/game';

const GameCanvas = () => {
  const renderCanvas = useRef(null);
  const game = GameFactory.getInstance();

  useEffect(() => {
    game.registerView(renderCanvas.current);
  }, [renderCanvas, game]);

  return (
    <canvas ref={renderCanvas}></canvas>
  );
};

export default GameCanvas;
6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?