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 UISelectCarButton,SelectTrackButtonSelectedTrackIcon,SelectedTrackTextReadyButtonReadyColors,NotReadyColorsMinimumPlayersForStart→ 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:
CarNameCarColorIndex
⚠️ 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)
✔ Confirms that custom properties are synced correctly
✔ Helpful for flag / location UI debugging
🔸 b) Create or reuse Player UI
✔ Prevents duplicate UI rows
✔ One UI row per Photon player
🔸 c) Validate required properties
If player has not finished setup yet:
❌ UI row is hidden
✔ Prevents half-loaded players showing wrong info
12️⃣ Kick system (Master only)
✔ Master client can kick players
✔ Random rooms = no kicking
This function is passed into the UI button.
13️⃣ Update UI row
This usually:
-
Updates nickname
-
Car name
-
Country flag
-
Ready icon
-
Kick button visibility
14️⃣ Ready button color (local player only)
✔ Green = Ready
✔ Default = Not ready
✔ Only changes for yourself
15️⃣ GAME START CHECK (CRITICAL LOGIC)
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
Each track starts with 0 votes
b) Count votes
Each player's selected track increments a counter.
c) Find most voted track(s)
✔ Supports ties
d) Random tie-breaker
✔ Prevents same track always winning
✔ Keeps randomness fair
e) Save selected track to room
This locks the track for everyone
17️⃣ Start Game Event
✔ 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:
✔ Create UI
✔ Sync properties
Player leaves:
✔ Remove UI
✔ Recheck readiness
20️⃣ Master Client switched
If original host leaves:
✔ New master gets control
✔ Track selection enabled
✔ Properties re-synced
21️⃣ Room property update (Track UI)
When track changes:
✔ Update track icon
✔ Update regime icon
✔ Update text label
This is pure UI update
22️⃣ Start Game Event Handling
When PE.StartGame is received:
a) Reset player state
✔ Prepares loading screen
b) Load selected track
✔ All players load same scene
✔ Multiplayer-safe
c) Close room (Master only)
✔ 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;
}
}
}
}
