UI Lobby Menu Replication

I’m trying to create a lobby menu for a multiplayer game that uses Listen Servers.

The lobby includes a display for each connected player, with some information about their character.

I have an ALobbyPlayerState class that inherits from APlayerState and contains the character information.

The UI elements are controlled from each player’s ALobbyPlayerController, which inherits from APlayerController. In this class, I have a function RefreshPlayers, which tells the widget to update. The widget then loops through the GameState->PlayerArray and updates accordingly. RefreshPlayers is called from the AddPlayerState and RemovePlayerState functions of ALobbyGameState, which inherits from AGameStateBase. This successfully updates the number of players displayed.

However, when a player sets their character info to update their PlayerState, it is only updating locally. I’ve tried a few solutions to replicate it to other clients, but with no success.

Things I have tried:
-Put the character data into a replicated struct, then call the RefreshPlayers function on each player controller (fails as the OnRep function is never called, even when logging shows the value changes, GetLifetimeReplicatedProps sets it up, and the UPROPERTY and UFUNCTION macros are correctly set)
-Add a delegate to broadcast the change in the PlayerState, and listen for it in the GameState to call the update function (fails as the callback is never called, despite logging showing that the AddDynamic is called to set the delegate and the broadcast is called to trigger it)
-Use Server and NetMulticast functions in the PlayerState to replicate the change

The player count updates because AddPlayerState / RemovePlayerState run on every machine when PlayerArray replicates. A field inside an existing PlayerState does not touch PlayerArray, so RefreshPlayers never runs again. The three things you tried fail for separate reasons.

Set the data on the server, then let OnRep refresh the widget.

  1. Client writes do not replicate. If the owning client assigns the struct locally, that client’s log shows the new value, but the server copy never changed, so nothing is sent and OnRep never runs on anyone else. OnRep also does not run for the machine that wrote the value itself. Route it through a Server RPC on the PlayerController (not on the PlayerState; a client does not own calls on a PlayerState the same way, and a non-owner cannot RPC):

UFUNCTION(Server, Reliable)
void Server_SetCharacter(const FCharacterInfo& Info);

Server_SetCharacter_Implementation sets the replicated property on that player’s PlayerState. Mark the property:

UPROPERTY(ReplicatedUsing=OnRep_CharacterInfo)
FCharacterInfo CharacterInfo;

GetLifetimeReplicatedProps uses DOREPLIFETIME(ALobbyPlayerState, CharacterInfo). Do not use COND_OwnerOnly, or other clients never receive it and their lobby never updates.

  1. The struct has to actually be dirty. Give the USTRUCT an operator== (or change it as a whole, CharacterInfo = Info, not a member write the replication system can miss). If there is no equality operator, UE cannot tell that it changed.

  2. OnRep is the refresh. On each machine, including late joiners, OnRep_CharacterInfo calls RefreshPlayers, and the widget reads GameState->PlayerArray there. On the listen server, OnRep is not called when the server assigns the property, so call it yourself after the write:

CharacterInfo = Info;
OnRep_CharacterInfo();

Clients get the same function from replication. One path for host and clients.

Why the other two attempts stay silent:

  • A dynamic multicast delegate does not replicate. AddDynamic on the client binds the client’s GameState. Broadcast on the server runs the server’s GameState, which is a different object. Both logs are true, and they are not the same process. Do not use delegates to cross the network. Use OnRep.

  • A NetMulticast from the client is dropped. Only the server can multicast. And if you multicast the payload while also replicating the property, the multicast often arrives before the property, so the widget reads the old struct. Skip the multicast. The replicated property plus OnRep is the whole update.

RefreshPlayers should only read replicated state, never write it. Call it from OnRep_CharacterInfo, and keep the existing AddPlayerState / RemovePlayerState calls for join and leave. After this, a client changing character will show on the host and on every other client, including someone who joins after the value was set, because the PlayerState replicates the current struct to them and OnRep runs on receive.