[UEFN][Verse] UIにImage/Textureを貼る実装。これからVerseやっていきな人へ

こんにちは、Unity/Unrealエンジニアの[イワケン]()です。
最近はUEFNを楽しんでいます。

今回の目標はこのように、UEFN (Fortnite) のUIにTextureを貼るのがゴールです。
私のプロフィール写真をUIとして貼り付けています。

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/d8fb7397-cf7a-06c2-6c84-6ed8826b2bba.gif)

このようにマルチプレイに対応しています。Switchで同じパーティでMapに入っても正しくUIが表示されています。

![IMG_7103.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/9ca194d6-86e8-0bce-4018-83bbe73c4a91.jpeg)


私たちのようなUnity/Unrealはちょっとできるけど「これからUEFN/Verseやっていき」な人にとって
UEFNのTextureのUI実装は3つの壁があります。

- ①VerseでのUI実装がよくわからん
- ②Textureの貼り付けよくわからん
- ③マルチプレイ対応したUI実装がよくわからん

③について、今回のコードでは対応していますが、長くなるので解説は別記事にしたいと思います。簡単に言うと、シングルプレイではうまくいくけど、マルチプレイになるとうまくいかないという実装を避けるための実装です。


今回は**①②の疑問について解決しつつ、サンプルコード付きで手順について示したいと思います。**
私もよくわからんと思いながらドキュメントや生のソースコード見ながら理解しようとしています。一緒に理解していきましょう。

# 今回の完成コード

```python:ui_practice_device.verse

using {/Fortnite.com/Devices }
using {/Fortnite.com/UI}
using {/UnrealEngine.com/Temporary/UI}
using {/UnrealEngine.com/Temporary/SpatialMath}
using {/Verse.org/Assets}
using {/Verse.org/Simulation}
using {/Verse.org/Colors}

ui_practice_device := class(creative_device):

    var WidgetClassPerPlayer: [player]MyWidgetClass = map{}

    @editable
    Button:button_device = button_device{}

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        InitializeUI()
        Button.InteractedWithEvent.Subscribe(OnPushedButton)

    InitializeUI():void=
        AllPlayers := GetPlayspace().GetPlayers()
        for(Player : AllPlayers, WClass := set WidgetClassPerPlayer[Player] = MyWidgetClass{}):
            WClass.AddUI(Player)

    OnPushedButton(Agent:agent):void=
        InitializeUI()


MyWidgetClass := class:
    ImageSize:vector2 = vector2{X := 256.0, Y := 256.0}
    var TextureWidget:texture_block = texture_block{DefaultImage := Textures.iwaken2}
    var TextureWidget2:texture_block = texture_block{DefaultImage := Textures.iwaken1}
    var TextureWidget3:texture_block = texture_block{DefaultImage := Textures.iwaken2}
    var TextureWidget4:texture_block = texture_block{DefaultImage := Textures.iwaken1}

    AddUI(Agent:agent):void=
        if(Player := player[Agent], PlayerUI := GetPlayerUI[Player]):
            Widget:canvas = CreateWidget(Agent)
            PlayerUI.AddWidget(Widget)

    CreateWidget(Agent:agent):canvas=

        set TextureWidget = texture_block{DefaultImage := Textures.iwaken2,  DefaultDesiredSize := ImageSize}
        set TextureWidget2 = texture_block{DefaultImage := Textures.iwaken1,  DefaultDesiredSize := ImageSize}
        set TextureWidget3 = texture_block{DefaultImage := Textures.iwaken2,  DefaultDesiredSize := ImageSize}
        set TextureWidget4 = texture_block{DefaultImage := Textures.iwaken1,  DefaultDesiredSize := ImageSize}

        MyCanvas: canvas = canvas:
            Slots := array:
                # 左
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.0, Y:=0.5}, Maximum := vector2{X:=0.0, Y:=0.5}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.0, Y:=0.5}
                    SizeToContent := true
                    Widget := TextureWidget
                # 上
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.5, Y:=0.0}, Maximum := vector2{X:=0.5, Y:=0.0}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.5, Y:=0.0}
                    SizeToContent := true
                    Widget := TextureWidget2
                # 右
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=1.0, Y:=0.5}, Maximum := vector2{X:=1.0, Y:=0.5}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=1.0, Y:=0.5}
                    SizeToContent := true
                    Widget := TextureWidget3
                # 下
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.5, Y:=1.0}, Maximum := vector2{X:=0.5, Y:=1.0}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.5, Y:=1.0}
                    SizeToContent := true
                    Widget := TextureWidget4
        return MyCanvas
```

# VerseによるUEFNのUI実装

## VerseのUI実装の全体像

UI実装の公式ドキュメントは[こちら](https://dev.epicgames.com/documentation/ja-jp/uefn/creating-and-removing-widgets-in-unreal-editor-for-fortnite)なのですが、正直よくわからんということでコード見ながら少しずつ理解していきましょう。

Verseの生のコード「`UnrealEngine.digest.verse`」を読むと、次のようなclass関係をPlantUMLにすると次のようになります。(ChatGPTにPlantUMLを出してもらった)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/dc576791-4667-1e73-7af3-28170a2b3e28.png)

ここで大事な構造の理解が

- playerがplayer_uiを持つ
- player_uiがwidgetを持つ (つまり、playerがwidgetを持つ)
- **widgetにはたくさんの種類がある**
  - canvas
  - button_load (ボタンのUI要素)
  - texture_block (TextureのUI要素)
  - text_block (文字のUI要素)
  - ...
- **canvasの中にwidgetを複数持つことができる** (厳密にはcanvas_slotの中)

ここで、widget(ウィジェット)とはUIの要素だと思ってください。
実践的にはcanvas (widgetの子クラス) をplayerに付与し、canvasの中にwidgetのパーツを複数配置することが多いです (UnityやUnrealの実装的にもそれが多い)

ちなみに、Verse言語ではクラス名はスネークケースで小文字なので混乱せず見てください。([参考ドキュメント](https://dev.epicgames.com/documentation/ja-jp/uefn/verse-code-style-guide-in-unreal-editor-for-fortnite))

## Canvasと子供のwidgetの関係を視覚化すると 

例えば、先ほどのUIはこうなっているとも言えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/1b588021-ee06-971b-44ea-b3ad3c9063dc.png)

Widget構造としては
- canvas
    - canvas_slot
        - texture_block
        - texture_block
        - texture_block
        - texture_block

## PlayerにUI(Widget)を貼り付ける

そして一番上のcanvasを各プレイヤーにAddWidgetすることでUIに張り付ける。というイメージです。

コードの部分で言うと


```python
# Playerにwidget(canvas)をAddWidget
AddUI(Agent:agent):void=
    if(Player := player[Agent], PlayerUI := GetPlayerUI[Player]):
        Widget:canvas = CreateWidget(Agent)
        PlayerUI.AddWidget(Widget)
```

agentはplayerの親クラスです。
引数がAgentなのは、`Player := player[Agent]`とすると、Agentからplayerを取り出せるからです (ここの理解についてはこれからなので後日記事に)

## canvasの構成
ではcanvasを構成する実装がこちら

```python
 CreateWidget(Agent:agent):canvas=
        set TextureWidget = texture_block{DefaultImage := Textures.iwaken2,  DefaultDesiredSize := ImageSize}
        set TextureWidget2 = texture_block{DefaultImage := Textures.iwaken1,  DefaultDesiredSize := ImageSize}
        set TextureWidget3 = texture_block{DefaultImage := Textures.iwaken2,  DefaultDesiredSize := ImageSize}
        set TextureWidget4 = texture_block{DefaultImage := Textures.iwaken1,  DefaultDesiredSize := ImageSize}

        MyCanvas: canvas = canvas:
            Slots := array:
                # 左
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.0, Y:=0.5}, Maximum := vector2{X:=0.0, Y:=0.5}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.0, Y:=0.5}
                    SizeToContent := true
                    Widget := TextureWidget
                # 上
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.5, Y:=0.0}, Maximum := vector2{X:=0.5, Y:=0.0}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.5, Y:=0.0}
                    SizeToContent := true
                    Widget := TextureWidget2
                # 右
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=1.0, Y:=0.5}, Maximum := vector2{X:=1.0, Y:=0.5}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=1.0, Y:=0.5}
                    SizeToContent := true
                    Widget := TextureWidget3
                # 下
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X:=0.5, Y:=1.0}, Maximum := vector2{X:=0.5, Y:=1.0}}
                    Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
                    Alignment := vector2{X:=0.5, Y:=1.0}
                    SizeToContent := true
                    Widget := TextureWidget4
        return MyCanvas
```

canvasはSlotsというUIの要素を持つ配列を持っています。

今回は4つのTextureをUIに表示したいので、4つのtexture_block (widgetの子クラス) を準備して、Slotsに当てはめていきます。

そこで

「Anchorsって何？Offsetsって何？」と人々は混乱するでしょう。

直感的に数値を理解するためには、次のように実際のWidget上に作ってみることをお勧めします。
こちらの記事「[[UEFN][Verse] canvas_slotのパラメータ相互関係チートシート](https://zenn.dev/t_tutiya/articles/c5b137947ec5e2)」も理解の助けになるので参考にしてみてください。

## Widgetの編集画面で数値を確認する

- UEFNのContent Browserから右クリックで[User Interface]>[Widget BluePrint]を選択します。
- 次の項目では[User Widget]を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/3634a05a-37af-b7eb-8daf-56a71dfd9ed5.png)

そうするとWidgetの編集画面が現れます。

左から

- [Canvas Panel]の追加
- [Screen Size]から好きなサイズを選択
    - この記事では1080i,1080pを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/2d961ec1-ee79-759b-f88b-939e3252fb05.png)

- [Image]を4つ配置
- 大きさと場所を適切な場所にする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/c8c4048d-361a-0f28-75d1-56c4a5541f6f.png)

さて、ここから「Anchor」の設定をしていきますが、正直理解するのは難しいです。
私のAnchorの目的は「画面サイズが変わってもいい感じに配置したい」というのが目的だと思っています。
なので、今回は「各画像は画面の縁にくっついてほしい」という仕様を目指しつつ、具体的にAnchorを設定してみましょう。

一番上のImageのAnchorについて設定してみましょう。

- Imageを選択
- [Anchors]のプルダウンを開く
    - 画面上側に位置しているのを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/7e090480-99f2-0078-4bf8-5fde8a52ac7e.png)

白いマークが移動しているのがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/c0a42c75-0b4d-ba06-a310-1fb9adc8f52f.png)


[Anchor]はこれで設定できました。

では、Anchorに対して、Imageをどう配置するか決めたいです。

- わかりやすいのが[Size]
    - 今回は`256`と入力
- わからんのが
    - [Offset]
    - [Position]
    - [Alignment]

今回のAnchorではoffsetはなしでいいそうです (いったんそういうことで)

今回のImageに対して、Anchorと、Imageの上部の部分を一致させたいとします。
Alignmentは「位置合わせ」の意味で「Pivot」に近いです。図の中心です。
これを上部で位置合わせするために Alignmentを`(0.5,1.0)`で設定します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/0729f7c2-4739-024a-1737-9145321fdc06.png)

最終的に以下のような設定にします


|項目|値|
|---|---|
|PositionX|0.0|
|PositionY|0.0|
|SizeX|256.0|
|SizeY|256.0|
|Alignment|(0.5,0.0)|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/35073915-2134-1246-3630-90b0069241fb.png)

他のImageも同じように試してみましょう。

そうすると、Verseで書くべき値がわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/190668c5-3572-f9e3-2c6a-f5250af299d1.png)

```python
    canvas_slot:
        Anchors := anchors{Minimum := vector2{X:=0.5, Y:=0.0}, Maximum := vector2{X:=0.5, Y:=0.0}}
        Offsets := margin{Top := 0.0,Left := 0.0, Right := 0.0, Bottom := 0.0}
        Alignment := vector2{X:=0.5, Y:=0.0}
        SizeToContent := true
        Widget := TextureWidget2
```

えっ、編集したWidgetをそのままUIに貼れないんですか？についてはこれから調査します (できてほしいんだが...)

# ②TextureをVerseのコードから呼び出す方法

## TextureをAssets.digest.verseに登録する方法

```python
    var TextureWidget:texture_block = texture_block{DefaultImage := Textures.iwaken2}
```
このコードのように、Textureの素材をVerseのコードから呼ぶためには以下の手順が必要です。(公式ドキュメントは[こちら](https://dev.epicgames.com/documentation/ja-jp/uefn/exposing-assets-to-verse-in-unreal-editor-for-fortnite))

- Content以下に「Textures」フォルダを作る (名前は何でもよい)
    - Texturesフォルダ以下にTextureデータをドラッグ&ドロップ (png,jpgなど)
    - すべてを保存
- Menuバーの[Verse]>[Build Verse Code]からビルドする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/ce578210-5377-9ce6-661e-6d8e525f6ebf.png)

- Verse Explorerから[Assets.digest.verse]を開く
- ファイル名と同じ変数が追加されていたら成功

```python:Assets.digest.verse
using {/Verse.org/Assets}
Textures := module:
    iwaken2<scoped {UIPractice}>:texture = external {}

    iwaken1<scoped {UIPractice}>:texture = external {}
```

追加されている。嬉しい～～～

## TextureをVerseコードで使用する方法

- `using {/Verse.org/Assets}`を追加
- `{フォルダ名/module名}.{ファイル名/変数名}`として使用可能
    - 例 `Textures.iwaken2`


これでエラーが出なければ成功です。


# まとめ

VerseのUI実装は私にとってまだ難しいです。ドキュメントを呼んでもわからないことがたくさんです。生のVerseソースコードを読みつつ構造を理解していく所存です。

こういったサンプルコードによって多くの人が救われることを祈っています。

もし、もっとこういう実装の方がいいよとアドバイスがあれば積極的に受け取りたいです。


# 参考文献

- 公式ドキュメント
  - [インゲーム ユーザー インターフェース (UI)](https://dev.epicgames.com/documentation/ja-jp/uefn/creating-in-game-ui-in-verse) 
  - [UEFN 内のアセットを Verse に公開する](https://dev.epicgames.com/documentation/ja-jp/uefn/exposing-assets-to-verse-in-unreal-editor-for-fortnite) 
- UI実装の参考
    - [t_tutiyaさんのZenn記事 [UEFN][Verse] canvas_slotのパラメータ相互関係チートシート](https://zenn.dev/t_tutiya/articles/c5b137947ec5e2)
- マルチプレイ対応の実装の参考
    - [ごろ助 / GorosukeさんのマルチプレイヤーゲームにおけるVerse UIの処理の解説ツイート](https://twitter.com/GorosukeUEFN/status/1660129740949458945?s=20)