【UEFN】ボタンに反応するUIをVerseで作成する(Verseの学習⑨)

ボタンをもったUIでの処理

VerseのUI機能

VerseのUI機能は現状で決して充実しているとは言えず、今後のアップデートが期待されている項目の一つです。 今後、機能拡張があるとはいえ、基本的には現状ある機能の流れを踏襲した形になると思われ、現状の機能を理解しておくことは、今後の開発の大きな手助けになるはずです。

(また、UMG ViewModelの機能が、24.20までは使用できていたのですが、記事作成時点の24.40バージョンでは、バグにより使用できず、Verseなしで、ボタンを持ったGUIを独自にカスタムすることが現状できなくなっているように思われます)

EPIC本家のページでもUIに関するVerseのドキュメントはありますが、ボタンとの連携といった部分はないようでしたので、今回は、ボタンを持ったPopupDialogとしての使用方法を中心に説明していきたいと思います。

今回開発するもの

  • ゲーム上のボタンデバイスを押した際に、PopUpDialogでUIを表示する
  • PopUpされたUIには、上部のメッセージと、2つのボタンがある
  • ボタンを押した際に、そのボタンに応じた処理を実行する

簡単なテキストをVerseから表示するプログラム

まず、最初に、テキストが表示されるまでの部分を組んでみます。

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

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

fruit_select_ui_device := class(creative_device):
  @editable
  _FruitSelectButton : button_device = button_device{}

  OnBegin<override>()<suspends>:void=
    _FruitSelectButton.InteractedWithEvent.Subscribe(OnInteractedWithFruitsSelectButton)
  
  OnInteractedWithFruitsSelectButton(Agent : agent) : void=
    if:
      Player := player[Agent]
      PlayerUI := GetPlayerUI[Player]
    then:
      FruitWidget := canvas:
        Slots := array:
          canvas_slot:
            Widget := text_block{DefaultText := StringToMessage("Hello World!")}
      PlayerUI.AddWidget(FruitWidget)
    return

ポイントとしては、この部分です。

文字列をMessage型に変換するための簡単な関数を準備しています。

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

文字列のままでは画面に表示することができないため、こうしてmessage型に変換することが必要となります。

Verseで、UIを作成しようとすると非常によく使うコードですので、この書き方は覚えておいた方がいいかと思います。

Widgetの形でUIパーツをまず作成し、ボタンに触ったプレイヤーのUIに作成したWidgetを追加するという流れになります。

canvas_slotで区切られた部分がtextのパーツ部分です。このようにslotと呼ばれる単位でパーツを追加していきます。

          canvas_slot:
            Widget := text_block{DefaultText := StringToMessage("Hello World!")}

VerseをビルドしてできるCreativeDeviceに、適当なボタンを割り付け、実行してみます。

ボタンを押した際に、画面左上に”Hello, Worldと表示されれば正解です。

画面左上にHello World

ボタン2個とテキスト1個を表示するプログラム

では、次にボタンを2個追加し、レイアウトも調整します。

OnInteractedWithFruitsSelectButton_step01(Agent : agent) : void=

  if:
    Player := player[Agent]
    PlayerUI := GetPlayerUI[Player]
  then:
    FruitWidget := 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 := -175.0, Top := -50.0, Right := 350.0, Bottom := 50.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := text_block:
            DefaultText := StringToMessage("Which Do You Like?")
            DefaultTextColor := NamedColors.Black
            DefaultJustification := text_justification.Center
        canvas_slot:
          Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
          Offsets := margin{Left := -300.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := button_loud{DefaultText := StringToMessage("Ringo")}
        canvas_slot:
          Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
          Offsets := margin{Left := 20.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := button_loud{DefaultText := StringToMessage("Banana")}

    InputMode := player_ui_slot:
      InputMode := ui_input_mode.All
    PlayerUI.AddWidget(FruitWidget, InputMode)

  return

テキスト1つとボタン2つになったため、canvas_slotの部分が3つに増えています。

ボタンの部分は以下のようになっており、様々な調整項目がセットされています。

この部分を例にとり、解説していきます。

canvas_slot:
    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
    Offsets := margin{Left := -300.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
    Alignment := vector2{X := 0.0, Y := 0.0}
    SizeToContent := false
    Widget := button_loud{DefaultText := StringToMessage("Ringo")}

上記の部分のプロパティは、WidgetBlueprintで作成する場合の値と同一の値になるため、WidgetBlueprintを仮に作成して、レイアウトの調整はGUI上で実施すればデザインしやすいかと思います。

どういったことか説明するために、WidgetBlueprintを一つ作成し、そこにCanvasとボタンを一つ配置して説明していきます。

Widget Blueprintの作成

キャンバスの作成とボタンの配置

Verse上のコードと、WidgetBlueprintの対応関係はこのようになっています。

VerseとWidgetBlueprintの対応

このような形で、WidgetBlueprint上で、レイアウトを行って、その値をVerseコードに持っていけば、スムーズにレイアウトすることができます。

コードを実行し、ボタンを押した際に、以下のように表示されれば、正常に動作しています。

テキストと2つのボタンを持ったUI

ただ、このままでは、ボタンを押すことができません。ここで、ボタンを押せるようにさらにプログラムを改修していきます。

ボタンを押すとメッセージを表示するプログラム完成版

2つのパーツに分けて解説していきます。 まず、前半部分を以下のように変えます。

fruit_select_ui_device := class(creative_device):
  @editable
  _FruitSelectButton : button_device = button_device{}

  _RingoButton : button_loud = button_loud{}
  _BananaButton : button_loud = button_loud{}
  

  OnBegin<override>()<suspends>:void=

    _RingoButton.SetText(StringToMessage("Ringo"))
    _RingoButton.OnClick().Subscribe(OnRingoButtonClicked)

    _BananaButton.SetText(StringToMessage("Banana"))
    _BananaButton.OnClick().Subscribe(OnBananaButtonClicked)

    _FruitSelectButton.InteractedWithEvent.Subscribe(OnInteractedWithFruitsSelectButton)

RingoButtonとBananaButtonをまず、事前に準備しています。

そのボタンを押したときの動作としてOnRingoButtonClicked関数と、OnBananaButtonClicked関数を登録しています。

次に後半部分です。

  ## map
  var _FruitWidgetMap : [player]widget = map{}

  OnInteractedWithFruitsSelectButton(Agent : agent) : void=
    if:
      Player := player[Agent]
      PlayerUI := GetPlayerUI[Player]
    then:
      FruitWidget := 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 := -175.0, Top := -250.0, Right := 350.0, Bottom := 50.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := text_block:
              DefaultText := StringToMessage("Which Do You Like?")
              DefaultTextColor := NamedColors.Black
              DefaultJustification := text_justification.Center
          canvas_slot:
            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
            Offsets := margin{Left := -300.0, Top := -60.0, Right := 280.0, Bottom := 130.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := _RingoButton
          canvas_slot:
            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
            Offsets := margin{Left := 20.0, Top := -60.0, Right := 280.0, Bottom := 130.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := _BananaButton

      InputMode := player_ui_slot:
        InputMode := ui_input_mode.All
      PlayerUI.AddWidget(FruitWidget, InputMode)

      ### save widget info
      if:
        set _FruitWidgetMap[Player] = FruitWidget


  OnRingoButtonClicked(WidgetMessage : widget_message) : void=
    HideWidget(WidgetMessage.Player)
    ### Process for Ringo
    Print("Ringo")

     return

  OnBananaButtonClicked(WidgetMessage : widget_message) : void=
    HideWidget(WidgetMessage.Player)
    ### Process for Banana
    Print("Banana")
    return

  HideWidget(Player : player) : void = 
    if:
      PlayerUI := GetPlayerUI[Player]
      FruitWidget := _FruitWidgetMap[Player]
    then:
      PlayerUI.RemoveWidget(FruitWidget)
      
    return

Widgetはボタンを押したプレイヤーにのみ、表示する必要があります。

そのため、ボタンを押し、Widgetが表示されたプレイヤーがどのWidgetを保持しているのか、保存しておく必要があります。そのため、これを覚えておくための変数を準備しています。

  var _FruitWidgetMap : [player]widget = map{}

canvas_slot登録時は事前に作成しておいたボタンを登録するように変えています。

          canvas_slot:
            (中略)
            Widget := _RingoButton

ボタンが入力を受け取れるように、InputModeを変更して登録しています。

      InputMode := player_ui_slot:
        InputMode := ui_input_mode.All
      PlayerUI.AddWidget(FruitWidget, InputMode)

ボタンを押したプレイヤーと、Widgetの関係を記録しておきます。

      if:
        set _FruitWidgetMap[Player] = FruitWidget

ボタンを押した際には、Widgetをプレイヤーから削除しています。

  HideWidget(Player : player) : void = 
    if:
      PlayerUI := GetPlayerUI[Player]
      FruitWidget := _FruitWidgetMap[Player]
    then:
      PlayerUI.RemoveWidget(FruitWidget)
      
    return

あとは、適宜、ボタンを押した際の処理を、OnRingoButtonClicked関数とOnBananaButtonClicked関数の中に実装していけば完成となります。

自分の作例では、リンゴとバナナを出現させる処理を書いています。

ボタンを押したらリンゴが出現

まとめ

今回は、VerseからUIを作成する方法について解説しました。Verseからは画像が使用できず、まだまだ機能として十分とは言えませんが、今後、UI周りは大きく改善していくと思われ、一度、現状でできることを確認しておくのがいいのではないでしょうか。

やや、後半、コードの難易度があがってしまっており、あまりに記事が長くなってしまったため、そこを駆け足で説明してしまっています。

説明が足りていない部分もあるかと思いますので、わからない点があれば、ご質問ください。どういった点を説明すればいいのか、参考にさせていただければと思います。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com