[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」なのだけどボタンのプロパティのインタラクトテキストを変更することで好きなテキストに変更できるので「調べる」に変更した
いい感じだ