3
1

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.

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

Last updated at Posted at 2023-10-05

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

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

output2.gif

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

IMG_7103.jpg

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

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

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

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

今回の完成コード

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実装の公式ドキュメントはこちらなのですが、正直よくわからんということでコード見ながら少しずつ理解していきましょう。

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

image.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言語ではクラス名はスネークケースで小文字なので混乱せず見てください。(参考ドキュメント)

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

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

image.png

Widget構造としては

  • canvas
    • canvas_slot
      • texture_block
      • texture_block
      • texture_block
      • texture_block

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

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

コードの部分で言うと

# 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を構成する実装がこちら

 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のパラメータ相互関係チートシート」も理解の助けになるので参考にしてみてください。

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

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

image.png

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

左から

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

image.png

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

image.png

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

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

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

image.png

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

image.png

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

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

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

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

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

image.png

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

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

image.png

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

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

image.png

    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に登録する方法

    var TextureWidget:texture_block = texture_block{DefaultImage := Textures.iwaken2}

このコードのように、Textureの素材をVerseのコードから呼ぶためには以下の手順が必要です。(公式ドキュメントはこちら)

  • Content以下に「Textures」フォルダを作る (名前は何でもよい)

    • Texturesフォルダ以下にTextureデータをドラッグ&ドロップ (png,jpgなど)
    • すべてを保存
  • Menuバーの[Verse]>[Build Verse Code]からビルドする
    image.png

  • Verse Explorerから[Assets.digest.verse]を開く

  • ファイル名と同じ変数が追加されていたら成功

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ソースコードを読みつつ構造を理解していく所存です。

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

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

参考文献

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?