InRoomUI the multiplayer room lobby controller for a Photon PUN game.

Table of Contents

1️⃣ What is this script?

InRoomUI is the multiplayer room lobby controller for a Photon PUN game.

It handles:

  • Showing players in the room
  • Selecting car
  • Selecting track
  • Ready / Not Ready state
  • Syncing custom properties (car, country, city, flags, etc.)
  • Deciding when the game can start

It runs before the match starts, inside the lobby.


2️⃣ Interfaces used (VERY IMPORTANT)

IInRoomCallbacks, IOnEventCallback

These allow this script to:

  • Detect when players join / leave
  • Detect player property changes
  • Detect room property changes
  • Receive custom Photon events (like StartGame)

3️⃣ Serialized Fields (UI & Settings)

These are UI references set from the Unity Inspector:

  • PlayerItemInRoomUI → One player row UI
  • SelectCarButton, SelectTrackButton
  • SelectedTrackIcon, SelectedTrackText
  • ReadyButton
  • ReadyColors, NotReadyColors
  • MinimumPlayersForStart → Game cannot start before this

📌 These do not do anything alone — they are used by logic later


4️⃣ Important Properties

Room CurrentRoom => PhotonNetwork.CurrentRoom;
bool IsMaster => PhotonNetwork.IsMasterClient;
bool IsRandomRoom => CurrentRoom.CustomProperties.ContainsKey(C.RandomRoom);
Player LocalPlayer => PhotonNetwork.LocalPlayer;

These are shortcuts so code is readable.


5️⃣ Dictionary of Players

Dictionary<Player, PlayerItemInRoomUI> Players;

This maps:

Photon Player → UI Row

Every player in the room gets one UI entry.


6️⃣ Awake() – Button setup

This runs once when the object is created.

What happens here:

✔ Disable player UI template
✔ Hook buttons to actions

SelectTrackButton → Open track selection window
SelectCarButton   → Open car selection window
ReadyButton       → Toggle ready state

Nothing multiplayer happens here yet.


7️⃣ OnEnable() – Lobby setup (VERY IMPORTANT)

This runs every time the lobby screen opens.

🔹 Step-by-step:

a) Register Photon callbacks

PhotonNetwork.AddCallbackTarget(this);

Without this → no multiplayer events received


b) Hide Track selection if not allowed

SelectTrackButton.SetActive(IsMaster || IsRandomRoom);

✔ Only Master Client can pick track
✔ OR random-room allows voting


c) Clear old player UI

Destroy old UI objects
Players.Clear();

This prevents duplicate UI rows


d) Create UI for every player in the room

foreach (CurrentRoom.Players)
    TryUpdateOrCreatePlayerItem(player);

This:

  • Creates UI rows
  • Updates player info
  • Shows ready state

e) Auto-select car if player has none

if (WorldLoading.PlayerCar == null OR no CarName)

✔ Picks first available car
✔ Saves it in Photon Custom Properties
✔ Sets IsReady = false

This guarantees every player has a car


f) Master auto-selects first track

if (IsMaster && no TrackName)

✔ Prevents empty track state
✔ Ensures UI shows something


g) Select Ready button (UI focus)

Just UI polish.


8️⃣ Selecting Car

OnSelectCar(CarPreset selectedCar)

When player selects a car:

✔ Window closes
✔ Local car updated
✔ Photon Custom Properties updated:

  • CarName
  • CarColorIndex

⚠️ Only local player updates this


9️⃣ Selecting Track

OnSelectTrack(TrackPreset selectedTrack)

Two cases:

🎲 Random Room

Each player votes for a track

LocalPlayer.SetCustomProperties(C.TrackName)

👑 Normal Room

Only Master Client decides

CurrentRoom.SetCustomProperties(C.TrackName)

10️⃣ Ready Button Click (IMPORTANT)

OnReadyClick()

What REALLY happens here:

✔ Toggle Ready state
✔ Save car color again
✔ Save Country, City, Flag info

C.CountryName
C.CityName
C.flagType
C.FlagPaint

This info is:

  • Sent to all players
  • Used later for UI / flags / nameplates

📌 PlayerPrefs is used only to read local data
📌 Photon syncs it to everyone


✅ END OF PART 1

So far you learned:

  • What this script is
  • How lobby initializes
  • How car/track/ready works
  • How player data is synced

Player UI updates, Ready check, voting, and game start

11️⃣ TryUpdateOrCreatePlayerItem(Player targetPlayer)

This is the heart of the lobby logic.
It is called when:

  • Player joins

  • Player leaves

  • Player changes any custom property

  • Room properties change

  • Master client changes


🔸 a) Debug info (Country & City)

if (targetPlayer.CustomProperties.ContainsKey(C.CountryName))
{
Debug.Log(targetPlayer.CustomProperties[C.CityName] + " wth " +
targetPlayer.CustomProperties[C.CountryName]);
}

✔ Confirms that custom properties are synced correctly
✔ Helpful for flag / location UI debugging


🔸 b) Create or reuse Player UI

if (!Players.TryGetValue(targetPlayer, out playerItem))
{
playerItem = Instantiate(PlayerItemUIRef);
Players.Add(targetPlayer, playerItem);
}

✔ Prevents duplicate UI rows
✔ One UI row per Photon player


🔸 c) Validate required properties

if (!customProps.ContainsKey(C.CarName) ||
!customProps.ContainsKey(C.IsReady))

If player has not finished setup yet:

❌ UI row is hidden
✔ Prevents half-loaded players showing wrong info


12️⃣ Kick system (Master only)

if (PhotonNetwork.IsMasterClient && !IsRandomRoom)

✔ Master client can kick players
✔ Random rooms = no kicking

This function is passed into the UI button.


13️⃣ Update UI row

playerItem.UpdateProperties(targetPlayer, kickAction);

This usually:

  • Updates nickname

  • Car name

  • Country flag

  • Ready icon

  • Kick button visibility


14️⃣ Ready button color (local player only)

if (targetPlayer.IsLocal)
{
ReadyButton.colors = idReady ? ReadyColors : NotReadyColors;
}

✔ Green = Ready
✔ Default = Not ready
✔ Only changes for yourself


15️⃣ GAME START CHECK (CRITICAL LOGIC)

if (IsMaster && !WaitStartGame &&
CurrentRoom.PlayerCount >= MinimumPlayersForStart &&
CurrentRoom.Players.All(p => p.Value.IsReady))

All conditions MUST be true:

Condition Meaning
IsMaster Only Master starts game
!WaitStartGame Prevent double start
MinimumPlayersForStart Enough players
All players ready Fair start

💥 If any one fails → game does NOT start


16️⃣ Track voting system (Random Room only)

a) Create vote counter

Dictionary<string, int> votesForTracks;

Each track starts with 0 votes


b) Count votes

foreach (player)
{
if player voted
votesForTracks[track]++;
}

Each player's selected track increments a counter.


c) Find most voted track(s)

int maxVotes = votesForTracks.Max(v => v.Value);

✔ Supports ties


d) Random tie-breaker

selectedTracks.RandomChoice();

✔ Prevents same track always winning
✔ Keeps randomness fair


e) Save selected track to room

CurrentRoom.SetCustomProperties(C.TrackName)

This locks the track for everyone


17️⃣ Start Game Event

PhotonNetwork.RaiseEvent(
PE.StartGame,
null,
Receivers = All
);

✔ Sent to every client
✔ Reliable delivery
✔ This is NOT RPC — better for state sync


18️⃣ WaitStartGame = true

This is VERY IMPORTANT

It prevents:

  • Double event sending

  • Scene loading twice

  • Room corruption

Without it → multiple StartGame events


19️⃣ Player leaving & joining

Player joins:

OnPlayerEnteredRoom()

✔ Create UI
✔ Sync properties


Player leaves:

OnPlayerLeftRoom()

✔ Remove UI
✔ Recheck readiness


20️⃣ Master Client switched

OnMasterClientSwitched()

If original host leaves:

✔ New master gets control
✔ Track selection enabled
✔ Properties re-synced


21️⃣ Room property update (Track UI)

OnRoomPropertiesUpdate()

When track changes:

✔ Update track icon
✔ Update regime icon
✔ Update text label

This is pure UI update


22️⃣ Start Game Event Handling

public void OnEvent(EventData photonEvent)

When PE.StartGame is received:

a) Reset player state

IsReady = false
IsLoaded = false

✔ Prepares loading screen


b) Load selected track

WorldLoading.LoadingTrack = track;
LoadingScreenUI.LoadScene(...)

✔ All players load same scene
✔ Multiplayer-safe


c) Close room (Master only)

CurrentRoom.IsOpen = false;
CurrentRoom.IsVisible = false;

✔ Prevents late joins
✔ Locks match


using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using Photon.Pun; using Photon.Realtime; using ExitGames.Client.Photon; using Hashtable = ExitGames.Client.Photon.Hashtable; using GameBalance; ///

/// To display and control the parameters in the room. ///

using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Photon.Pun;
using Photon.Realtime;
using ExitGames.Client.Photon;
using Hashtable = ExitGames.Client.Photon.Hashtable;
using GameBalance;

/// <summary>
/// To display and control the parameters in the room.
/// </summary>
public class InRoomUI : MonoBehaviour, IInRoomCallbacks, IOnEventCallback
{

[SerializeField] PlayerItemInRoomUI PlayerItemUIRef;
[SerializeField] Button SelectCarButton;
[SerializeField] Button SelectTrackButton;
[SerializeField] Image SelectedTrackIcon;
[SerializeField] Image RegimeIcon;
[SerializeField] TextMeshProUGUI SelectedTrackText;
[SerializeField] SelectTrackUI SelectTrackUI;
[SerializeField] SelectCarMenuUI SelectCarMenuUI;
[SerializeField] int MinimumPlayersForStart = 2;
[SerializeField] Button ReadyButton;
[SerializeField] ColorBlock ReadyColors;
[SerializeField] ColorBlock NotReadyColors;

//Connected players dictionary.
Dictionary<Player, PlayerItemInRoomUI> Players = new Dictionary<Player, PlayerItemInRoomUI>();

Room CurrentRoom { get { return PhotonNetwork.CurrentRoom; } }
bool IsMaster { get { return PhotonNetwork.IsMasterClient; } }
bool IsRandomRoom { get { return CurrentRoom.CustomProperties.ContainsKey (C.RandomRoom); } }
Player LocalPlayer { get { return PhotonNetwork.LocalPlayer; } }

bool WaitStartGame;

void Awake ()
{
//Initialized all buttons.

PlayerItemUIRef.SetActive (false);
SelectTrackButton.onClick.AddListener (() =>
{
WindowsController.Instance.OpenWindow (SelectTrackUI);
SelectTrackUI.OnSelectTrackAction = OnSelectTrack;
});
SelectCarButton.onClick.AddListener (() =>
{
WindowsController.Instance.OpenWindow (SelectCarMenuUI);
SelectCarMenuUI.OnSelectCarAction = OnSelectCar;
});

ReadyButton.onClick.AddListener (OnReadyClick);
}

void OnEnable ()
{
PhotonNetwork.AddCallbackTarget (this);

if (CurrentRoom == null)
{
return;
}

SelectTrackButton.SetActive (IsMaster || IsRandomRoom);
OnRoomPropertiesUpdate (CurrentRoom.CustomProperties);

foreach (var playerKV in Players)
{
Destroy (playerKV.Value.gameObject);
}

Players.Clear ();

foreach (var playerKV in CurrentRoom.Players)
{
TryUpdateOrCreatePlayerItem (playerKV.Value);
}

var localPlayer = PhotonNetwork.LocalPlayer;

//Choosing the first car at the entrance to the room.
if (WorldLoading.PlayerCar == null || !LocalPlayer.CustomProperties.ContainsKey(C.CarName))
{
var selectedCar = WorldLoading.AvailableCars.First ();
WorldLoading.PlayerCar = selectedCar;
LocalPlayer.SetCustomProperties (C.CarName, selectedCar.CarCaption);
LocalPlayer.SetCustomProperties (C.CarName, selectedCar.CarCaption, C.CarColorIndex, PlayerProfile.GetCarColorIndex (selectedCar), C.IsReady, false);
}

//Choosing the first track when create the room.
if (PhotonNetwork.IsMasterClient && (CurrentRoom.CustomProperties == null || !CurrentRoom.CustomProperties.ContainsKey (C.TrackName)))
{
var hashtable = new Hashtable ();
hashtable.Add (C.TrackName, B.GameSettings.Tracks.First ().name);
CurrentRoom.SetCustomProperties (hashtable);
}

ReadyButton.Select ();
}

public virtual void OnDisable ()
{
PhotonNetwork.RemoveCallbackTarget (this);
}

void OnSelectCar (CarPreset selectedCar)
{
WindowsController.Instance.OnBack ();
WorldLoading.PlayerCar = selectedCar;
LocalPlayer.SetCustomProperties (C.CarName, selectedCar.CarCaption, C.CarColorIndex, PlayerProfile.GetCarColorIndex(selectedCar));
}

void OnSelectTrack (TrackPreset selectedTrack)
{
WindowsController.Instance.OnBack ();
var hashtable = new Hashtable ();
hashtable.Add (C.TrackName, selectedTrack.name);

if (IsRandomRoom)
{
LocalPlayer.SetCustomProperties (hashtable);
}
else if (IsMaster)
{
CurrentRoom.SetCustomProperties (hashtable);
}
}

void OnReadyClick ()
{
LocalPlayer.SetCustomProperties (C.IsReady, !(bool)LocalPlayer.CustomProperties[C.IsReady], C.CarColorIndex, PlayerProfile.GetCarColorIndex(WorldLoading.PlayerCar));

Debug.Log("wthOnReadyPlayer" + textures_runtime_tgc.myCityName);
var csss = new Hashtable();
csss.Add(C.CountryName, textures_runtime_tgc.myCountryName);
csss.Add(C.CityName, textures_runtime_tgc.myCityName);
csss.Add(C.flagType, PlayerPrefs.GetString("physicalFlag", "0")); //yaw

//csss.Add(C.FlagPaint, (string)PlayerPrefs.GetInt("flagastextureBool", 0)); //yaw
csss.Add(C.FlagPaint, PlayerPrefs.GetInt("flagastextureBool", 0).ToString());

LocalPlayer.SetCustomProperties(csss);
}

//Updating a player when changing any property.
void TryUpdateOrCreatePlayerItem (Player targetPlayer)
{
if (targetPlayer.CustomProperties.ContainsKey(C.CountryName))
{
Debug.Log(targetPlayer.CustomProperties[C.CityName]+"wth" + targetPlayer.CustomProperties[C.CountryName]);
}

PlayerItemInRoomUI playerItem = null;

//Get or create PlayerItem.
if (!Players.TryGetValue(targetPlayer, out playerItem))
{
playerItem = Instantiate (PlayerItemUIRef, PlayerItemUIRef.transform.parent);
Players.Add (targetPlayer, playerItem);
}

var customProps = targetPlayer.CustomProperties;

//SetActive(false) if player without any property.
if (customProps == null ||
!customProps.ContainsKey (C.CarName) ||
!customProps.ContainsKey (C.IsReady)
)
{
playerItem.SetActive (false);
return;
}

var idReady = (bool)customProps[C.IsReady];

System.Action kickAction = null;
if (PhotonNetwork.IsMasterClient && !IsRandomRoom)
{
kickAction = () =>
{
PhotonNetwork.CloseConnection (targetPlayer);
};
}

playerItem.SetActive (true);
playerItem.UpdateProperties (targetPlayer, kickAction);

if (targetPlayer.IsLocal)
{
ReadyButton.colors = idReady ? ReadyColors : NotReadyColors;
}

//We inform all players about the start of the game.
if (IsMaster && !WaitStartGame &&
CurrentRoom.PlayerCount >= MinimumPlayersForStart &&
CurrentRoom.Players.All
(p =>
p.Value.CustomProperties.ContainsKey(C.IsReady) &&
(bool)p.Value.CustomProperties[C.IsReady]
))
{
//Calculate votes
if (CurrentRoom.CustomProperties.ContainsKey (C.RandomRoom))
{
Dictionary<string, int> votesForTracks = new Dictionary<string, int>();

foreach (var track in B.MultiplayerSettings.AvailableTracksForMultiplayer)
{
votesForTracks.Add (track.name, 0);
}

//Get all votes.
foreach (var player in CurrentRoom.Players)
{
var track = player.Value.CustomProperties.ContainsKey(C.TrackName)? (string)player.Value.CustomProperties[C.TrackName]: "";
if (!string.IsNullOrEmpty (track))
{
votesForTracks[track]++;
}
}

//Get max votes.
int maxVotes = votesForTracks.Max(kv => kv.Value);

//Get tracks with max votes.
List<string> selectedTracks = new List<string>();
foreach (var track in votesForTracks)
{
if (track.Value >= maxVotes)
{
selectedTracks.Add (track.Key);
}
}

//Random choice
var customProperties = new Hashtable ();
customProperties.Add (C.TrackName, selectedTracks.RandomChoice());
CurrentRoom.SetCustomProperties (customProperties);
}

PhotonNetwork.RaiseEvent (PE.StartGame, null, new RaiseEventOptions () { Receivers = ReceiverGroup.All }, SendOptions.SendReliable);
WaitStartGame = true;
}
}

//Clear player items.
public void RemoveAllPlayers ()
{
foreach (var player in Players)
{
Destroy (player.Value.gameObject);
}
Players.Clear ();
}

public void OnMasterClientSwitched (Player newMasterClient)
{
Debug.LogFormat ("New master is player [{0}]", newMasterClient.NickName);
SelectTrackButton.SetActive (IsMaster);

foreach (var player in CurrentRoom.Players)
{
TryUpdateOrCreatePlayerItem (player.Value);
}
UpdateCustomProperties ();
}

public void OnPlayerEnteredRoom (Player newPlayer)
{
TryUpdateOrCreatePlayerItem (newPlayer);
UpdateCustomProperties ();
}

public void OnPlayerLeftRoom (Player otherPlayer)
{
if (Players.ContainsKey (otherPlayer))
{
Destroy(Players[otherPlayer].gameObject);
Players.Remove (otherPlayer);
}

UpdateCustomProperties ();
}

public void OnPlayerPropertiesUpdate (Player targetPlayer, Hashtable changedProps)
{
TryUpdateOrCreatePlayerItem (targetPlayer);
}

public void OnRoomPropertiesUpdate (Hashtable propertiesThatChanged)
{
if (propertiesThatChanged.ContainsKey (C.TrackName))
{
var trackName = (string)propertiesThatChanged[C.TrackName];

var track = B.GameSettings.Tracks.FirstOrDefault (t => t.name == trackName);

SelectedTrackIcon.sprite = track.TrackIcon;
RegimeIcon.sprite = track.RegimeSettings.RegimeImage;
SelectedTrackText.text = string.Format ("{0}: {1}", track.TrackName, track.RegimeSettings.RegimeCaption);
}
}

void UpdateCustomProperties ()
{
if (IsMaster)
{
var customProperties = new Hashtable ();
customProperties.Add (C.RoomCreator, PlayerProfile.NickName);
CurrentRoom.SetCustomProperties (customProperties);
}
}

//We get information about the start and start loading the level.
public void OnEvent (EventData photonEvent)
{
if (photonEvent.Code == PE.StartGame)
{
WorldLoading.IsMultiplayer = true;
PhotonNetwork.LocalPlayer.SetCustomProperties (C.IsReady, false, C.IsLoaded, false, C.TrackName, "");

if (!CurrentRoom.CustomProperties.ContainsKey (C.TrackName))
{
return;
}

var trackName = (string)CurrentRoom.CustomProperties[C.TrackName];

var track = B.GameSettings.Tracks.FirstOrDefault (t => t.name == trackName);
WorldLoading.LoadingTrack = track;

LoadingScreenUI.LoadScene (track.SceneName, track.RegimeSettings.RegimeSceneName);

if (IsMaster)
{
CurrentRoom.IsOpen = false;
CurrentRoom.IsVisible = false;
}
}
}
}

Leave a Reply

Your email address will not be published. Required fields are marked *