【ReactUnity】ReactでUnityのUI実装を行なってみた


# はじめに
メリークリスマスイブ！
こちらは「[QualiArts Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qualiarts)」 24日目の記事になります。

[株式会社QualiArts](https://qualiarts.jp/)でUnityエンジニアをしています吉成です。
ReactUnityというフレームワークを用いるとReactでUnityのUI実装を行えるとのことで、面白そうなので触ってみました。
今回は実際に動かしてUI実装を行なってみました的な内容を記事にします。

# ReactUnity
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/7df7906d-6dc9-68ef-f354-4effb935f973.png)

ReactUnityはUnityUIおよびUIToolkit用のReactとHTMLのフレームワークです。
ReactUnityを用いることでReactでUIを構築し、Unity内での表示を行うことができます。
TypeScript、redux、i18next、react-routerなどのパッケージと一緒に利用でき、CSS機能のサブセットとFlexboxレイアウトシステムもサポートしています。

- [[ReactUnity] GitHub](https://github.com/ReactUnity)
- [[ReactUnity] ドキュメント](https://reactunity.github.io/)

また、現時点のドキュメントはまだβ版で、[Learn ReactUnity](https://reactunity.github.io/learn)というチュートリアルが5%ほど完成しており、[API Reference](https://reactunity.github.io/reference)は40%ほどが完成している状態とのことです。

# ひとまず動かしてみる
[coreリポジトリ](https://github.com/ReactUnity/core)のREADMEを見つつ、動かしてみます。

## 必要な技術

必要な技術は下記の通りです。

- Node 12
- Unity 2019.4
- TMPro v2 or v3

ここでのバージョンは最小推奨バージョンで、可能であれば最新の安定版を利用するようにとのことでした。
また、Nodeは開発中のみで使用され、実行時またはプロジェクトのビルド時には必要ないとのことです。

今回、自分は下記のバージョンで確認を行いました。

- Node 16.13.0
- Unity 2021.3.16f1
- TextMeshPro 3.0.6

## ReactUnityのインストール
ReactUnityは2種類の方法でインストールすることができます。

OpenUPM経由でインストール (推奨)
```
npx openupm-cli add com.reactunity.core com.reactunity.quickjs
```

または、git URLでPackage Managerを使用して追加
```
https://github.com/ReactUnity/core.git#latest
```
### OpenUPM経由でインストール (推奨)
推奨の方法でやっていきます。

まず新しくUnityプロジェクトを作成します。
そしてUnityプロジェクトのルートのディレクトリで下記のコマンドを実行します。
```
npx openupm-cli add com.reactunity.core com.reactunity.quickjs
```
これにより、[ReactUnity](https://github.com/ReactUnity/core)と[ReactUnityQuickJS](https://github.com/ReactUnity/quickjs)がパッケージとしてインストールされました。

ReactUnityQuickJSとは、ReactUnityを対象としたUnity-jsbのフォークです。
これによりReactUnityにQuickJSエンジン機能が追加されます。

[QuickJS](https://bellard.org/quickjs/)とはCで書かれた他のシステムへの組み込みが可能なJavaScriptエンジンで、JavaScriptの実装、モジュール、非同期ジェネレーター、プロキシー、BigIntなど、ES2020仕様をサポートしています。
そして[Unity-jsb](https://github.com/ialex32x/unity-jsb)は、QuickJSを統合することで、UnityへのJavaScriptのランタイム機能の追加を可能としたライブラリです。

### git URLでPackage Managerを使用して追加
こちらの方法も見ていきます。

まず新しくUnityプロジェクトを作成します。
そして「Window > Package Manager」を選択してPackage Managerを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/c264333c-6d96-e0a2-79b7-b6d394da5279.png)
「Add package from git URL」を選択し、次のURLを入力して「Add」を押します。
```
https://github.com/ReactUnity/core.git#latest
```
これにより、[ReactUnity](https://github.com/ReactUnity/core)がパッケージとしてインストールされました。

そして、ReactUnityQuickJSを追加でインストールするかの確認ダイアログが出るので「YES」を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/128b835f-95a6-c31b-f980-eb39f90a74c6.png)
これにより、[ReactUnityQuickJS](https://github.com/ReactUnity/quickjs)もパッケージとしてインストールされました。

## 動かしてみる
READMEには使用法として下記の４フローが書かれています。

1. Canvasを作成してReactUnityコンポーネントを追加
2. `npx @reactunity/create`をUnityプロジェクトのルートで実行し、Reactプロジェクトを作成
3. `npm start`をReactプロジェクトから実行
4. Unityで「Play」をクリック

実際にやっていきます。

### Canvasを作成してReactUnityコンポーネントを追加

Hierarchy上にCanvasを作成します（ここら辺のやり方とかは省いてしまいます）。

そしてCanvasにReactUnityコンポーネントを付与しようと思いますが、**無いです**。
その代わり「ReactUnityUGUI」と「ReactUnityUIDocument」というそれっぽいコンポーネントがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/3e99de79-d684-c4b0-1ccf-da1a7a20ff1a.png)
最初にReactUnityの説明をした際に、
> ReactUnityはUnityUIおよびUIToolkit用のReactとHTMLのフレームワークです。

と書きましたが、ReactUnityはUnityUIとUIToolkitの両方をサポートしています。

UnityUIでUI構築を行いたい場合には「ReactUnityUGUI」コンポーネントを、UIToolkitでUI構築を行いたい場合には「ReactUnityUIDocument」コンポーネントを追加します。

今回は「ReactUnityUGUI」コンポーネントを使い、UnityUIでUI構築を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/f8a26d30-80e0-90fc-657c-ec334b7371f6.png)
ちなみに「ReactUnityUIDocument」を追加すると次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/8cba0cd1-02ff-c7e6-cc21-496626fca52b.png)
UIDocumentでUIToolkitのPanelSettingsのアセットを作成して参照するように言われるので、そちらを対応して利用してください。

### Reactプロジェクトを作成

Unityプロジェクトのルートで次のコマンドを実行します。
```
npx @reactunity/create
```
これによりUnityプロジェクトのルートの中にreactというフォルダが作成されました。
このreactフォルダ以下がReactプロジェクトとなります。

### `npm start`をReactプロジェクトから実行

Reactプロジェクトから実行するので、Unityプロジェクトのルートからreactフォルダの中に入り、次のコマンドを実行します。
```
npm start
```

すると、次のように怒られて実行ができません。
```
sh: react-unity-scripts: command not found
```

必要なパッケージが足りていないようなので、次のコマンドを実行して必要なパッケージをインストールします。
```
npm install
```
そして、インストールが終わったら再度次のコマンドを実行します。
```
npm start
```
次は処理が止まらずにサーバーが起動しました。

### Unityで「Play」をクリック
それではUnityで「Play」をクリックしてみます。

TextMeshProで大量のエラーと1件の警告が出ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/607ad1e9-a8a7-4f88-518b-d1103b061958.png)
そしてエラーと同時に次のダイアログが立ち上がりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/2dc8820a-985a-a70e-88fb-e51f6ba2dcd3.png)
TextMesh Proを利用するために必要なリソースをプロジェクトに追加する必要があるとのことなので「Import　TMP Essentials」を押して、必要なリソースをインポートします。

再度Unityで「Play」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/7593aee6-b279-3650-7383-92cc2b8f82b5.png)
今度はエラーも警告も出ずに画面のテキストが表示されました。

# コードを見ていく
それではコードを見ていきましょう。

`react/src/`を見ると下記の2つのファイルがあります。

- index.tsx
- index.scss

それぞれ見ていきます。
## index.tsx
```tsx
import { Renderer } from '@reactunity/renderer';
import './index.scss';

function App() {
  return <scroll>
    <text>{`Go to <color=red>src/index.tsx</color> to edit this file`}</text>
  </scroll>;
}

Renderer.render(<App />);
```

`.tsx`はJSXを含むTypeScriptファイルに用いる拡張子です。
ちなみに純粋なTypeScriptファイルの拡張子は`.ts`、JSXを含むJavaScriptファイルに用いる拡張子は`.jsx`です。

TypeScriptは簡単に言うと、型の構文を持つJavaScriptのAltJSです。
AltJS(Alternative JavaScript)とはJavaScriptの代わりとなる言語の総称で、コードをJavaScriptに変換して使用します。

また、JSXは「JavaScript XML」の略で、JavaScript言語構文のReact拡張機能です。
JSXを利用するとソースコードにマークアップ言語を記述して、コンポーネントをレンダリングすることができます。
今回のコードではApp関数の中にマークアップ言語がそのまま記述されていることが分かると思います。
JSXもコンパイルによって最終的にJavaScriptに変換されます。

それではコードを見ていきましょう。
```tsx
import { Renderer } from '@reactunity/renderer';
import './index.scss';
```
ここではレンダリングを行うためのレンダラーと後ほど説明するSCSSファイルをインポートしています。

次にApp関数を見ていきます。
```tsx
function App() {
  return <scroll>
    <text>{`Go to <color=red>src/index.tsx</color> to edit this file`}</text>
  </scroll>;
}
```
App関数がJSXで記述されたコンポーネントを返しているのが分かると思います。
これは関数コンポーネントと呼ばれ、`<App />`として呼び出すことができます。

ここではscrollというコンポーネントの中に、textコンポーネントを配置しています。
textコンポーネントの内部が`{` `}`で囲われていますが、中括弧で囲うことで内部の式を展開することができるようになります。
また、式を展開できるので、この中では変数を展開することもできます。

ただ、textコンポーネントの中に文字列化されたcolorコンポーネントが入っているように見えて気持ち悪いですね。
```tsx
<text>{`Go to <color=red>src/index.tsx</color> to edit this file`}</text>
```
実はこのcolorはJSXのコンポーネントではなく、TextMeshProのリッチテキストタグで別物です。
リッチテキストタグがJSXコンポーネントとして識別されないように文字列化していたのですね。
実際に生成されたTextMeshProのオブジェクトを見ても、リッチテキストタグがそのまま埋め込まれていることが分かると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/17c485f7-9562-26b4-562a-81ae5dc28306.png)
最後に先ほどの関数コンポーネントを`<App />`として呼び出して、レンダラーのrender関数に渡し、レンダリングをおこなっています。
```tsx
Renderer.render(<App />);
```

## index.scss
```scss
scroll {
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  padding: 40px;
}
```

`.scss`はSCSSファイルの拡張子です。
SCSSはCSSを高機能にした拡張メタ言語で、SCSSを用いることでCSSをより効率的に書くことができます。
SCSSはコンパイルによって最終的にCSSに変換されます。

一つ一つ説明はしませんが、例えばpaddingは要素の内側の余白です。
今回は40pxが設定されているので、scrollの内側の上下左右に40pxの余白が設定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/7593aee6-b279-3650-7383-92cc2b8f82b5.png)
ここでUnityを再生したままpaddingの値を0pxに修正してみましょう。
```scss
scroll {
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  padding: 0px;// 0pxに変更
}
```
修正したら保存をしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/66533c20-1f2b-84e6-111b-4e255b31e91e.png)
そうすると**再生した状態でリアルタイムで画面が更新され**、scrollの内側の上下左右の余白が0pxになりました。

個人的にこのリアルタイム更新は驚きました。

## Unity上での構造を見てみる

実際にUnity上でどういう構造に展開されているのかを見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/1707f841-6d71-c3fa-2e54-756494187e40.png)
Canvasの中にスクロール用のオブジェクトが置かれ、その中にテキストが置かれています。
非アクティブでスクロールバーが配置されているので、Gameウィンドウのサイズを変更してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/6ff23a01-498a-bd66-c24b-4bb760ce621c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/b1f3e54d-25a2-b525-45a4-d424a3762ba6.png)
スクロールバーが表示されて、[Horizontal Scrollbar]のオブジェクトがアクティブになりました。

ただスクロールバーが中途半端な位置に表示されていて気持ち悪いですね。
これはscrollの高さを指定していないのが原因で、HTML/CSSとしては適切な挙動です。
そこでscrollに高さ指定を追加しましょう。
```scss
scroll {
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  padding: 0px;
  height: 100%;//　高さ指定を追加
}
```
そして保存をすると、スクロールバーが画面の一番下に付く形に変わりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/5ea48bba-2f0c-9503-a45b-9d1ff328e69c.png)

# いろいろ触ってみる
それではいろいろと触っていきましょう。
画面が小さいと見づらいので、Gameウィンドウは元のサイズに戻しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/66533c20-1f2b-84e6-111b-4e255b31e91e.png)

## 変数を使ってみる

先ほど、textコンポーネントの内部が`{` `}`で囲われており、この中では変数を展開することもできるという話をしたので、せっかくなので変数を使ってみましょう。

```tsx
import { Renderer } from '@reactunity/renderer';
import './index.scss';

function App() {
  const color = 'blue'
  return <scroll>
    <text>{`Go to <color=` + color + `>src/index.tsx</color> to edit this file`}</text>
  </scroll>;
}

Renderer.render(<App />);
```
色をcolor変数（constなので正確には定数）として外出してみました。
保存をすると、tsxファイルを変更した場合でも**再生した状態でリアルタイムで画面が更新され**、テキストの色が変わっていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/6e1e8f59-90d2-1ac2-abcb-359a3b62079c.png)

## ボタンの実装
ボタンを実装できるとやれることが広がると思うのでボタンを実装しましょう。

### 見た目の用意
まずは見た目だけを作っていきます。
index.tsxファイルを次のように変更します。
```tsx
import { Renderer } from '@reactunity/renderer';
import './index.scss';

function App() {
  return <scroll>
      <text>0</text>
      <view>
        <button class="reset">reset</button>
        <button class="minus">minus</button>
        <button class="plus">plus</button>
      </view>
    </scroll>;
}

Renderer.render(<App />);
```
`<view>`はHTMLの`<div>`的な立ち位置として使います。`<button>`はボタンです。

次にindex.scssファイルを次のように変更します。
```scss
scroll {
  display: flex;
  justify-content: center;
  height: 100%;
  background-color: white;
}

text {
  font-size: 100px;
  text-align: center;
}

view {
  display: flex;
  flex-flow: row no-wrap;
  justify-content: center;
  button {
    margin: 20px;
    color: white;
    &.reset {
      background-color: black;
    }
    &.minus {
      background-color: blue;
    }
    &.plus {
      background-color: red;
    }
  }
}
```
入れ子構造にして書いてみたりして、少しSCSS的な書き方を入れてみました。
中身は結構単純化して適当なので、もっと良い書き方があると思います。

保存をするとGameウィンドウの見た目は次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/576e836f-6270-6e84-90c4-fecc97895448.png)

### ボタンクリック時の処理の記述
次にcount変数を用意し、クリックした際の処理を記述し、カウント値を更新していこうと思います。

index.tsxを次のように変更します。
```tsx
import { Renderer } from '@reactunity/renderer';
import './index.scss';

function App() {
  let count = 0;
  return <scroll>
      <text>{count.toString()}</text>
      <view>
        <button class="reset" onClick={() => count = 0}>reset</button>
        <button class="minus" onClick={() => count--}>minus</button>
        <button class="plus" onClick={() => count++}>plus</button>
      </view>
    </scroll>;
}

Renderer.render(<App />);
```
まずcount変数を用意しています。
```tsx
let count = 0;
```
そしてbuttonコンポーネントにonClickという属性を追加しています。
```tsx
<button class="reset" onClick={() => count = 0}>reset</button>
<button class="minus" onClick={() => count--}>minus</button>
<button class="plus" onClick={() => count++}>plus</button>
```
これによってボタンをクリックした際の処理を記述することができます。
今回はボタンをクリックした際にcount変数の値を更新し、それによってtextコンポーネント内の変数表示を更新しようとしています。
```tsx
<text>{count.toString()}</text>
```

保存をして、実際に動かしてみてください。
現状だと、**ボタンをクリックしてもカウントが更新されない**と思います。

Reactではレンダリングが行われた後、内部で参照している変数が更新されても、そのままでは再レンダリングは行われません。

### useStateを用いた要素の再レンダリング
Reactではstateという機能を用いることによって、状態変更を元に画面の再レンダリングを行うことができます。
詳しくはReactの公式ドキュメントの[stateとライフサイクル](https://ja.reactjs.org/docs/state-and-lifecycle.html)を参照してください。

そして、React 16.8からHookという新機能が追加されており、stateなどのReactの機能を、クラスを書かずに使えるようになっています。
そしてuseStateというものがstateを扱うためのHookです。

それでは実際に使ってみましょう。
index.tsxを次のように変更します。
```tsx
import { Renderer } from '@reactunity/renderer';
import { useState } from 'react';
import './index.scss';

function App() {
  const [count, setCount] = useState(0)

  return <scroll>
      <text>{count.toString()}</text>
      <view>
        <button class="reset" onClick={() => setCount(0)}>reset</button>
        <button class="minus" onClick={() => setCount(count - 1)}>minus</button>
        <button class="plus" onClick={() => setCount(count + 1)}>plus</button>
      </view>
    </scroll>;
}

Renderer.render(<App />);
```
まず、useStateを用いるためにuseStateをインポートしています。
```tsx
import { useState } from 'react';
```
次にuseState関数を用いて値を参照するためのcount変数と値を設定するためのsetCount関数を作成します。
```tsx
const [count, setCount] = useState(0)
```
この時のcount変数の初期値はuseState関数の引数に渡している0です。

そしてボタンクリック時のcount変数の値の更新を、直接count変数の値を変更するのではなく、setCount関数を通して変更するようにします、
```tsx
<button class="reset" onClick={() => setCount(0)}>reset</button>
<button class="minus" onClick={() => setCount(count - 1)}>minus</button>
<button class="plus" onClick={() => setCount(count + 1)}>plus</button>
```
また、記述内容は変わらないですが、textで表示しているcount変数はuseState関数から返されたcount変数に変更されています。
```tsx
<text>{count.toString()}</text>
```
保存をして、実際に動かしてみてください。
次はボタンのクリックの応じてカウントが更新されることが確認できると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69978/1eaa4dcc-b4ae-435c-9514-2570cbb80a7c.png)
useStateを用いることで（stateを用いることで）、値を設定したタイミングでその値を参照しているコンポーネントだけを再レンダリングすることができ、効率的にレンダリングの制御を行うことができます。

# おわりに
今回はReactUnityを用いてReactでUnityのUI実装を少しだけ行なってみました。

＋αでReactとC#の間の情報伝達周りまで書けると良かったのですが、ドキュメントを見た感じだとReact側の書き方は分かるのですが、C#側の書き方が不足しており、本記事には入れていないです。

ReactUnityのドキュメントの次のページを読んでいただくと、どのようなものかイメージが付くかなと思います。
- [React - C# communication (Interop)](https://reactunity.github.io/learn/interop)
- [useGlobals()](https://reactunity.github.io/reference/api/useglobals)

また、Reactも触りの部分をちょろっと説明しただけなので、興味がある方はドキュメントを読んでいただけると良いかなと思います。
- [Reactドキュメント](https://ja.reactjs.org/docs/getting-started.html)

触ってみた感想としては、Webフロントもやっていた自分からすると画面を組み立てやすかったです。
何よりリアルタイムで画面に変更が反映されるのがとても使い心地が良かったです。

本番のアプリとかで使ったりする予定は無いのですが、チーム内でしか使わないちょっとした開発サポートツールを作るのには使ってみても面白いかもなという印象です。

是非興味がある方は触ってみていただけますと幸いです。