This document is about: FUSION 2
SWITCH TO

Photon Fusion Shared Mode: In-Room Lobby System

This page accompanies the Photon Fusion Shared Mode: In-Room Lobby System with Networked Ready Sync video and explains how to build a room-based lobby where connected players can see each other, synchronize their ready status, and wait for the Master Client to start the match.

Use this page when players should connect to a Fusion room before entering gameplay and need a synchronized staging area for ready checks, player customization, team selection, or match configuration.

If you would like to learn more about the INetworkStruct, please take a look here.

Photon Fusion Shared Mode: In-Room Lobby System with Networked Ready Sync

Overview

An in-room lobby exists after players have already connected to the same Fusion session.

It is different from a matchmaking lobby or room browser. Photon matchmaking is responsible for finding or creating the room. The in-room lobby manages the players who are already connected to that room.

The system built in this tutorial includes:

  • a shared lobby scene;
  • a synchronized list of connected players;
  • Networked ready-state data;
  • dynamically created player UI entries;
  • a local ready toggle;
  • a Start Game button visible only to the Master Client;
  • validation that all players are ready;
  • synchronized scene loading;
  • player join and leave handling.

The UI itself is local Unity presentation. Only the lobby data and match-start decision need to be synchronized through Fusion.

Video Timeline

Time Section
00:00 Introduction
00:31 Importing the template
01:07 Verifying the template
01:33 Adjusting the StartMenu script
02:37 Creating and configuring the lobby scene
06:57 Creating the UILobbyPlayer component
08:59 Creating LobbySessionController and LobbyPlayerData
09:20 Implementing the LobbyPlayerData struct
10:12 Implementing LobbySessionController
26:06 Connecting Unity references
26:46 Adding the lobby scene to Build Settings
26:59 Testing the final result

In-Room Lobby Architecture

The lobby can be divided into three layers.

Layer Responsibility
Fusion session Maintains the room connection and connected PlayerRef values.
Networked lobby state Stores each player’s name, ready status, and optional selections.
Unity UI Displays the synchronized state and sends local player actions.

A practical script structure includes:

Script or Type Responsibility
StartMenu Connects to the Fusion room and opens the lobby instead of immediately entering gameplay.
LobbyPlayerData Network-compatible struct containing one player’s lobby state.
LobbySessionController Owns synchronized lobby data and controls match start.
UILobbyPlayer Displays one player’s name and ready state.
Lobby panel controller Creates, updates, and removes player UI entries.

The exact script names can vary, but the separation between Networked state and local UI should remain.

Starting from Fusion Asteroids

The tutorial uses the Fusion Asteroids sample as its base project.

Before creating the lobby:

  1. Import or open the sample.
  2. Add the project’s App ID from the Photon Dashboard.
  3. Verify that Fusion connects successfully.
  4. Run a development build or second instance.
  5. Confirm that players can join the same room.
  6. Confirm that the existing gameplay scene loads correctly.

Testing the original template first makes it easier to identify whether later problems come from the lobby implementation or the initial Fusion setup.

Importing UI Assets

The UI can use any Unity-compatible asset package.

The tutorial uses a Kenney space-themed UI pack, but the networking logic does not depend on those assets.

The lobby interface needs:

  • a main lobby panel;
  • a player-list container;
  • a player-row prefab;
  • player-name text;
  • ready-status text;
  • a Toggle Ready button;
  • a Start Game button;
  • a Leave Room button.

Keep the UI hierarchy independent from the networked controller wherever possible.

Modifying the Connection Flow

The original sample connects to a room and immediately loads gameplay.

For an in-room lobby, the sequence becomes:

  1. Connect to the Fusion room.
  2. Load or enable the lobby scene.
  3. Register connected players.
  4. Wait for ready-state changes.
  5. Let the Master Client start the match.
  6. Load the gameplay scene for all players.

Update StartMenu so that a successful connection no longer bypasses the lobby.

Lobby Scene as the Initial Network Scene

The lobby can be loaded as the initial Fusion scene when the session starts.

Conceptually, the connection arguments reference the lobby scene rather than the gameplay scene:

C#

var startGameArgs = new StartGameArgs
{
    GameMode = GameMode.Shared,
    SessionName = roomName,
    Scene = lobbySceneReference,
    SceneManager = networkSceneManager
};

The exact scene-loading API depends on the project’s Fusion setup.

The important point is that all players enter the same synchronized lobby scene before gameplay begins.

Lobby Scene UI Hierarchy

A possible hierarchy is:

LobbyCanvas
└── LobbyPanel
    ├── Title
    ├── PlayerList
    │   └── PlayerItemContainer
    ├── ToggleReadyButton
    ├── StartGameButton
    └── LeaveRoomButton

The player-item container receives one instantiated UI row for every connected player.

UILobbyPlayer

UILobbyPlayer represents one player in the visible lobby list.

It is a local UI component, not a NetworkBehaviour.

Its responsibilities are:

  • display the player name;
  • display whether the player is ready;
  • optionally identify the local player;
  • optionally display host or Master Client status;
  • update colors, labels, or icons.

Example:

C#

using TMPro;
using UnityEngine;

public class UILobbyPlayer : MonoBehaviour
{
    [SerializeField]
    private TMP_Text _playerNameText;

    [SerializeField]
    private TMP_Text _readyStatusText;

    public void SetPlayerName(string playerName)
    {
        _playerNameText.text = playerName;
    }

    public void SetReadyState(bool isReady)
    {
        _readyStatusText.text =
            isReady ? "Ready" : "Not Ready";
    }
}

The synchronized lobby controller supplies the values.

Ready-State Presentation

The tutorial visually distinguishes the two ready states.

For example:

State Display
Ready White text or a positive icon
Not Ready Red text or a warning icon

Color is presentation only. The authoritative value remains the Networked ready property.

Creating the Player-Row Prefab

To create the prefab:

  1. Create a UI GameObject.
  2. Add text for the player name.
  3. Add text or an icon for ready status.
  4. Attach UILobbyPlayer.
  5. Assign its references.
  6. Save it as a prefab.
  7. Remove the temporary scene instance if it is no longer needed.

The lobby panel instantiates this prefab dynamically for each connected player.

LobbyPlayerData

LobbyPlayerData stores the synchronized information for one player.

Because it is stored in Fusion Networked state, it must use Fusion-compatible fields and implement:

C#

INetworkStruct

A possible definition is:

C#

using Fusion;

public struct LobbyPlayerData : INetworkStruct
{
    public NetworkString<_32> PlayerName;
    public NetworkBool IsReady;
}

Do not use a managed C# string directly inside Networked state.

Use NetworkString<TSize> with a capacity appropriate for the project.

Why Use INetworkStruct?

INetworkStruct allows several related values to be stored as one Fusion-compatible value.

It is useful for player lobby data because one entry can contain:

  • display name;
  • ready state;
  • selected skin;
  • team;
  • character selection;
  • rank or level;
  • other compact lobby settings.

The struct must contain only Fusion-supported network types.

Extending LobbyPlayerData

The structure can later be expanded:

C#

public struct LobbyPlayerData : INetworkStruct
{
    public NetworkString<_32> PlayerName;
    public NetworkBool IsReady;
    public byte SelectedSkinIndex;
    public byte TeamIndex;
}

Compact integral values are useful for selections that map to project assets or configuration tables.

Avoid synchronizing full Unity asset references or UI objects through the struct.

LobbySessionController

LobbySessionController is the central Networked controller for the room lobby.

It is responsible for:

  • registering players;
  • removing disconnected players;
  • storing lobby data;
  • receiving ready-state requests;
  • checking whether all players are ready;
  • identifying the Master Client;
  • starting the gameplay scene;
  • notifying local UI when lobby data changes.

It should be attached to a NetworkObject in the lobby scene.

Storing Player Data

One approach is to store the lobby entries in a Networked dictionary:

C#

[Networked, Capacity(16)]
public NetworkDictionary<PlayerRef, LobbyPlayerData>
    Players => default;

The key is the Fusion PlayerRef.

The value is the player’s LobbyPlayerData.

The capacity should match the maximum supported room size.

Why Key by PlayerRef?

PlayerRef is Fusion’s network identity for a connected player.

Using it as the key makes it possible to:

  • associate data with the correct connection;
  • remove data when the player leaves;
  • identify the local player;
  • determine which UI row to update;
  • validate RPC senders;
  • map a player to their gameplay object later.

Do not use a player’s display name as the unique network identifier because names may not be unique.

Registering Players

When a player joins, the State Authority of the lobby controller adds a new entry.

Conceptually:

C#

private void RegisterPlayer(PlayerRef player)
{
    if (!Object.HasStateAuthority)
    {
        return;
    }

    if (Players.ContainsKey(player))
    {
        return;
    }

    Players.Add(
        player,
        new LobbyPlayerData
        {
            PlayerName = $"Player {player.PlayerId}",
            IsReady = false
        }
    );
}

The actual player name can come from:

  • room connection data;
  • authentication data;
  • a pre-game menu;
  • a separate RPC;
  • persistent profile data.

Player Join Detection

The project can detect joins through Fusion callbacks or the existing sample’s connection architecture.

The lobby controller should respond when a new PlayerRef appears.

Typical responsibilities include:

  1. Confirm that the player is not already registered.
  2. Create default lobby data.
  3. Set IsReady to false.
  4. Add the player to the Networked collection.
  5. Let every client create the corresponding UI row.

Only the appropriate State Authority should mutate the Networked collection.

Player Leave Detection

When a player disconnects:

  1. Remove the player from the Networked collection.
  2. Remove the player’s local UI row.
  3. Recalculate whether all remaining players are ready.
  4. Update Master Client controls if authority changed.

Conceptually:

C#

private void UnregisterPlayer(PlayerRef player)
{
    if (!Object.HasStateAuthority)
    {
        return;
    }

    Players.Remove(player);
}

Always handle disconnection. Do not assume players leave only through the lobby’s Leave Room button.

Local UI Dictionary

The lobby UI controller can maintain a standard local C# dictionary:

C#

private readonly Dictionary<PlayerRef, UILobbyPlayer>
    _playerItems = new();

This dictionary is not Networked.

It maps synchronized player identities to local UI instances.

When lobby state changes:

  • create missing UI rows;
  • update existing rows;
  • destroy rows for players who left.

Building the Player List

A refresh method can iterate through the synchronized player collection:

C#

private void RefreshPlayerList()
{
    foreach (var pair in LobbyController.Players)
    {
        PlayerRef player = pair.Key;
        LobbyPlayerData data = pair.Value;

        if (!_playerItems.TryGetValue(
                player,
                out UILobbyPlayer item))
        {
            item = Instantiate(
                _playerItemPrefab,
                _playerListContainer
            );

            _playerItems.Add(player, item);
        }

        item.SetPlayerName(data.PlayerName.ToString());
        item.SetReadyState(data.IsReady);
    }
}

After updating the current entries, remove UI rows whose players are no longer present.

Detecting Lobby Data Changes

The UI should update when Networked lobby data changes.

Possible approaches include:

  • a Fusion Change Detector;
  • Render() comparisons;
  • OnChangedRender for individual properties;
  • events raised by a network-state wrapper;
  • refreshing when player join, leave, or ready actions occur.

For collection-based state, a Change Detector or local snapshot comparison is often useful.

The important rule is that the UI reads synchronized data rather than assuming that a local button press succeeded.

Event-Driven UI Updates

A local event can decouple the networking controller from individual UI rows.

For example:

C#

public event Action<PlayerRef, LobbyPlayerData>
    PlayerDataChanged;

When the synchronized value changes, the controller invokes the event.

The UI listens and refreshes the corresponding row.

The C# event itself is local. The data that causes it is Networked.

Toggling the Local Ready State

The local player presses the Toggle Ready button.

That button should request a change through the Fusion network flow.

The UI must not directly edit a Networked collection unless the local peer has the required State Authority.

A common pattern is to send an RPC to the lobby controller’s State Authority.

Ready-State RPC

Conceptually:

C#

[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
private void RPC_SetReady(
    NetworkBool isReady,
    RpcInfo info = default)
{
    PlayerRef player = info.Source;

    if (!Players.TryGet(
            player,
            out LobbyPlayerData data))
    {
        return;
    }

    data.IsReady = isReady;
    Players.Set(player, data);
}

Using RpcInfo.Source prevents a caller from freely choosing another player’s PlayerRef.

The receiving authority associates the request with the actual sender.

Requesting a Toggle

The local UI reads the current synchronized state and sends the opposite value:

C#

public void ToggleLocalReady()
{
    PlayerRef localPlayer = Runner.LocalPlayer;

    if (!Players.TryGet(
            localPlayer,
            out LobbyPlayerData data))
    {
        return;
    }

    RPC_SetReady(!data.IsReady);
}

The UI should wait for the Networked value to update before changing its final displayed state.

This avoids showing a state that the authority rejected.

Per-Player NetworkBehaviour Alternative

Another valid architecture is to spawn one lobby-data NetworkObject for every player.

That object can contain:

C#

[Networked]
public NetworkBool IsReady { get; set; }

The local player owns their corresponding object and changes its state.

The central-collection approach is often simpler for:

  • a compact lobby;
  • centralized validation;
  • Master Client-controlled start logic;
  • storing all player data in one place.

The per-player-object approach may fit projects that already spawn a persistent network player object before gameplay.

Use one architecture consistently rather than duplicating the same state in both places.

Master Client Start Control

Only the Master Client should be able to start the match.

In Shared Mode, the Master Client normally has State Authority over scene-owned network objects such as the lobby controller.

The UI can therefore use the lobby controller’s authority state to determine whether the Start Game button should be visible.

Conceptually:

C#

_startGameButton.gameObject.SetActive(
    LobbyController.Object.HasStateAuthority
);

Do not rely only on button visibility for security. The network-side start method must also verify authority.

Checking Whether Everyone Is Ready

The lobby controller iterates over all registered players:

C#

private bool AreAllPlayersReady()
{
    if (Players.Count == 0)
    {
        return false;
    }

    foreach (var pair in Players)
    {
        if (!pair.Value.IsReady)
        {
            return false;
        }
    }

    return true;
}

The Start Game button becomes interactable only when:

  • the local peer controls the lobby controller;
  • the lobby contains the required number of players;
  • every registered player is ready.

Minimum Player Count

A production lobby may require more than one player.

For example:

C#

private const int MinimumPlayers = 2;

Then:

C#

if (Players.Count < MinimumPlayers)
{
    return false;
}

This prevents an empty or incomplete room from satisfying the ready check.

Updating the Start Game Button

The local UI can update the button state from the current Networked collection:

C#

_startGameButton.interactable =
    LobbyController.Object.HasStateAuthority &&
    LobbyController.AreAllPlayersReady();

Recalculate this when:

  • a player joins;
  • a player leaves;
  • a ready state changes;
  • State Authority changes;
  • Master Client migration occurs.

Starting the Match

When the Master Client clicks Start Game:

  1. Verify State Authority.
  2. Recheck that all players are ready.
  3. Prevent duplicate start requests.
  4. Load the gameplay scene through the Fusion runner.
  5. Let Fusion synchronize the scene transition.

Conceptually:

C#

public void StartMatch()
{
    if (!Object.HasStateAuthority)
    {
        return;
    }

    if (!AreAllPlayersReady())
    {
        return;
    }

    Runner.LoadScene(
        gameplaySceneReference
    );
}

The exact scene API depends on the project’s network scene manager.

Validate Again on the Network Side

Do not assume the button’s disabled state is sufficient.

A modified client or stale UI could still attempt to invoke the start action.

The authoritative method must verify:

  • caller authority;
  • player count;
  • ready status;
  • current lobby state;
  • whether the match has already started.

Preventing Duplicate Starts

Store a Networked or authority-owned flag:

C#

[Networked]
private NetworkBool MatchStarting { get; set; }

Before loading:

C#

if (MatchStarting)
{
    return;
}

MatchStarting = true;

This prevents repeated clicks from initiating multiple scene-load calls.

Synchronized Scene Loading

Use the active Fusion runner and network scene manager to load the gameplay scene.

Do not use a local-only call such as:

C#

SceneManager.LoadScene(...)

for the authoritative multiplayer transition unless the project’s Fusion architecture explicitly coordinates it.

A local Unity scene load can move only one client and break the shared session flow.

Leaving the Room

The Leave Room button should shut down the local Fusion runner and return the player to the connection menu.

Conceptually:

C#

public async void LeaveRoom()
{
    await Runner.Shutdown();

    // Re-enable or load the main menu.
}

After leaving:

  • remove local lobby UI;
  • restore menu controls;
  • clear cached room data;
  • release runner references where required.

Other clients detect the disconnection and remove the player from the Networked lobby collection.

Setting Up LobbySessionController in Unity

Create a lobby controller GameObject:

LobbySessionController

Add:

  • NetworkObject;
  • LobbySessionController;
  • any required Fusion callback component;
  • references to the gameplay scene if configured through the Inspector.

Because it is a scene NetworkObject, verify that the object is baked or registered according to the project’s Fusion scene workflow.

Setting Up the Lobby Panel

Create:

InRoomLobbyPanel

Attach the local lobby UI controller.

Assign:

  • player-row prefab;
  • player-list parent;
  • Toggle Ready button;
  • Start Game button;
  • Leave Room button;
  • lobby content root;
  • LobbySessionController reference.

The main UI content can begin inactive and become visible after the room connection succeeds.

Button Bindings

Bind the buttons to local UI or controller methods:

Button Method
Toggle Ready ToggleLocalReady()
Start Game StartMatch()
Leave Room LeaveRoom()

The Start Game button should call an authority-validated network method.

Build Settings

Add both scenes to Unity Build Settings:

  • lobby scene;
  • gameplay scene.

Ensure that:

  • scene references use the correct build indexes or SceneRef values;
  • every client has the same scene list;
  • the network scene manager can resolve both scenes;
  • the connection flow starts in the lobby scene.

A scene missing from Build Settings may work in the Editor but fail in a standalone build.

Testing with Multiple Instances

Test with at least two clients.

Suggested workflow:

  1. Start the first client.
  2. Create or join the room.
  3. Confirm that the lobby scene appears.
  4. Start a second client.
  5. Join the same room.
  6. Confirm that both player names appear.
  7. Toggle ready on Player 1.
  8. Verify that both clients display the change.
  9. Toggle ready on Player 2.
  10. Confirm that the Master Client’s Start Game button becomes active.
  11. Verify that non-Master clients cannot start.
  12. Click Start Game.
  13. Confirm that all clients enter gameplay together.

Testing Player Disconnection

Also test:

  1. Join with multiple players.
  2. Set all players to ready.
  3. Disconnect one player.
  4. Confirm that their UI entry disappears.
  5. Confirm that readiness is recalculated.
  6. Confirm that the remaining Master Client can still control the lobby.

Unexpected disconnections should produce the same cleanup as the Leave Room button.

Testing Master Client Migration

In Shared Mode, the Master Client can leave.

Test this case:

  1. Start two or more clients.
  2. Identify the current Master Client.
  3. Disconnect that client.
  4. Confirm that Fusion assigns a new Master Client.
  5. Confirm that the new authority can see and use the Start Game button.
  6. Confirm that synchronized lobby data remains valid.

Do not cache Master Client status only once when the UI opens.

Late Joiners

Because ready states are stored in Networked data, a player joining later receives the current lobby state.

The late joiner should see:

  • all existing players;
  • their current names;
  • their current ready states;
  • current selections;
  • whether the match is already starting.

This is one reason Networked properties or collections are preferable to using RPC events alone.

UI Is Not Network State

Do not synchronize instantiated UI objects.

Each client creates its own local UI based on the same Networked lobby data.

Synchronize:

  • player identity;
  • ready state;
  • selections;
  • lobby status.

Keep local:

  • TextMeshPro references;
  • instantiated row GameObjects;
  • button states;
  • colors;
  • animations;
  • layout.

Expanding the Lobby

The same data pattern can support additional features.

Skin Selection

Add:

C#

public byte SelectedSkinIndex;

The UI maps the index to a local skin icon or preview.

Team Selection

Add:

C#

public byte TeamIndex;

The authority can validate team size and balancing rules.

Character Selection

Store a compact character identifier:

C#

public byte CharacterIndex;

Use a local configuration table to resolve it to a prefab or visual.

Lobby Chat

Short messages can be sent using RPCs or a bounded Networked history.

Do not continuously append unbounded chat data to Networked state.

Host Settings

The Master Client can configure:

  • selected map;
  • game mode;
  • round duration;
  • score limit;
  • team rules.

Store persistent match settings in Networked properties so all clients and late joiners receive them.

Player Statistics

Lobby UI can display profile data such as:

  • rank;
  • level;
  • win count;
  • account badge.

Persistent account data should usually come from an authentication or backend service rather than being trusted from arbitrary client input.

Networked State and Events

The general pattern is:

  1. A player performs a local UI action.
  2. The action is sent to the appropriate authority.
  3. The authority validates it.
  4. The authority changes Networked state.
  5. Fusion replicates the state.
  6. Each client updates its local UI.

This pattern applies to ready states, teams, skins, maps, and other lobby selections.

Key Concepts

Concept Description
In-room lobby Staging interface for players already connected to the same Fusion room.
PlayerRef Fusion identifier for a connected player.
INetworkStruct Interface for structs stored in Fusion Networked state.
LobbyPlayerData Struct containing one player’s synchronized lobby values.
NetworkDictionary Fixed-capacity Networked collection keyed by PlayerRef.
NetworkBool Fusion-compatible synchronized boolean.
NetworkString Fixed-capacity string suitable for Networked state.
UILobbyPlayer Local presentation component for one player row.
Master Client Shared Mode peer responsible for authority over applicable shared objects.
State Authority Peer allowed to write a NetworkObject’s state.
Ready validation Check that every required player is ready before starting.
Network scene loading Fusion-coordinated transition that moves all clients to gameplay.
Late joiner Player joining after the room and lobby state already exist.

Best Practices

Use this checklist when building an in-room lobby:

  • Keep UI objects local.
  • Store persistent lobby values in Networked state.
  • Key player data by PlayerRef.
  • Use INetworkStruct for compact grouped player data.
  • Set an explicit collection capacity.
  • Use Fusion-compatible types inside Networked structs.
  • Let the proper State Authority modify lobby data.
  • Use RPC sender information instead of trusting a supplied player ID.
  • Validate every ready-state and start-game request.
  • Make the Start Game button visible only to the Master Client.
  • Also validate Master Client authority in code.
  • Require all players to be ready before loading gameplay.
  • Add a minimum player count where required.
  • Prevent duplicate match-start requests.
  • Handle unexpected disconnections.
  • Recalculate UI and readiness after every join or leave.
  • Test Master Client migration.
  • Use Fusion scene loading for synchronized transitions.
  • Add every network scene to Build Settings.
  • Test with standalone clients, not only the Unity Editor.

Common Pitfalls

Pitfall Why It Is a Problem
Loading gameplay immediately after connecting Players never enter the in-room staging flow.
Treating the lobby as Photon matchmaking An in-room lobby manages players who are already connected.
Synchronizing UI GameObjects UI should be reconstructed locally from Networked data.
Using managed strings in Networked structs Networked state requires supported fixed-capacity types.
Letting clients edit another player’s ready state Ready requests must be tied to the actual RPC sender.
Showing Start Game only through local UI logic A modified client could still call the start method.
Not revalidating readiness before loading Lobby state may change after the button becomes enabled.
Using local SceneManager.LoadScene Other Fusion clients may remain in the lobby scene.
Forgetting Build Settings Standalone clients may fail to resolve the lobby or gameplay scene.
Not removing disconnected players Stale entries can prevent the match from starting.
Caching Master Client status permanently Authority can change when the current Master Client leaves.
Using RPCs as the only ready-state mechanism Late joiners would not know the current state.
Updating UI immediately before state confirmation The UI can show a value that the authority rejected.
Using an unbounded collection Fusion Networked collections require a defined capacity.
Duplicating lobby state in multiple systems Conflicting sources of truth can produce inconsistent UI.

Summary

An in-room lobby gives players a synchronized staging area after they connect to a Fusion Shared Mode session.

LobbyPlayerData stores compact player information such as display name and ready state. LobbySessionController maintains the Networked player collection, validates ready requests, handles joins and leaves, and lets the Master Client start the match only when the lobby requirements are satisfied.

Each client constructs its own local player-list UI from the synchronized data. The UI sends player intentions, but the authoritative Networked controller decides whether those changes are accepted.

Once all required players are ready, the Master Client uses Fusion’s network scene-loading flow to move the entire room into gameplay together.

Last updated on

Back to top