UI Widgets lose input focus on consoles, causing a player softlock.

Summary

When a UI widget created with the Verse API is displayed on consoles, it intermittently loses input focus. This prevents the player from interacting with the widget or the game, causing a softlock that requires the player to respawn. The issue seems to be an engine-level problem with focus management, especially with controllers.

Please select what you are reporting on:

Unreal Editor for Fortnite

What Type of Bug are you experiencing?

UI/Tools

Steps to Reproduce

Create a UI widget using the Verse API. The widget must contain multiple focusable buttons (e.g., a shop or menu).
Set up a trigger to display this widget to the player (e.g., interacting with a device or entering a volume).
Launch a session on a console platform (PlayStation, Xbox, or Switch) using a controller.
Trigger the UI widget to appear on the screen.
Close the widget and repeat the process multiple times.

Note: The bug is intermittent but occurs more frequently with complex UIs containing many buttons arranged in both rows and columns.

Expected Result

The UI widget should always receive and maintain input focus upon being displayed. The player should be able to navigate all buttons with the controller and close the widget to return to gameplay without any issue.

Observed Result

Intermittently, the UI widget appears on the screen but does not have input focus. The player cannot navigate the buttons, select any option, or close the widget. The game is effectively softlocked for that player, and the only way to continue is to force a respawn, losing progress.

Platform(s)

PlayStation 4, PlayStation 5, Xbox One, Xbox Series X|S, Nintendo Switch.

Island Code

7998-1825-5183

Video

Additional Notes

This is a critical, progression-blocking bug. We strongly believe this is an engine-level issue with UEFN’s input focus management for controllers, not an error in our Verse implementation.
Our reasoning:
The issue occurs across different UI implementations and systems within our project.
A previous workaround (adding a 0.2s delay before showing the UI) lost its effectiveness after a Fortnite engine update, suggesting external changes are affecting its behavior.
The community forums contain numerous reports from other developers facing the exact same softlock issue with UI focus.
Mitigation attempts and key findings:
Dynamic State Changes: Dynamically changing the state of buttons after the UI is rendered (e.g., hiding, showing, or disabling them) is a primary trigger for the focus loss. This points to an issue in how the engine handles UI refreshes and state updates.
Complex Layouts: We’ve identified that nesting buttons within other layout widgets is a major trigger for focus loss. As a workaround, we tried placing each button in its own individual canvas to separate the button’s functionality from the main visual layout. This approach helps reduce the issue but does not solve the root cause.
Forcing a UI refresh: Tried hiding and re-showing the widget instantly (ā€œdouble tapā€), and overlaying a temporary transparent image. These methods did not fix the issue.
This bug severely impacts player experience and retention, as it forces players to abandon their session. Any guidance or a potential fix from the engine side would be greatly appreciated.

I can confirm this is a game killing issue. We have this in our map that we’ve been trying to debug for the whole day now. Map code is 2856-7078-4649, go to sell area and interact with the shop (although we’ll be full changing this VERY SOON)

Console UI focus seems to be a problem in many areas. There’s always been the issue of if buttons aren’t aligned with other buttons, they can’t be accessed. Which can lock the player in the UI if the close button specifically doesn’t align.

I’m running into a problem specifically right now when using two Verse based UI. They each operate fine on their own. But I integrated my Shop UI into my Smartphone UI for ease of access. On PC this integration works perfectly. But on Console, opening the Shop UI from the Smartphone UI locks focus and the player cannot interact or close the interface. There is an annoying workaround that if the player presses their Start button once to open Fortnite default menu and press again to close it, Focus will return. But I imagine these two issues are linked. Console based UI handling is just not good and needs a look at in a future quality of life update.

EDIT - My only idea would be Epic implemented a cursor based focused for console players. All the latest consoles have thumbsticks at this point. If focus was no longer locked to the buttons and players could move their thumbsticks and had to manually aim at a button, I think it would solve all these issues. See mock up. The circle would move around freely using thumbsticks.

1 Like

I’ve been putting in an auto timeout on all my UI’s so that if the player doesn’t click a button in say 15 seconds it auto closes the UI. Just make sure to call ResetTimer on button click event. This way if a player does get soft locked they don’t have to respawn, they can just hang out till the verse ui closes due to inactivity.

2 Likes

This didn’t work for us and in fact locked the player the second they opened the Verse UI.

You’ll need to make a map of the players and their timeout and then reset it on each interaction:

This is the bulk of our UI timeout system, I haven’t had the chance to turn it into a standalone module yet but I have this same setup in all of our core verse UI.

Hope it helps, let me know if you get stuck


    @editable:
        Categories := array{InventoryBasicCategory}
        ToolTip := UITimeoutToolTip
    UITimeout<public> : float = 15.0 # Time before auto-close


    var TimeoutActive<private> : [player]logic = map{} # Track if timeout is active
    var TimeoutWarningShown<private> : [player]logic = map{} # Track if warning was shown per player
    var TimeoutStartTime<private> : [player]float = map{} # Track when the timeout started for each player
    var LastInteractionTime<private> : [player]float = map{} # Track when the player last interacted with the inventory



    # Start timeout timer
    StartTimeout<public>(Player : player) : void =
        # Reset timeout state
        if (set TimeoutActive[Player] = true):
            Logger.Print("Set timeout active for player {Player}")
        
        # Reset warning flag
        if (set TimeoutWarningShown[Player] = false):
            Logger.Print("Reset timeout warning flag for player {Player}")
        
        # Record the current time
        CurrentTime := GetSimulationElapsedTime()
        if (set TimeoutStartTime[Player] = CurrentTime):
            Logger.Print("Set timeout start time for player {Player} to {CurrentTime}")
        
        if (set LastInteractionTime[Player] = CurrentTime):
            Logger.Print("Set last interaction time for player {Player} to {CurrentTime}")
        
        # Hide any previous countdown text (placeholder for now)
        # HideCountdownText(Player)
        
        # Start the timeout task if enabled
        if (UIConfig.UITimeout > 0.0):
            spawn{ HandleTimeout(Player) }
            Logger.Print("Started new timeout task for player {Player} with full duration of {UIConfig.UITimeout} seconds")

    # Reset timeout for a player - always reset to the full duration
    ResetTimeout<public>(Player : player) : void =
        # If timeout is enabled, reset the state and timestamps
        if (UIConfig.UITimeout > 0.0):
            # Stop any running timeout to reset it completely
            if (set TimeoutActive[Player] = false):
                #Sleep(0.1) # Brief pause to let the current timeout task exit
                
            # Update the last interaction time to now
            CurrentTime := GetSimulationElapsedTime()
            if (set LastInteractionTime[Player] = CurrentTime):
                Logger.Print("Updated last interaction time for player {Player} to {CurrentTime}")
            
            # Reset warning flags and active state
            if (set TimeoutWarningShown[Player] = false):
                Logger.Print("Reset timeout warning flag for player {Player}")
            
            # Hide countdown text if shown (placeholder for now)
            # HideCountdownText(Player)
            
            # Restart the timeout
            if (set TimeoutActive[Player] = true):
                Logger.Print("Reactivated timeout for player {Player}")
                # Start a new timeout task
                spawn{ HandleTimeout(Player) }
            
            # Log the full reset
            Logger.Print("Completely reset timeout for player {Player} to full duration of {UIConfig.UITimeout} seconds")


    # TODO: ADD the Timeout Countdown Text and the Warning Sound!!!!!!!!!!!!!!!!!!!!!!!!!
    # Handle UI timeout
    HandleTimeout<private>(Player : player)<suspends> : void =
        # Double-check if timeout should run
        if (not TimeoutActive[Player]?):
            Logger.Print("Timeout not active for player {Player}, exiting timeout task")
            return
        
        Logger.Print("Starting timeout monitoring for player {Player} with duration {UIConfig.UITimeout}")
        
        # Main timeout loop - check every second
        loop:
            # Check if the timeout should still be active
            if (not TimeoutActive[Player]?):
                Logger.Print("Timeout deactivated for player {Player}, breaking timeout loop")
                # HideCountdownText(Player) # You can add a Text Block or other widget to show a warning
                break
            
            # Calculate time since last interaction
            if (LastTime := LastInteractionTime[Player]):
                CurrentTime := GetSimulationElapsedTime()
                ElapsedTime := CurrentTime - LastTime
                RemainingTime := UIConfig.UITimeout - ElapsedTime
                
                # Check if we've reached warning time
                WarningThreshold := 5.0 # UIConfig.UITimeoutWarning placeholder
                
                if (RemainingTime <= 0.0):
                    # Time's up, close the inventory
                    Logger.Print("Timeout elapsed for player {Player}, closing inventory")
                    # HideCountdownText(Player) # Placeholder for now
                    CloseInventory(Player)
                    break
                else if (RemainingTime <= WarningThreshold and not TimeoutWarningShown[Player]?):
                    # Start warning period
                    if (set TimeoutWarningShown[Player] = true):
                        Logger.Print("Entering warning period for player {Player}, {RemainingTime} seconds remaining")
                        
                        # Play warning sound placeholder
                        # if (WarningAudio := UIConfig.TimeoutWarningAudio?):
                        #     WarningAudio.Play(Player)

                # Update countdown during warning period (placeholder for now)
                if (TimeoutWarningShown[Player]?):
                    # ShowCountdownText(Player, Ceil[RemainingTime]) # Placeholder for now
        
            # Sleep for a short time before checking again
            Sleep(1.0)


########### In Every OnClick Event Handler add a call to Reset your timeout
HandleItemClicked := class:
    Item : inventory_item_display
    Controller : inventory_controller
    
    OnClick(Message : widget_message) : void =
        # Reset timeout on item interaction
        Controller.ResetTimeout(Message.Player)
        Controller.Logger.Print("Reset timeout for item interaction")
        if (set Controller.SelectedItem[Message.Player] = option{Item}):
            Controller.Logger.Print("Selected item {Item.ResourceName} for player {Message.Player}")
        # TODO: Show item details overlay
        Controller.ShowItemDetails(Message.Player, Item)

I appreciate the feedback, thank you. I realize how to do that, but I’m saying that console players just full crash and there is just nothing that gets them out of it for us.

FORT-938138 has been added to our ā€˜To Do’ list. Someone’s been assigned this task.

1 Like