見出し画像

[UEFN&Verse]Verseで編集できるゲームっぽいメッセージウィンドウ

前回上の記事でゲームっぽいメッセージウィンドウを作った
ウィジェットブループリントという便利なUIの編集機能を利用したのだけどこれを使うとVerse上でテキストの編集ができないことが判明した
先々アップデートで可能になりそうだが現状では無理らしい
それができないとゲーム中に表示したいメッセージの数だけ別々のウィジェットブループリントとダイアログを作らなければいけなくなるので非効率だ
なので今回はテキストをVerse上で編集できる方法で作り直してみた


プロジェクトを作る

UEFNを起動してプロジェクトを作成する
テンプレートは何でも良いし、新規に作らなくても既にあるプロジェクト上でも良いのだけど古いプロジェクトを使いまわすとあるはずの機能がなかったりするので新規に作ったほうが安全
記事ではTestProject003という名前で島テンプレートのBlankを指定している

必要なデバイスを配置する

達成したいのは複数のオブジェクトを調べるとそれぞれに違ったメッセージを出す仕組みを1つのVerseファイルで組むことだ

シーンにボタンを3つ配置する

三つのボタンにカスタムメッシュを指定する
適当に岩、木、ボールにした

Verseファイルを作成する

名前は image_dialog_device とした

シーンにimage_dialog_deviceを配置する

レイアウトのあたりをつける

今回はウィンドウのレイアウトをVerse上で作るのだけど、各パーツの座標やサイズを数値で入力するのはけっこう難しいので、ウィジェットブループリントで配置を決め、そこから数値をもらうと良い

ウィンドウ用のテクスチャをコンテンツブラウザに登録する

画像はこちらのサイトからお借りした

ウィジェットブループリントを生成する

ダイアログが開くので Modal Dialog Variant を選択する

コンテンツブラウザにNewWidgetBlueprintが作られるのでダブルクリックでエディタを開く

このようにレイアウトを組む

エディターの細かい使い方はこちらを参考にして欲しい

今回ちょっと変えたのは全てのパーツのアンカーをすべてセンターに指定したことだ

Verseファイルを編集する

これが今回の全コード

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

StringToMessage<localizes>(value:string)<computes> : message = "{value}"


# See https://dev.epicgames.com/documentation/en-us/uefn/create-your-own-device-in-verse for how to create a verse device.

# A Verse-authored creative device that can be placed in a level
image_dialog_device := class(creative_device):

    var widgetMap : [player]widget = map{}

    @editable _Button1 : button_device = button_device{}
    @editable _Button2 : button_device = button_device{}
    @editable _Button3 : button_device = button_device{}

    Msg1 : string = "これは岩のようだ"
    Msg2 : string = "これは木のようだ"
    Msg3 : string = "これはボールのようだ"

    OnBegin<override>()<suspends>:void=
        _Button1.InteractedWithEvent.Subscribe(InteractedWithButton1)
        _Button2.InteractedWithEvent.Subscribe(InteractedWithButton2)
        _Button3.InteractedWithEvent.Subscribe(InteractedWithButton3)
  
    InteractedWithButton1(Agent : agent) : void= AddWidget(Agent, Msg1)
    InteractedWithButton2(Agent : agent) : void= AddWidget(Agent, Msg2)
    InteractedWithButton3(Agent : agent) : void= AddWidget(Agent, Msg3)
                        

    AddWidget(Agent : agent, Msg : string) : void=
        if:
            Player := player[Agent]
            PlayerUI := GetPlayerUI[Player]
        then:
            MessageWidget := CreateWidget(Msg)
            InputMode := player_ui_slot:
                InputMode := ui_input_mode.All
            PlayerUI.AddWidget(MessageWidget, InputMode)
            if:
                set widgetMap[Player] = MessageWidget
                              

    CreateWidget(Msg : string) : canvas=
        var btn : button_quiet = button_quiet{DefaultText := StringToMessage("閉じる")}
        btn.OnClick().Subscribe(OnButtonClicked)

        MessageWidget : canvas = canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -725.0, Top := 36.0, Right := 1457.0, Bottom := 418.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := texture_block:
                        DefaultImage := box_blue
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -658.0, Top := 142.0, Right := 1319.0, Bottom := 210.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := text_block{DefaultText := StringToMessage(Msg), DefaultTextColor :=  NamedColors.White}
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -132.0, Top := 387.0, Right := 260.0, Bottom := 40.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := btn
        return MessageWidget

    OnButtonClicked(WidgetMessage : widget_message) : void=
        if:
            PlayerUI := GetPlayerUI[WidgetMessage.Player]
            MessageWidget := widgetMap[WidgetMessage.Player]
        then:
            PlayerUI.RemoveWidget(MessageWidget)
        return

ザックリ説明すると
まずは必要な機能のインポート

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

次は、UIのボタンテキストやテキストフィールドに文字を指定するためにmessageという型を使うがVerseで文字を扱うときは普通stringを使うので
string -> messageに変換する必要がある
そのための関数定義

StringToMessage<localizes>(value:string)<computes> : message = "{value}"

次は、ボタン毎の処理
シーンに配置したすべてのボタンをVerseに登録
それぞれのボタンに対応するメッセージを定義
OnBegin()でそれぞれのボタンが押された時の関数を登録

今回は配置したボタンは3つだが、遊べるゲームを作ろうとしたら100個ぐらいになるかもしれない
スマートなコーディングとは言えないけど今のUEFNではこんなものだ
将来的には改善されるのだと思う

    @editable _Button1 : button_device = button_device{}
    @editable _Button2 : button_device = button_device{}
    @editable _Button3 : button_device = button_device{}

    Msg1 : string = "これは岩のようだ"
    Msg2 : string = "これは木のようだ"
    Msg3 : string = "これはボールのようだ"

    OnBegin<override>()<suspends>:void=
        _Button1.InteractedWithEvent.Subscribe(InteractedWithButton1)
        _Button2.InteractedWithEvent.Subscribe(InteractedWithButton2)
        _Button3.InteractedWithEvent.Subscribe(InteractedWithButton3)
  
    InteractedWithButton1(Agent : agent) : void= AddWidget(Agent, Msg1)
    InteractedWithButton2(Agent : agent) : void= AddWidget(Agent, Msg2)
    InteractedWithButton3(Agent : agent) : void= AddWidget(Agent, Msg3)

次は、それぞれのボタンが押されたときに違うメッセージでウィンドウを構築して登録するための関数

    AddWidget(Agent : agent, Msg : string) : void=
        if:
            Player := player[Agent]
            PlayerUI := GetPlayerUI[Player]
        then:
            MessageWidget := CreateWidget(Msg)
            InputMode := player_ui_slot:
                InputMode := ui_input_mode.All
            PlayerUI.AddWidget(MessageWidget, InputMode)
            if:
                set widgetMap[Player] = MessageWidget

この行は生成したウィジェットをプレイヤーと紐づけて保存している

            if:
                set widgetMap[Player] = MessageWidget

プレイヤーは複数人いる可能性があり、それぞれのプレイヤーがウィンドウを開くのでmap(プレイヤー:生成されたウィジェット)の形で保存している
理由はクローズボタンが押されたときに破棄するため

なぜif文なのかといえばmapへの値の代入が失敗コンテキストだからだ
なので本来は失敗したときの処理をelse:に書かないといけないのだけど深追いはしないことにする

次の関数が今回のポイントとなるUIを定義している部分
ウィジェットブループリントエディターでやっていることと同じようにcanvasを生成しその下に必要なパーツを挿入している

    CreateWidget(Msg : string) : canvas=
        var btn : button_quiet = button_quiet{DefaultText := StringToMessage("閉じる")}
        btn.OnClick().Subscribe(OnButtonClicked)

        MessageWidget : canvas = canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -725.0, Top := 36.0, Right := 1457.0, Bottom := 418.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := texture_block:
                        DefaultImage := box_blue
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -658.0, Top := 142.0, Right := 1319.0, Bottom := 210.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := texture_block{DefaultText := StringToMessage(Msg), DefaultTextColor :=  NamedColors.White}
                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -132.0, Top := 387.0, Right := 260.0, Bottom := 40.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := btn
        return MessageWidget
  • texture_block ウィンドウの枠となるテクスチャ

  • texture_block 文章を表示するテキスト

  • button_quiet ウィンドウを閉じるためのボタン

の3つを登録している

パーツごとに見ていく

                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -725.0, Top := 36.0, Right := 1457.0, Bottom := 418.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := texture_block:
                        DefaultImage := box_blue
                        DefaultDesiredSize := vector2{X := 1000.0, Y := 1000.0}

最初のパーツはウィンドウ枠のテクスチャ
Anchorsはアンカーの位置のことで今回はセンターを指定しているので
Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}
固定的にこのような指定になる
OffsetsはLeft,Top,Right,Bottomにウィジェットブループリントエディターで指定した位置x、位置Y、サイズX、サイズYをコピーすればよい
Alignmentも同様にエディターからコピーすればいい

Widget := texture_block:
  DefaultImage := box_blue
ここでテクスチャパーツを生成している
DefaultImageに最初に登録したウィンドウ画像のファイル名を指定する
Verse ExplorerからAssets.digest.verseというファイルを開くと

このような形で自動的にテクスチャが登録されていることが確認できる

using {/Verse.org/Assets}
box_blue<scoped {TestProject003}>:texture = external {}

次はウィンドウの上にテキストフィールドパーツを置く
Offsets,Alignment,SizeToContentはすべてエディターの値をコピーする

                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -658.0, Top := 142.0, Right := 1319.0, Bottom := 210.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := text_block{DefaultText := StringToMessage(Msg), DefaultTextColor :=  NamedColors.White}

Widget := text_block{DefaultText := StringToMessage(Msg), DefaultTextColor := NamedColors.White}
ここでテキストフィールドを生成している
DefaultTextに表示したいテキストを指定するのだが、ここに引数で受け取った値を代入している
テキストカラーは白を指定

最後のブロックはウィンドウを閉じるためのボタン

                canvas_slot:
                    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                    Offsets := margin{Left := -132.0, Top := 387.0, Right := 260.0, Bottom := 40.0}
                    Alignment := vector2{X := 0.0, Y := 0.0}
                    SizeToContent := false
                    Widget := btn

ボタンインスタンスは事前に生成し、押された時にコールするイベントハンドラを登録している

        var btn : button_quiet = button_quiet{DefaultText := StringToMessage("閉じる")}
        btn.OnClick().Subscribe(OnButtonClicked)

ウィンドウを閉じるためのボタンが押された時のイベントハンドラ

    OnButtonClicked(WidgetMessage : widget_message) : void=
        if:
            PlayerUI := GetPlayerUI[WidgetMessage.Player]
            MessageWidget := widgetMap[WidgetMessage.Player]
        then:
            PlayerUI.RemoveWidget(MessageWidget)
        return

プレイヤーUIに登録したウェジットを破棄しウィンドウをクローズする

これでやりたいことは達成できたが最後に細かい調整
ボタンデバイスを選択したときのメッセージはデフォルトでは「INTERACT」なのだけどボタンのプロパティのインタラクトテキストを変更することで好きなテキストに変更できるので「調べる」に変更した

いい感じだ


この記事が気に入ったらサポートをしてみませんか?