【Unity】UI ToolkitをランタイムUIとして使ってみる

**※2024年12月17日更新情報**

[2024年Unityアドベントカレンダー](https://qiita.com/advent-calendar/2024/unity)の12月17日担当として[「UI Toolkit」記事](https://qiita.com/advent-calendar/2020/unity)を執筆しました。 **最新版のUI Toolkit情報** を受け取れますのでぜひ確認して頂ければと思います。

https://zenn.dev/ohbashunsuke/articles/unity-uitoolkit-2024

----

この記事は[Unity Advent Calendar 2020](https://qiita.com/advent-calendar/2020/unity)の12月16日の記事です。

Webフロント技術からインスパイアされて開発されたUnityの次世代UIシステム**「UI Toolkit」**がUnity2020.1から**ランタイムUIサポート**をしています。[^1] 
今までのGameObjectベースではなく**HTML/CSS**のような記述で開発します。

[^1]: 今までEditorのスクリプトのみ対応していました

>UI Toolkit is based on, and inspired by, standard web technologies. If you have experience developing web pages or applications, much of your knowledge might be transferable, and many of the core concepts might be familiar.

[Unity - Manual: UI Toolkit](https://docs.unity3d.com/2020.1/Documentation/Manual/UIElements.html)参照

ここ最近はずっとuGUIで開発していた自分としてはとても気になっていたため、**UI ToolkitをランタイムUI**として使ってみることにしました。

## 執筆する上で作ったもの

移動・透明度のアニメーションをしながら表示して、ボタンをクリックしたら全体が消えていくシンプルなUIを作ってみました。

![complete3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/46051484-0cd4-bcfb-26e6-f948351baac5.gif)

本記事では大きく以下の2点について説明していきます。

1. UI Toolkitを使ったUI画面構築方法
1. アニメーションの実装方法

**UI Toolkit全くやったこと無い人が何となく理解できるようになる**というのが目標とします。

## 環境

- Unity2020.2.0b14.3668
- UI Toolkit 1.0.0-preview.13



## 1. UI Toolkitを使ったUI画面構築方法

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/20844d5a-250d-0ce4-76dd-aaae6348cfc8.png" width=550 />

この画面をUI Toolkitで作っていく過程を説明していきます。

- 3隅にそれぞれLabel
- 真ん中に画像
- 真ん中下にButtonとLabel

このような構成です。
**※背景は3D空間上に配置しているためUI Toolkitで管理していません**

旧来のuGUIのように要素ごとにGameObjectとコンポーネントををHierarchyにペタペタ配置していくということはせず、UXMLと呼ばれる**XMLのようなフォーマット**で書いていきます。


先のUI構成要素をUXMLで表現すると以下のようになります。
※Sample.uxmlというUXMLファイルを作成しています。

```xml
<ui:UXML xmlns:ui="UnityEngine.UIElements">
    <!-- 左上 : バージョン表記 -->
    <ui:Label text="ver1.0.0-preview" name="Version" />
    <!-- 右上 : ユーザーID表記 -->
    <ui:Label text="UserId : 10378426348" />

    <!-- 中央UIパーツ -->
    <ui:VisualElement name="LogoContainer">
        <!-- ロゴ画像 -->
        <ui:VisualElement name="ProductLogo" />
        <!-- ボタン -->
        <ui:Button text="TAP TO START&#10;" name="BtnTapToStart" />
    </ui:VisualElement>

    <!-- 左下 : コピーライト表記 -->
    <ui:Label text="2020(C)ohbashunsuke" />
</ui:UXML>
```
適宜説明をコメントアウトしているので深く説明しませんが、**HTMLでマークアップしていく感覚**でUI要素羅列していきます。

### UXMLの簡易説明

ソースコードに3つの要素が出てきました。

- VisualElement
- Label
- Button

`Label`、`Button`、`VisualElement`は、それぞれ`UnityEngine.UIElements`内のクラス名です。

```xml
<ui:Label text="ver1.0.0-preview" name="Version" />
```
例えばこれは「`Label`クラスのインスタンスに`ver1.0.0-preview`という文字列を`text`プロパティに代入した」という意味になります。


ちなみに現状のUXMLを画面に映し出してみます。

### UXMLを画面に反映する準備

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/18ed6be2-4a3a-96fd-cc6d-751eadd0b17e.png" width=350 />

`Create > UI Toolkit > Panel Settings Asset`を選択してProjectに`PanelSettings`を作成しておきます。


1. ヒエラルキーにGameObjectを追加(UIGameObjectと命名)
1. UIGameObjectに`UIDocumentコンポーネント`をAddComponent
2. `UIDocument`のインスペクタの`SourceAsset`に先のUXMLをセット
3. 予め作成した`PanelSettings`を`UIDocument`インスペクタの`Panel Settings`にセット 

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/b0147a47-c24f-9dd9-3719-78a2c5c80fdb.png" width=450 />

このようなインスペクタになります。

#### 画面に表示!!
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/4814b1d4-3c2a-dce2-a82b-848eab532372.png" width=550 />

UI要素をUXMLに記述しただけなので、**書いた順**に要素が表示されています。

ここから配置、装飾処理を施していきます。


### USSファイルで配置と装飾

```
.
├── Sample.uss
└── Sample.uxml
```
Sample.ussをSample.uxmlと同階層に作成します。
USSファイルはHTML/CSSでいう`CSS`にあたります。

### USSをUXMLにロードさせる

```xml
<Style src="Sample.uss" />
```
上記のコードをUXMLの2行目に追加します。
これでSample.uxmlがSample.ussを参照するようになります。

## スタイルの定義

まずは左上のテキストから。

```css
/*左上要素*/
.lt {
    position: absolute;
    left: 14px;
    top: 14px;
}
```
`position: absolute;`と`absolute`を設定することで絶対座標で配置することを指示します。
`left:14px, top:14px`の設定で左から14px、上端から14pxの位置に配置する事になります。

この設定をUXMLで指定します。

```xml
<ui:Label text="ver1.0.0-preview" name="Version" class="lt" />
```
バージョン表記部分に `class="lt"`を追加し、先のUSSで定義した`lt`を適用しました。
**※lt・・・LeftTopという意味**

HTML/CSSの考え方と同じで、USSで定義したスタイルをUXMLの要素に割り当てます。この作業他2隅の要素に適用すると、以下のような見た目になります。
(ltの他**rt**・・・`RightTop`、**lb**・・・`LeftBttom`を追加)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/4c6a3f2a-0ff6-c98b-5b50-cb315953df11.png" width=450 />

各隅のテキスト要素が意図通り配置されました。

ソースコードを見ていきましょう。

### Sample.uxml

```xml
<ui:UXML xmlns:ui="UnityEngine.UIElements">
    <!-- スタイルのロード -->
    <Style src="Sample.uss" />
    <!-- 左上 : バージョン表記 -->
    <ui:Label text="ver1.0.0-preview" name="Version" class="lt" />
    <!-- 右上 : ユーザーID表記 -->
    <ui:Label text="UserId : 10378426348" class="rt" />

<!-- ~~~~ ロゴ部分省略 ~~~~ -->

    <!-- 左下 : コピーライト表記 -->
    <ui:Label text="2020(C)ohbashunsuke" class="lb" />
</ui:UXML>
```
各Label要素に`class`が追加されています。例えば`class="rt"`が追加された要素は右上端から14pxの距離に配置されるという事になります。

USSファイルを見ていきます。

### Sample.uss

```css
/*左上要素*/
.lt {
    position: absolute;
    left: 14px;
    top: 14px;
}

/*右上要素*/
.rt {
    position: absolute;
    right: 14px;
    top: 14px;
}

/*左下要素*/
.lb {
    left: 14px;
    position: absolute;
    bottom: 14px;
}
```
USSには配置処理を記述しており、このclassが適用されるだけで配置処理が実行されます。

### 余談 : USSに変数を使用する

先のUSSファイル内に14pxがいくつも出てきました。変数に置き換えられると便利ですよね。
USSに**変数**を使用していきます。

`--変数名: 値`という文法で変数定義。`var(--変数名)`で変数を使用します。

#### サンプル
 
```css
:root{
    --hogehoge:428;
}
.piyo{
    /* 「width:428;」と同じ処理になる */
    width: var(--hogehoge);
}
```
変数をどこに定義するのか？という話になりますが、公式リファレンスで`:root`要素に定義していたのでとりあえずそれに従っています。
[Unity - Manual: USS custom properties (variables)](https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-USS-CustomProperties.html)


USSで変数を使うことで先のサンプルは以下のようになりました。

```css
:root{
   /*変数margin定義*/
    --margin: 14px;
}

/*左上要素*/
.lt {
    position: absolute;
    left: var(--margin);
    top: var(--margin);
}

/*右上要素*/
.rt {
    position: absolute;
    right: var(--margin);
    top: var(--margin);
}

.lb {
    position: absolute;
    left: var(--margin);
    bottom: var(--margin);
}
```

## 画像の利用

```css
#ProductLogo {
    background-image: url('/Assets/Project/Images/Logo.png');
    height: 258px;
    width: 604px;
}
```
画像をただ配置するのは簡単です。
`background-image: url(画像のパス);`、`width`と`height`を指定します。
※`width`と`height`の指定が無いとサイズ0という事なのか表示されません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/1b63238b-a989-3109-0c6f-8707647bd680.png" width=450 />

このように画像も配置されました。

### 最後にボタン

角丸ボタンを作りたかったので、9スライスでの使用を想定した画像を使用しています。
![button-base-0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/d50e2dfa-df25-af24-b835-e8d75ea114a5.png)

```css
#BtnTapToStart {
    top: auto;
    width: 302px;
    height: 80px;
    position: absolute;
    bottom: 18%;
    font-size: 26px;
    opacity: 1;
    background-image: url('/Assets/Project/Images/Atlas/OutGame/button-base-0.png');
    -unity-background-scale-mode: stretch-to-fill;
    -unity-slice-left: 10;
    -unity-slice-top: 10;
    -unity-slice-right: 10;
    -unity-slice-bottom: 10;
    background-color: rgba(0, 0, 0, 0);
    color: rgb(210, 210, 210);
}
```
画像のスライス処理が入っているので、多少コードが長くなっています。


ボタンクリック時の処理をC#側に書いていきます。

```cs
var document = GetComponent<UIDocument>();
var root = document.rootVisualElement;
// 「BtnTapToStart」をキーにしてボタンインスタンスを取得
var tapToStartButton = root.Q<Button>("BtnTapToStart");
tapToStartButton.clicked += () => Debug.Log("画面全体の透明アニメーション処理実行");
```
指定要素の名前で取得する場合は`Q<T>(要素名)`を使うと良いでしょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/f0728898-2fdd-8ad5-a22e-ac85015f71a4.png" width=450 />

このような感じでUI要素がすべて表示されました。

### UI Toolkitにおける画面の構築方法のまとめ

- HTML/CSSのような感覚で配置とスタイルを適用
- UIDocumentコンポーネントでUXMLを画面に表示
- USSで変数を使う場合、`--変数名: 値`で定義、`var(--変数名)`で使用
- `Q<T>(キー)`で要素を取得

## 2.アニメーションの実装方法

UI要素の配置とデザインの反映は出来ました。
ここからはアニメーションの実装方法を検証していきます。

### スクリプトでアニメーション

大好きなDOTweenを使ってアニメーションをさせてみます。
[DOTween (HOTween v2) | アニメーション ツール | Unity Asset Store](https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676?locale=ja-JP)

#### 平行移動アニメーション
![slidex.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/d7b55aef-022d-bcbc-5e1b-a71242454d38.gif)
ヘッダ・フッターの横スライドアニメーションを実装していきます。[^2]

[^2]: 紹介しているgif動画は透明+移動アニメーションですが、脳内補完をお願い致します。

```cs
var root = GetComponent<UIDocument>().rootVisualElement;
VisualElement target = root.Q<VisualElement>("target");
// 2秒かけて(-100, 0, 0)に向かってtargetを移動させる
DOTween.To(() => target.transform.position,
    x => target.transform.position = x, new Vector3(-100, 0), 2f)
    .SetEase(Ease.OutQuart);
```
VisualElementには`transform`というITransform型のプロパティが定義されています。以下がITransform型のソースです。

```cs
namespace UnityEngine.UIElements
{
  public interface ITransform
  {
    Vector3 position { get; set; }
    Quaternion rotation { get; set; }
    Vector3 scale { get; set; }
    Matrix4x4 matrix { get; }
  }
}
```
このようにTransformコンポーネントと同じようなプロパティが定義されているため、 VisualElementでも同様の使い方でアニメーションさせることが出来ます。

##### パーツの初期座標

各パーツVisualElementの実行時の座標`ITransform.position`は配置された場所が**(0, 0, 0)**です。


#### 透明度アニメーション
![fade2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/f5a3ee6d-84de-a227-d84f-ab91892fe82a.gif)

ロゴ部分の透明度アニメーションを実装していきます。

```cs
void Alpha(VisualElement ve, float duration, float alphaValue)
{
    DOTween.To(() => ve.resolvedStyle.opacity,
            x => style.opacity = new StyleFloat(x), alphaValue, duration);
}
```
透明をアニメーションさせる場合は、このようなコードになります。
ポイントとしては、以下の現在の透明度を取得部分です。

```cs
// 現在の透明度
var currentOpacity = ve.resolvedStyle.opacity;
```
VisualElementクラスの`resolveStyle`プロパティから透明度(opacity)を取得する必要があります。[^1]
[^1]: `ve.style.opacity`と単純にVisualElementのstyleプロパティから透明度を取得しようとしてもダメ

### AnimationClipでアニメーション実装

デフォルト状態では**無理**(だと思う)。

AnimationClipは**シリアライズされるもの**しか扱えません。
UXML内のオブジェクトにAnimationClipがアクセスすることが出来ないため、工夫が必要になりそうです。少なくともデフォルト状態では使えません。


### UI Toolkitのアニメーション実装のまとめ

- スクリプト実装一択**(デフォルト状態ではAnimationClipは使えません)**
- UIパーツの配置座標はそれぞれ(0, 0, 0)
- 現在の透明度は`resolveStyle`から取得する
- DOTween便利



## まとめ

本記事ではUI Toolkitを**ランタイムのUI**として使ってみました。
uGUIと違ってUIパーツの数分GameObjectを作らなくてよいのは新鮮でした。

USSでUIパーツの配置・装飾を定義したら、それを反映するだけで全体の見た目を一括で更新できてしまうのは、**UIレギュレーションを守る機能**として強力だなとも思いました。
(※その分設計が命ということになるののですが...)

今回はあえてUI Toolkitのヘルパー機能`UI Builder`には触れていません。実際にUIToolkitでUIを構築する際はUI Builderを使った方が良いです。こちらはまたの機会に。

- デザイナー**「UXMLで構成要素を定義していく、配置や装飾はUSSで記述していく」**
- エンジニア**「C#で各要素の処理を書いていく、アニメーションを付けていく」**

このような分業が理想ではあるし、UI Toolkitで出来るような気もしますが、ただ現状の機能ではまだまだ課題が多い気もします。僕自身の検証も足りていないため、なんとも言えません。

以下パッと思いついた課題を羅列しています。未解決です。

### 思いつく課題

- GameObjectで作成するより自由度が低い
- ParticleSystemをUIで挟む構造は現状出来そうにない
- UIにポストエフェクトをかけられるか？=>RenderTextureに書き込んでuGUIのRawImageで表示する？
- USSファイルのインポート、使い回し方法
- 複雑なボタンの作り方
- ボタンの長押し対応
- USSからSpriteを利用する方法はある？
- AnimationClipが使えないためデザイナーがUIアニメーションを付けれない？(ソースコードを編集するのはハードル高し)

などなど。

この辺りは引き続き検証していく事になりそうで、まだまだ実戦投入するには早い印象です。
ただHTML/CSS感覚でUnityのUIを作れるのは面白いため試していない方は一度体験してみると良いかと。


今回のサンプルプロジェクトをGithubにアップロードしていますので気になる方はどうぞ。
[baobao/UIToolkitRuntimeUISampler](https://github.com/baobao/UIToolkitRuntimeUISampler)
※本記事の内容は説明のため簡略化していますのでリポジトリ内のファイルとは差分があります

## 最後に

Unityを使ったUI開発について今年のアドベントカレンダーに投稿しています。そちらもよろしくお願い致します。
[【Unity】新規ゲームのUI開発で気をつけた39のTips前編 - Qiita](https://qiita.com/ohbashunsuke/items/0570234362e2e45c0011)


明日は[@glitchpop](https://qiita.com/glitchpop)さんの番です。


↓↓2024年最新UI Tookitまとめはこちら↓↓

https://zenn.dev/ohbashunsuke/articles/unity-uitoolkit-2024



## 参考リンク

- [Comparison of UI systems in Unity - Unity マニュアル](https://docs.unity3d.com/ja/2020.2/Manual/UI-system-compare.html)
- [UI Toolkit | UI Toolkit | 1.0.0-preview.13](https://docs.unity3d.com/Packages/com.unity.ui@1.0/manual/index.html)
- [Overview | UI Builder | 1.0.0-preview.11](https://docs.unity3d.com/Packages/com.unity.ui.builder@1.0/manual/index.html)
- [UIElements: First Steps - Unity Learn](https://learn.unity.com/tutorial/uielements-first-steps#)
- [UIElements Tutorial for Unity: Getting Started | raywenderlich.com](https://www.raywenderlich.com/6452218-uielements-tutorial-for-unity-getting-started#toc-anchor-003)
- [【Unity】【UIElements】USS（スタイルシート）を使って装飾を行う方法まとめ - LIGHT11](https://light11.hatenadiary.com/entry/2020/04/05/121336)