📖

ノベルゲームのシナリオをJSXで書きたい

2021/10/31に公開

Zenn 初投稿です。よろしくお願いします。

最近遊んだアドベンチャーゲームがとても面白くて自分でも作ってみたくなったので、こんなものを作ってみました。

https://github.com/mkizka/advx-poc

RPG ツクールっぽい画面です。

使い方

function App() {
  const choice = useChoice();
  return (
    <Game>
      <SenarioRenderer>
        <Text>テキスト</Text>
        <Choice choices={["選択肢1", "選択肢2"]} />
        <Branch if={choice.answer == "選択肢1"}>
          <Text>選択肢1が選ばれました</Text>
        </Branch>
        <Branch if={choice.answer == "選択肢2"}>
          <Text>選択肢2が選ばれました</Text>
        </Branch>
      </SenarioRenderer>
      <ScreenRenderer>
        <MessageWindow />
      </ScreenRenderer>
    </Game>
  );
}

基本的なコンポーネント

  • Game
    • 一番上を囲うコンポーネント
    • 中身はContext.Providerを並べてるだけ
  • SenarioRenderer
    • シナリオを書くためのコンポーネント
    • カスタムレンダラが渡されたchildrenを JSON に変換する
  • ScreenRenderer
    • 画面に要素を配置するためのコンポーネント
    • 中身はほぼ@inlet/react-pixiStageを置いてるだけ

その他や詳細はテスト用ディレクトリのApp.tsxや本体の実装を見てください。

https://github.com/mkizka/advx-poc/blob/main/playground/src/App.tsx

なぜ作ったか

RPG ツクールやティラノスクリプトなど、ある程度形式が決まったゲームをなるべく簡単に作るためのツールはすでに多くあると思います。

それらのいくつかは、JavaScript などを用いてツールを拡張出来るものがあります。
でも凝ったことをしようとすればするほどそうした拡張の比率がどんどん増えてきて、拡張と元々の構文とで実装の見通しが悪くなると思うんですよね。

ということで、全部 JavaScript で書けるようなやつがあっても良いんじゃないかと思い作ってみました。

やったこと

多くの部分はライブラリに頼っており特に難しいことはしていませんが、いろいろ調べたり考えたことを書いておきます。

React のカスタムレンダラの実装(react-reconciler)

シナリオを JSX で書くにあたって専用のレンダラを作りました。

React でカスタムレンダラを作るにはreact-reconcilerに、各種メソッドを実装した HostConfig と呼ばれるオブジェクトを渡す必要があります。
HostConfig に実装したメソッドは React のレンダリング時に呼び出され、ツリー構造の生成や、差分検出、差分の適用などが行われるようです。

最新の情報ではありませんが以下の記事などが参考になりました。

https://blog.atulr.com/react-custom-renderer-1/

このあたりのリポジトリで HostConfig の実際の実装が確認できます。

https://github.com/facebook/react/blob/main/packages/react-art/src/ReactARTHostConfig.js
https://github.com/inlet/react-pixi/blob/master/src/reconciler/hostconfig.js
https://github.com/jiayihu/react-tiny-dom/blob/master/renderer/tiny-dom.js

シナリオ記述のためのコンポーネント設計

カスタムレンダラの実装が複雑にならないように、レンダリング用のコンポーネント(react-domで言うところの DOM 要素)は以下の 2 つのみにしました。

  • Text
    • <Text>テキスト</Text>のように書くと画面に描画するためのテキストになる
  • Action
    • 渡された関数を実行して次の要素に移る

つまり、レンダリング結果は結局のところ以下のようになります。

[
  { type: "Text", message: "テキスト" },
  { type: "Action", action: ... },
  { type: "Text", message: "テキスト" },
  { type: "Text", message: "テキスト" }
]

これらがメッセージウィンドウのクリックなどに応じて 1 つずつ表示または実行されていきます。

従って、Text以外のシナリオ用コンポーネントはActionをラップして実装しています。
例えば、選択肢を表示するChoiceはこんな感じです。

function Choice({ choices }) {
  const choice = useChoice();
  return <Action action={() => choice.setChoices(choices)} />;
}

問題:異なるレンダラ間でコンテキストを共有できない

現状、実行すると以下のような警告がコンソールに表示されます。

Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.

これは自作のレンダラと画面描画用のレンダラ(@inlet/react-pixi)とでコンテキストを共有しようとしていることに起因しています。

本来、異なるレンダラ間でコンテキストを共有する場合は、HostConfig のisPrimaryRendererfalseにすることで許容されるようです。

https://github.com/facebook/react/issues/17275

しかし今回はreact-dom、自作レンダラ、画面描画レンダラの 3 つがあります。react-domとそれぞれで共有する分には警告はでませんが、後ろ 2 つで同じコンテキストを使おうとしているため警告が出ています。

- react-dom
  - @inlet/react-pixiのレンダラ
  - 自作レンダラ

今のところどうしようもないようなので、大きな問題が出るまでは放置しています。

おわり

最初に想定していたものはおおむね実現出来たので満足です。Reconciler に触れてみて、React の理解も深まった気がします。

かなり大雑把に実装してしまったので、もっと上手く出来るという方はぜひ作ってください。そして使わせてください。

GitHubで編集を提案

Discussion