Verse UI - How to keep widgets on first players screen without second player removing them?

In my Verse UI canvas, any widgets that are defined outside of MakeCanvas() are removed from the first player’s screen when a second player presses a button device to display the UI canvas on their screen. I can’t just define all of my UI widgets inside of MakeCanvas(), because I need to reference some of the widgets for other functions.

Could someone explain to me how to assign variables like UI widgets to only the player who presses the button device to initiate their own canvas, instead of to everybody? Thanks!

In this old post I saw that @AxelCapek advises @DolphinDom and @Cleverlike_Studios to define widgets inside of the MakeCanvas() function. https://forums.unrealengine.com/t/normal-uefn-verse-when-multiple-people-have-the-same-canvas-applied-to-them-itll-remove-it-off-the-first-only-one-person-at-a-time-can-have-the-canvas/773150

This works great for widgets that I don’t need to reference in other functions, like my unchanging title text (WidgetTitle in my snippet below). However, I need to define my widgets for the changeable WidgetRow1 text and for the clickable WidgetUIButton outside of the MakeCanvas() because I them for HandleUIButtonClick() and UpdateText(). The UI widgets are not accessible to me for my functions if I trap them in MakeCanvas(). That’s why I defined WidgetRow1 and WidgetUIButton at the beginning of the script, as global variables. But now I see that I don’t want them global, I want them to apply per player.

In addition, since my variable currentTextPart is global, changes with “set currentTextPart” apply to all players, which is bad because I want it to only change for the player who clicks the UI button.

Here’s a simplified version of my code:

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

ui_dialog_simple1 := class(creative_device):

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

    #===== Devices =============================================================
    @editable 
    ButtonUIAdd: button_device = button_device {} 

    #===== Editable Properties ================================================
    @editable 
    TitleText : string = "Title"
    @editable 
    TextRow1: []string = array:
        "Row 1 Part 1"
        "Row 1 Part 2"
        "Row 1 Part 3"

    #===== Variables ==========================================================

    WidgetRow1:text_block = text_block {}
    WidgetUIButton:button_regular = button_regular {} 

    var MaybeMyUIPerPlayer : [player]?canvas = map{}
    var currentTextPart : int = 0 

    #===== Functions ==========================================================

    OnBegin<override>()<suspends>:void=
        Print("UI Popup NextExit Device Began")
        ButtonUIAdd.InteractedWithEvent.Subscribe(HandleButtonUIAddInteraction)
        WidgetUIButton.OnClick().Subscribe(HandleUIButtonClick)

    HandleButtonUIAddInteraction(Agent:agent):void= 
        Print("Button Device Interacted With")
        AddCanvas(Agent) 
        UpdateText(currentTextPart)

    HandleUIButtonClick(Message: widget_message):void= 
        Print("UI Button Clicked")
        if (currentTextPart = TextRow1.Length):
            RemoveCanvas(Message)
            set currentTextPart = 0
  
        else:  
            if (currentTextPart <= TextRow1.Length - 1):
                if (currentTextPart = TextRow1.Length - 1):
                    UpdateText(currentTextPart)
                else:
                    UpdateText(currentTextPart)   
                    
    UpdateText(Index:int):void= 
        Print("Update Text")
        if(currentStr := TextRow1[Index]):
            WidgetRow1.SetText(StringToMessage(currentStr))
        set currentTextPart += 1 

    AddCanvas(Agent:agent):void=
        Print("Add Canvas")
        if (InPlayer := player[Agent], PlayerUI := GetPlayerUI[InPlayer]):
            if (MyUI := MaybeMyUIPerPlayer[InPlayer]?):
                PlayerUI.RemoveWidget(MyUI)
                if (set MaybeMyUIPerPlayer[InPlayer] = false) {}
            else:
                NewUI := CreateMyUI()
                PlayerUI.AddWidget(NewUI, player_ui_slot{InputMode := ui_input_mode.All})
                if (set MaybeMyUIPerPlayer[InPlayer] = option{NewUI}) {}

    CreateMyUI():canvas=
        Print("Create UI")

        WidgetTitle:text_block = text_block: 
            DefaultText := {StringToMessage(TitleText)}

        NewCanvas := canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors:
                        Minimum := vector2{X := 0.5, Y := 0.25}
                        Maximum := vector2{X := 0.5, Y := 0.25}
                    Alignment := vector2{X := 0.5, Y := 0.5}
                    SizeToContent := true
                    Widget := stack_box:
                        Orientation := orientation.Vertical
                        Slots := array:
                            stack_box_slot:
                                Widget := WidgetTitle
                            stack_box_slot: 
                                Widget := WidgetRow1 
                            stack_box_slot:
                                Widget := WidgetUIButton

        return NewCanvas
        
    RemoveCanvas(Message: widget_message):void=
        if (PlayerUI := GetPlayerUI[Message.Player], MyUI := MaybeMyUIPerPlayer[Message.Player]?, SelectedButton := text_button_base[Message.Source]):
            Print("Remove Canvas")
            PlayerUI.RemoveWidget(MyUI)
            if (set MaybeMyUIPerPlayer[Message.Player] = false) {}

The only way I’ve been able to achieve this personally so far is putting the UI into a class and then creating a class object per player, which I store in a map using player as the key. Just as an example, if it helps, this is a snippet I’m doing that in: Height Meter for OnlyUp style maps | Uefn Code Snippet

1 Like

Oof I think I’m out of my depth. I haven’t done much with maps. Would this tutorial help? I just went through it today actually and it showed how to make a UI class but I wasn’t sure how to combine it with my other script. Custom Countdown Timer

Ooo I just saw you posted a snippet, I’ll check it out.

I’m not sure if that one will show on the screen for more than one player, honestly. If you ever want I’m totally willing to jump into a call at some point and go over maps with you if you are interested. I’m about out the door right now but I can explain a bit more when I’m back home later.

1 Like

I would really appreciate that!!! I keep running into situations where I should probably be using maps.

In the meantime I’ll look over your snippet and see if I can make sense of it for my purposes.

1 Like

Hey Twin! I tried to integrate your OnlyUp script into my dialog script and I got partially somewhere but it’s still pretty wacky. :laughing: My goal is a little different from yours because I have a UI button click. I made the class concrete so that I could use editable variables for it, hope that’s ok.

This is what I have so far. What happens in testing is that the canvas appears the first time on Button Device press, which is good. But then the button click gets weird because the sequence goes “Row 1 Part 1” then “Row 1 Part 3” then Part 1 again, then Part 3 again, then deletes the canvas. Instead of going Part 1, Part 2, Part 3, remove canvas. And after the canvas is gone the first time I can’t get it to come back again on button device press :frowning_with_open_mouth:

What do you think is going on here?

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

ui_dialog_simple1 := class(creative_device):

    @editable 
    ButtonUIAdd : button_device = button_device {} 

    @editable 
    Title : string = "Title"

    @editable
    TextRow1: []string = array:
        "Row 1 Part 1"
        "Row 1 Part 2"
        "Row 1 Part 3"
    
    var DisplayMap: [player]?dialog_widget = map{}

    OnBegin<override>()<suspends>:void=
        Print("UI Popup NextExit Device Began")
        ButtonUIAdd.InteractedWithEvent.Subscribe(HandleButtonUIAddInteraction)

    HandleButtonUIAddInteraction(Agent:agent):void=
        Print("Button Device Interacted With")
        AllPlayers := GetPlayspace().GetPlayers()
        for (Player : AllPlayers):
            DialogWidgetInstance := dialog_widget:
                VerseDevice := Self
                InstancePlayer := option{Player} 
                DialogTitle := Title 
                DialogRow1 := TextRow1 
            if (not DisplayMap[Player], set DisplayMap[Player] = option{DialogWidgetInstance}):
                DialogWidgetInstance.CreateMyUI()
                #if (Agent := agent[Player]):
                var currentTextPart : int = 0 
                DialogWidgetInstance.AddCanvas(Agent)
                DialogWidgetInstance.UpdateText(currentTextPart)

    #===== Dialog Widget Class ==========================================================

dialog_widget := class<concrete>():

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

    var VerseDevice : ui_dialog_simple1 = ui_dialog_simple1{}
    InstancePlayer: ?player = false ###

    WidgetRow1:text_block = text_block {}
    WidgetUIButton:button_regular = button_regular {} 

    var MaybeMyUIPerPlayer : [player]?canvas = map{}
    var currentTextPart : int = 0 

    DialogTitle : string = ""
    DialogRow1: []string = array{}

    HandleUIButtonClick(Message: widget_message):void= 
        Print("UI Button Clicked")
        if (currentTextPart = DialogRow1.Length):
            RemoveCanvas(Message)
            set currentTextPart = 0
  
        else:  
            if (currentTextPart <= DialogRow1.Length - 1):
                if (currentTextPart = DialogRow1.Length - 1):
                    UpdateText(currentTextPart)
                else:
                    UpdateText(currentTextPart)   
                    
    UpdateText(Index:int):void= 
        Print("Update Text")
        if(currentStr := DialogRow1[Index]):
            WidgetRow1.SetText(StringToMessage(currentStr))
        set currentTextPart += 1 

    AddCanvas(Agent:agent):void=
        Print("Add Canvas")
        if (InPlayer := player[Agent], PlayerUI := GetPlayerUI[InPlayer]):
            if (MyUI := MaybeMyUIPerPlayer[InPlayer]?):
                PlayerUI.RemoveWidget(MyUI)
                if (set MaybeMyUIPerPlayer[InPlayer] = false) {}
            else:
                NewUI := CreateMyUI()
                PlayerUI.AddWidget(NewUI, player_ui_slot{InputMode := ui_input_mode.All})
                if (set MaybeMyUIPerPlayer[InPlayer] = option{NewUI}) {}

    CreateMyUI():canvas=
        Print("Create UI")
        WidgetUIButton.OnClick().Subscribe(HandleUIButtonClick)
        WidgetTitle:text_block = text_block: 
            DefaultText := {StringToMessage(DialogTitle)}

        NewCanvas := canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors:
                        Minimum := vector2{X := 0.5, Y := 0.25}
                        Maximum := vector2{X := 0.5, Y := 0.25}
                    Alignment := vector2{X := 0.5, Y := 0.5}
                    SizeToContent := true
                    Widget := stack_box:
                        Orientation := orientation.Vertical
                        Slots := array:
                            stack_box_slot:
                                Widget := WidgetTitle
                            stack_box_slot: 
                                Widget := WidgetRow1 
                            stack_box_slot:
                                Widget := WidgetUIButton

        return NewCanvas
        
    RemoveCanvas(Message: widget_message):void=
        if (PlayerUI := GetPlayerUI[Message.Player], MyUI := MaybeMyUIPerPlayer[Message.Player]?, SelectedButton := text_button_base[Message.Source]):
            Print("Remove Canvas")
            PlayerUI.RemoveWidget(MyUI)
            if (set MaybeMyUIPerPlayer[Message.Player] = false) {} 

Here’s my current fix that @Twin01 helped me with. It appears to work. :slight_smile: I created a class called dialog_widget, and created a class object per player which is stored in a map using player as the key. Thanks so much, Twin!

I think there are probably more ways than one to accomplish multiplayer UI though. Has anyone else handled this differently? Does this seem like the best option?

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

ui_dialog_simple1 := class(creative_device):

    @editable 
    ButtonUIAdd : button_device = button_device {} 

    @editable 
    Title : string = "Title"

    @editable
    TextRow1: []string = array:
        "Row 1 Part 1"
        "Row 1 Part 2"
        "Row 1 Part 3"
    
    var DisplayMap: [player]?dialog_widget = map{}

    OnBegin<override>()<suspends>:void=
        Print("UI Popup NextExit Device Began")
        ButtonUIAdd.InteractedWithEvent.Subscribe(HandleButtonUIAddInteraction)

    HandleButtonUIAddInteraction(Agent:agent):void=
        Print("Button Device Interacted With")
        if (Player := player[Agent]):
            DialogWidgetInstance := dialog_widget:
                VerseDevice := Self
                InstancePlayer := option{Player} 
                DialogTitle := Title 
                DialogRow1 := TextRow1 
            if (set DisplayMap[Player] = option{DialogWidgetInstance}):
                DialogWidgetInstance.Init()
                DialogWidgetInstance.CreateMyUI()
                DialogWidgetInstance.AddCanvas(Agent)
                DialogWidgetInstance.UpdateText(DialogWidgetInstance.currentTextPart)
            else:
                Print("Error")

    #===== Dialog Widget Class ==========================================================

dialog_widget := class<concrete>():

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

    var VerseDevice : ui_dialog_simple1 = ui_dialog_simple1{}
    InstancePlayer: ?player = false ###

    WidgetRow1:text_block = text_block {}
    WidgetUIButton:button_regular = button_regular {} 

    var MaybeMyUIPerPlayer : [player]?canvas = map{}
    var currentTextPart : int = 0 

    DialogTitle : string = ""
    DialogRow1: []string = array{}

    Init():void= 
        WidgetUIButton.OnClick().Subscribe(HandleUIButtonClick)

    HandleUIButtonClick(Message: widget_message):void= 
        Print("UI Button Clicked")
        if (currentTextPart = DialogRow1.Length):
            RemoveCanvas(Message)
            set currentTextPart = 0
  
        else:  
            if (currentTextPart <= DialogRow1.Length - 1):
                if (currentTextPart = DialogRow1.Length - 1):
                    UpdateText(currentTextPart)
                else:
                    UpdateText(currentTextPart)   
                    
    UpdateText(Index:int):void= 
        Print("Update Text")
        if(currentStr := DialogRow1[Index]):
            WidgetRow1.SetText(StringToMessage(currentStr))
        set currentTextPart += 1 

    AddCanvas(Agent:agent):void=
        Print("Add Canvas")
        if (InPlayer := player[Agent], PlayerUI := GetPlayerUI[InPlayer]):
            if (MyUI := MaybeMyUIPerPlayer[InPlayer]?):
                PlayerUI.RemoveWidget(MyUI)
                if (set MaybeMyUIPerPlayer[InPlayer] = false) {}
            else:
                NewUI := CreateMyUI()
                PlayerUI.AddWidget(NewUI, player_ui_slot{InputMode := ui_input_mode.All})
                if (set MaybeMyUIPerPlayer[InPlayer] = option{NewUI}) {}

    CreateMyUI():canvas=
        Print("Create UI")
        Print("subscribe")
        WidgetTitle:text_block = text_block:    
            DefaultText := {StringToMessage(DialogTitle)}

        NewCanvas := canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors:
                        Minimum := vector2{X := 0.5, Y := 0.25}
                        Maximum := vector2{X := 0.5, Y := 0.25}
                    Alignment := vector2{X := 0.5, Y := 0.5}
                    SizeToContent := true
                    Widget := stack_box:
                        Orientation := orientation.Vertical
                        Slots := array:
                            stack_box_slot:
                                Widget := WidgetTitle
                            stack_box_slot: 
                                Widget := WidgetRow1 
                            stack_box_slot:
                                Widget := WidgetUIButton

        return NewCanvas
        
    RemoveCanvas(Message: widget_message):void=
        if (PlayerUI := GetPlayerUI[Message.Player], MyUI := MaybeMyUIPerPlayer[Message.Player]?, SelectedButton := text_button_base[Message.Source]):
            Print("Remove Canvas")
            PlayerUI.RemoveWidget(MyUI)
            if (set MaybeMyUIPerPlayer[Message.Player] = false) {}
3 Likes

I believe this is the best way to do what you want. Custom class attached to each player in a players map. @Twin01 is the expert for sure!

3 Likes

Nice! Ya it worked for me! It’s kinda complicated, for such a basic feature that most people need for UI. I think even Epic might agree multiplayer UI is complicated, because the pizza delivery game they’re teaching in the “Managing and Displaying Score” Verse UI tutorial is singleplayer. :eyes: But now that I know how to do it I’m set. :slight_smile:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.