Does anyone else find Verse UI really slow?

I have been messing around with different UI to create a shop in FN.

I originally created a widget and dialog device but that has a few issues. Firstly, you can’t really change anything about the UI at runtime. Secondly, we are stuck with an extremely low 6 buttons (5 if you want to take one out for a exit button).

These issues led me to the terrible verse UI code. The benefit here was that I could create more than 6 buttons and bind them all. What I did not expect was just how slow the UI is.

It can take up to 2 seconds before the UI even displays on screen and players can press a button several times before it actually does anything.

Is anyone else having this issue with UI? Is there a trick I’m not aware of that can speed up verse UI?

Same issues & limitations here, hope they’ll fix it soon

I have a HUD that has no interactions, it updates scores and has a clock showing elapsed time and it seems to have no slowness issues. I have not tried interactive HUD elements, so interactivity may be the issue.

I update text and times real-time, and it seems speedy and responsive doing that.

By comparison, I have worked with Apple’s iOS UI and while you could do anything you could imagine with it, getting individual elements to refresh (show changes) was a pain, basically black magic.

I also have a lot of HUD elements that have no interactions. But as soon as I create an interactable widget it is super slow (just that interactable widget, not the rest of the ‘static’ UI).

1 Like

Good to know. I had considered doing something HUD-based instead of vending machines (which I find pretty annoying), but that may not be practical.

Hello everyone!

Its good to see I’m not the only one who is having this issue and I’ve come up with a solution. I should warn everyone that I have not extensively tested this yet and I could be missing something.

The solution:

If you add your UI to the player canvas at the start of the game, you can set its visibility to collapsed. This actually makes opening the UI instantaneous. When adding the UI to the screen however, you need to ensure you have the InputMode set to none. You do not want to consume input on this UI!

if (InPlayerUI:= GetPlayerUI[InPlayer])
{
    InPlayerUI.AddWidget(ShopItemStack, 
     player_ui_slot{InputMode:=ui_input_mode.None, ZOrder:=1});

    Print("AddUI");
}

This does create a problem however. What if you want to have interactive UI? I’ve figured that out too.

Create a dummy canvas of some kind that will be added to the screen when you want to use your UI. This dummy will be empty and will consume input. When adding this to the screen however, you will want to set its visibility to hidden. If you forget to do this, your UI will not be interactable and will have an invisible canvas over top.

ShowUI():void=
{
    if (InPlayerUI:= GetPlayerUI[OwningPlayer?])
    {
        InPlayerUI.AddWidget(DummyWidget, 
         player_ui_slot{InputMode:=ui_input_mode.All, ZOrder:=0})

        DummyWidget.SetVisibility(widget_visibility.Hidden);
        ShopItemStack.SetVisibility(widget_visibility.Visible);
    }
}

Note: I don’t believe ZOrder has any impact on this and is just there as a artefact of my testing.

1 Like

How did you set up the dummy widget? I tried using an empty canvas and it still doesn’t let me interact with the buttons.

1 Like

Just tried this but you can not interact with the buttons. Same problem as @blindx2
How did you fixed it?

To help anyone who is looking to make fast working UI, this was my method. It is fairly large since I needed something for multiple players and needed it to be easy to extend.

I will post this in multiple posts below.

First up we have the manager

# This is added to any device that posseses a UI element.
# IE: You create a shop device and it needs UI so you add this to it.
# This allows you to create that specific UI and init it to all players.
kingdom_ui_initializer_interface := interface
{
    GenerateNewUIForPlayer(InPlayer:player, UIManager:kingdom_player_ui_manager):void;
}

kingdom_player_ui_manager := class()
{
    # Collection of icons for use, defined in verse
    ImageIcons<public>:[kingdom_ui_icon_tag]texture = map
    {
        kingdom_ui_icon_tag.UNDEFINED_ICON => Icons.Goldbar_Icon
        #Huge list of icon tags not posted to make code snippet smaller
    };

    # Array of UI Initializers
    var UIInitializers<private>:[]kingdom_ui_initializer_interface = array{};
    # Map of generated player UI('s)
    var LocalPlayerUI<private>:[]kingdom_player_ui = array{};

    # This is called at the start of the game. You will have to define when its best called though.
    # We collect all devices with a Tag and cast them to the Init Interface. 
    InitUIManager():void=
    {
        UIInitializersTags := GetCreativeObjectsWithTag(UIInitializerInterface{});

        for (InitializerTag : UIInitializersTags, UIInitializer := kingdom_ui_initializer_interface[InitializerTag])
        {
            set UIInitializers += array{UIInitializer};
        }
    }

    # Here we init a players UI. To see the PlayerUI class, it will be in the next post.
    # This basically just calls all the UI init interfaces and creates a new playerUI.
    # You could have GenerateNewUIForPlayer return the new UI instead of setting it
    # directly in the function.
    InitPlayerUI(NewPlayer:player):void=
    {
        set LocalPlayerUI += array{kingdom_player_ui{OwningPlayer := NewPlayer}}

        for (Initializer : UIInitializers)
        {
            Initializer.GenerateNewUIForPlayer(NewPlayer, Self);
        }

        #InputConsumptionDummy:kingdom_base_player_ui = kingdom_base_player_ui{};
        #RegisterNewPlayerUIEntry(NewPlayer, InputConsumptionDummy, kingdom_ui_type.INPUT_CONSUMER_UI);

        Print("Init player UI.");
    }

    # Get the UI type from all players
    GetPlayersUIOfType(UIType:kingdom_ui_type):[]kingdom_base_player_ui=
    {
        var PlayersUI:[]kingdom_base_player_ui = array{};

        for (PlayerUI : LocalPlayerUI,
        set PlayersUI += array{PlayerUI.PlayerUICollection[UIType]})
        {
        }

        return PlayersUI;
    }

    GetPlayerUIOfType(InPlayer:player, UIType:kingdom_ui_type)<transacts>:?kingdom_base_player_ui=
    {
        var PlayerUI:?kingdom_base_player_ui = false;

        for (UI : LocalPlayerUI,
        UI.OwningPlayer = InPlayer)
        {
            set PlayerUI = option{UI.PlayerUICollection[UIType]};
        }

        return PlayerUI;
    }

    RegisterNewPlayerUIEntry(InPlayer:player, NewUI:kingdom_base_player_ui, NewUIType:kingdom_ui_type):void=
    {
        if (NewUIType <> kingdom_ui_type.UNDEFINED_UI)
        {
            for (PlayerUI : LocalPlayerUI,
            InPlayer = PlayerUI.OwningPlayer)
            {
                if(set PlayerUI.PlayerUICollection[NewUIType] = NewUI)
                {
                    #NewUI.AddCanvasToPlayer(InPlayer, player_ui_slot{InputMode:= ui_input_mode.None});
                }
                else
                {
                    Print("Failed to Init UI.");
                }
            }
        }
    }

    AddToPlayerUI(InPlayer:player, UIType:kingdom_ui_type, ShouldConsumeInput:logic):void=
    {
        for (PlayerUI : LocalPlayerUI,
        InPlayer = PlayerUI.OwningPlayer)
        {
            PlayerUI.AddUIToPlayerScreen(UIType, ShouldConsumeInput);
        }
    }

    RemoveFromPlayerUI(InPlayer:player, UIType:kingdom_ui_type):void=
    {
        for (PlayerUI : LocalPlayerUI,
        InPlayer = PlayerUI.OwningPlayer)
        {
            PlayerUI.RemoveUIFromPlayerScreen(UIType);
        }
    }

    ClearAllPlayerUI(InPlayer:player):void=
    {
        for (PlayerUI : LocalPlayerUI,
        InPlayer = PlayerUI.OwningPlayer)
        {
            PlayerUI.RemoveAllUIFromPlayerScreen();
        }
    }
}
1 Like

This is my player UI class. It keeps all the UI in a collection and then can add it to a active stack. If you push more UI to the screen while you already have something there, it will cover it.

This works really good when you have multiple UI instances opening and you want to disable the one below without actually removing it.

AS A VERY IMPORTANT NOTE! At the time I did not implement ShouldConsumeInput when adding to the player screen. This will have to be implemented.

kingdom_ui_type := enum
{
    UNDEFINED_UI,
    INPUT_CONSUMER_UI,
    MAIN_MENU_UI,
    LOADOUT_PLAYER_WEAPONS_MENU_UI,
    LOADOUT_WEAPON_SELECTION_MENU_UI,
    BLACKSMITH_SHOP_UI,
    AMMO_SHOP_UI,
    CONSUMABLES_SHOP_UI,
    RECRUITMENT_SHOP_UI,
    LOADOUT_PERK_SELECTION_MENU_UI,
    EQUIPMENT_SHOP_UI
}

kingdom_player_ui := class()
{
    OwningPlayer:player;

    var MaybeCurrentActiveUI<private>:?kingdom_player_active_ui_handle = false;
    var ActiveUIStack<private>:[]kingdom_player_active_ui_handle = array{};

    var PlayerUICollection<public>:[kingdom_ui_type]kingdom_base_player_ui = map{};

    AddUIToPlayerScreen(UIType:kingdom_ui_type, ShouldConsumeInput:logic):void=
    {
        if (UIEntryToActivate := PlayerUICollection[UIType])
        {
            var IsUIAlreadyActive:logic = false;
            
            if (CurrentActiveUI := MaybeCurrentActiveUI?,
            CurrentActiveUI.UIType = UIType)
            {
                set IsUIAlreadyActive = true;
            }

            if (IsUIAlreadyActive = false)
            {
                var MaybeActiveUIHandle:?kingdom_player_active_ui_handle = false;
                if (ActiveUIStack.Length > 0)
                {
                    for (ActiveUI : ActiveUIStack,
                    ActiveUI.UIType = UIType)
                    {
                        set MaybeActiveUIHandle = option{ActiveUI};
                    }
                }

                if (MaybeActiveUIHandle?)
                {
                    # Remove the entry from the array
                    # we add this back in after
                    RemoveEntryFromActiveUI(UIType);
                }
                else
                {
                    NewUIHandle := kingdom_player_active_ui_handle
                    {
                        UIType := UIType,
                        UIHandle := UIEntryToActivate
                    };

                    set MaybeActiveUIHandle = option{NewUIHandle};
                }

                if (NewActiveUIHandle := MaybeActiveUIHandle?)
                {
                    if (CurrentActiveUI := MaybeCurrentActiveUI?)
                    {
                        CurrentActiveUI.UIHandle.RemoveCanvasFromPlayer(OwningPlayer);
                    }
                    
                    set ActiveUIStack += array{NewActiveUIHandle};
                    set MaybeCurrentActiveUI = option{NewActiveUIHandle};

                    UIEntryToActivate.AddCanvasToPlayer(OwningPlayer, player_ui_slot{InputMode:= ui_input_mode.All});
                    UIEntryToActivate.SetVisibility(widget_visibility.Visible);
                }
            }
            else
            {
                Print("Attempted to push UI to screen but it is already on the screen.");
            }
        }
        else
        {
            Print("No UI found in the player collection.");
        }
    }

    RemoveUIFromPlayerScreen(UITypeToRemove:kingdom_ui_type):void=
    {
        # We we have no current active UI, something has gone wrong
        if (ActiveUI := MaybeCurrentActiveUI?)
        {
            MaybeRemovedUI := RemoveEntryFromActiveUI(UITypeToRemove);
            if (RemovedUI := MaybeRemovedUI?)
            {
                RemovedUI.RemoveCanvasFromPlayer(OwningPlayer);
            }

            if (ActiveUIStack.Length > 0,
            NextActiveUI := ActiveUIStack[ActiveUIStack.Length - 1])
            {
                if (ActiveUI.UIType = UITypeToRemove)
                {   
                    NextActiveUI.UIHandle.AddCanvasToPlayer(OwningPlayer, player_ui_slot{InputMode:= ui_input_mode.All});
                    NextActiveUI.UIHandle.SetVisibility(widget_visibility.Visible);
                    set MaybeCurrentActiveUI = option{NextActiveUI};
                }
            }
            else
            {
                set MaybeCurrentActiveUI = false;
            }
        }
        else
        {
            Print("Tried to remove UI from player but player has no active UI!");
        }
    }

    RemoveAllUIFromPlayerScreen():void=
    {
        for (ActiveUI : ActiveUIStack)
        {
            ActiveUI.UIHandle.RemoveCanvasFromPlayer(OwningPlayer);
        }

        set ActiveUIStack = array{};
        set MaybeCurrentActiveUI = false;
    }

    # This should keep the order of the array
    RemoveEntryFromActiveUI<private>(UITypeToRemove:kingdom_ui_type):?kingdom_base_player_ui=
    {
        var NewUIStack:[]kingdom_player_active_ui_handle = array{};
        var RemovedUI:?kingdom_base_player_ui = false;
        for (UIEntry : ActiveUIStack)
        {
            if (UIEntry.UIType <> UITypeToRemove)
            {
                set NewUIStack += array{UIEntry};
            }
            else
            {
                set RemovedUI = option{UIEntry.UIHandle};
            }
        }

        set ActiveUIStack = NewUIStack;
        return RemovedUI;
    }
}
1 Like

Lastly we have the base_player_ui. This class is meant to be override by your actual UI instances. If you make a shop UI, override this. Loadout UI? Override this. Always make sure to call the super back to the base class though!

Just as a side note, I ran into lots of issues trying to make fast UI so some code might not be necessary (at least anymore). I wrote this a while ago and verse is always changing.

kingdom_base_player_ui := class()
{
    # Base canvas for the player UI
    var CanvasBase<protected>:canvas = canvas{};
    
    # Helper function for converting String to Message
    MakeMessageFromString<localizes>(InString:string):message = "{InString}";

    SetVisibility(Visibility:widget_visibility):void=
    {
        CanvasBase.SetVisibility(Visibility);
    }

    InitUI():void=
    {
    }

    AddCanvasToPlayer(InPlayer:player, UIInputMode:player_ui_slot):void=
    {
        if (InPlayerUI:= GetPlayerUI[InPlayer])
        {
            SetVisibility(widget_visibility.Collapsed);
            InPlayerUI.AddWidget(CanvasBase, UIInputMode);
            Print("AddUI");
        }
    }

    RemoveCanvasFromPlayer(InPlayer:player):void=
    {
        if (InPlayerUI:= GetPlayerUI[InPlayer])
        {
            SetVisibility(widget_visibility.Collapsed);
            InPlayerUI.RemoveWidget(CanvasBase);
            Print("RemovedUI");
        }
    }
}

kingdom_base_shop_player_ui := class(kingdom_base_player_ui)
{
    var ExitButton:button_loud = button_loud{};

    # Reference to the shop device for functions
    OwningShopDevice:kingdom_shop_manager_device;

    AddCanvasToPlayer<override>(InPlayer:player, UIInputMode:player_ui_slot):void=
    {
        (super:)AddCanvasToPlayer(InPlayer, UIInputMode);
    }
}
1 Like

As this was written for my map Twilight Hollow. You can check it out to see if the UI is what you might be looking for. It can take a while to load however so if you really want to see if, be patient.

Hopefully we see some better maps as this kind of stuff becomes more available.

Map Codes:

1322-2507-2160
8200-5173-7941

1 Like

Thank you for the code @Warkingi
This does not pay in any way :heart_eyes:

I will check it out and post the differences

I was looking for a solution for the very slow loading of the Verse UI. I tried @Warkingi’s proposed solution, but it didn’t work. After continuing my search and going through many iterations, I found another solution who I hope can help some of you.

I create my canvas when a player join my experience, add it with the method AddWidget to the player and the input mode to none. Inside my canvas, I setup a overlay, with the visibility at Collapsed.

When I need to show my UI, I set the visibility of the overlay to Visible and I do something weird who work : I remove the canvas with the method RemoveWidget and I add it again with AddWidget, but this time with the input mode to all. And it’s working perfectly.

To hide the Ui, I do the samething, remove my canvas, re-add it with the input mode to none and set the visibility of the overlay to collapsed.