57
41

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 1 year has passed since last update.

QualiArtsAdvent Calendar 2022

Day 24

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

Last updated at Posted at 2022-12-24

はじめに

メリークリスマスイブ!
こちらは「QualiArts Advent Calendar 2022」 24日目の記事になります。

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

ReactUnity

image.png

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

また、現時点のドキュメントはまだβ版で、Learn ReactUnityというチュートリアルが5%ほど完成しており、API Referenceは40%ほどが完成している状態とのことです。

ひとまず動かしてみる

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

これにより、ReactUnityReactUnityQuickJSがパッケージとしてインストールされました。

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

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

git URLでPackage Managerを使用して追加

こちらの方法も見ていきます。

まず新しくUnityプロジェクトを作成します。
そして「Window > Package Manager」を選択してPackage Managerを開きます。
image.png
「Add package from git URL」を選択し、次のURLを入力して「Add」を押します。

https://github.com/ReactUnity/core.git#latest

これにより、ReactUnityがパッケージとしてインストールされました。

そして、ReactUnityQuickJSを追加でインストールするかの確認ダイアログが出るので「YES」を押します。
image.png
これにより、ReactUnityQuickJSもパッケージとしてインストールされました。

動かしてみる

READMEには使用法として下記の4フローが書かれています。

  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
最初にReactUnityの説明をした際に、

ReactUnityはUnityUIおよびUIToolkit用のReactとHTMLのフレームワークです。

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

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

今回は「ReactUnityUGUI」コンポーネントを使い、UnityUIでUI構築を行います。
image.png
ちなみに「ReactUnityUIDocument」を追加すると次のようになります。
image.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
そしてエラーと同時に次のダイアログが立ち上がりました。
image.png
TextMesh Proを利用するために必要なリソースをプロジェクトに追加する必要があるとのことなので「Import TMP Essentials」を押して、必要なリソースをインポートします。

再度Unityで「Play」をクリックします。
image.png
今度はエラーも警告も出ずに画面のテキストが表示されました。

コードを見ていく

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

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

  • index.tsx
  • index.scss

それぞれ見ていきます。

index.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に変換されます。

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

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

ここではレンダリングを行うためのレンダラーと後ほど説明するSCSSファイルをインポートしています。

次にApp関数を見ていきます。

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コンポーネントが入っているように見えて気持ち悪いですね。

<text>{`Go to <color=red>src/index.tsx</color> to edit this file`}</text>

実はこのcolorはJSXのコンポーネントではなく、TextMeshProのリッチテキストタグで別物です。
リッチテキストタグがJSXコンポーネントとして識別されないように文字列化していたのですね。
実際に生成されたTextMeshProのオブジェクトを見ても、リッチテキストタグがそのまま埋め込まれていることが分かると思います。
image.png
最後に先ほどの関数コンポーネントを<App />として呼び出して、レンダラーのrender関数に渡し、レンダリングをおこなっています。

Renderer.render(<App />);

index.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
ここでUnityを再生したままpaddingの値を0pxに修正してみましょう。

scroll {
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  padding: 0px;// 0pxに変更
}

修正したら保存をしてください。
image.png
そうすると再生した状態でリアルタイムで画面が更新され、scrollの内側の上下左右の余白が0pxになりました。

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

Unity上での構造を見てみる

実際にUnity上でどういう構造に展開されているのかを見てみます。
image.png
Canvasの中にスクロール用のオブジェクトが置かれ、その中にテキストが置かれています。
非アクティブでスクロールバーが配置されているので、Gameウィンドウのサイズを変更してみます。
image.png
image.png
スクロールバーが表示されて、[Horizontal Scrollbar]のオブジェクトがアクティブになりました。

ただスクロールバーが中途半端な位置に表示されていて気持ち悪いですね。
これはscrollの高さを指定していないのが原因で、HTML/CSSとしては適切な挙動です。
そこでscrollに高さ指定を追加しましょう。

scroll {
  flex-direction: column;
  align-items: flex-start;
  flex-wrap: wrap;
  padding: 0px;
  height: 100%;// 高さ指定を追加
}

そして保存をすると、スクロールバーが画面の一番下に付く形に変わりました。
image.png

いろいろ触ってみる

それではいろいろと触っていきましょう。
画面が小さいと見づらいので、Gameウィンドウは元のサイズに戻しておきます。
image.png

変数を使ってみる

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

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

ボタンの実装

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

見た目の用意

まずは見た目だけを作っていきます。
index.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ファイルを次のように変更します。

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

ボタンクリック時の処理の記述

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

index.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変数を用意しています。

let count = 0;

そしてbuttonコンポーネントにonClickという属性を追加しています。

<button class="reset" onClick={() => count = 0}>reset</button>
<button class="minus" onClick={() => count--}>minus</button>
<button class="plus" onClick={() => count++}>plus</button>

これによってボタンをクリックした際の処理を記述することができます。
今回はボタンをクリックした際にcount変数の値を更新し、それによってtextコンポーネント内の変数表示を更新しようとしています。

<text>{count.toString()}</text>

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

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

useStateを用いた要素の再レンダリング

Reactではstateという機能を用いることによって、状態変更を元に画面の再レンダリングを行うことができます。
詳しくはReactの公式ドキュメントのstateとライフサイクルを参照してください。

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

それでは実際に使ってみましょう。
index.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をインポートしています。

import { useState } from 'react';

次にuseState関数を用いて値を参照するためのcount変数と値を設定するためのsetCount関数を作成します。

const [count, setCount] = useState(0)

この時のcount変数の初期値はuseState関数の引数に渡している0です。

そしてボタンクリック時のcount変数の値の更新を、直接count変数の値を変更するのではなく、setCount関数を通して変更するようにします、

<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変数に変更されています。

<text>{count.toString()}</text>

保存をして、実際に動かしてみてください。
次はボタンのクリックの応じてカウントが更新されることが確認できると思います。
image.png
useStateを用いることで(stateを用いることで)、値を設定したタイミングでその値を参照しているコンポーネントだけを再レンダリングすることができ、効率的にレンダリングの制御を行うことができます。

おわりに

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

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

ReactUnityのドキュメントの次のページを読んでいただくと、どのようなものかイメージが付くかなと思います。

また、Reactも触りの部分をちょろっと説明しただけなので、興味がある方はドキュメントを読んでいただけると良いかなと思います。

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

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

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

57
41
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
57
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?