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?
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).
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.
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();
}
}
}
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;
}
}
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);
}
}
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.
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.