Multiplayer
CCDS multiplayer runs on Photon PUN2: rooms, networked vehicles, synchronised missions, chat and nametags.
Prerequisites
- Photon PUN2 imported (free tier is sufficient).
- A PUN App ID from
https://dashboard.photonengine.com, entered via the PUN Wizard or the CCDS Setup Wizard. - The
PHOTON_UNITY_NETWORKINGscripting symbol defined — PUN2 sets it on import.
Without PUN2, all networked code compiles out and CCDS runs single-player. That is a supported configuration, not a broken one.
Single-player and multiplayer are the same scenes
There is no separate multiplayer scene or mission set. CCDS_NetworkManager.networkMode decides the mode:
CCDS_NetworkManager.Instance.SetSinglePlayerMode();
CCDS_NetworkManager.Instance.SetMultiplayerMode();
// Anywhere in gameplay code, prefer this over raw Photon checks
if (CCDS.IsMultiplayer()) { /* ... */ }
CCDS.IsMultiplayer() is the check to use. Hand-rolled PhotonNetwork.IsConnectedAndReady && InRoom tests drift out of agreement with the rest of CCDS.
The player's preference persists:
CCDS.SetPreferMultiplayer(true);
bool prefers = CCDS.GetPreferMultiplayer();
Room configuration
On CCDS_NetworkManager:
| Field | Type | Range | Default | Purpose |
|---|---|---|---|---|
networkMode |
NetworkMode |
— | SinglePlayer |
Offline or connected |
maxPlayersPerRoom |
int |
2–20 | 8 |
Room capacity |
playerName |
string |
— | "Player" |
Displayed name |
roomName |
string |
— | "" |
Current or target room |
roomIsVisible |
bool |
— | true |
Listed in the lobby |
roomIsOpen |
bool |
— | true |
Accepts new players |
missionType |
string |
— | "FreeRoam" |
Current room's mission type |
defaultMissionType |
string |
— | "FreeRoam" |
For new rooms |
Read-only state getters: IsInRoomAndReady, InLobby, InRoom, IsMultiplayer, IsMasterClient.
Connection flow
ConnectToPhoton()
→ OnConnectedToMaster
→ JoinLobby()
→ CreateRoom() / JoinRoom() / JoinOrCreateRoom() / JoinRandomRoom()
→ OnJoinedRoom
→ PhotonNetwork.Instantiate spawns the player vehicle
The public entry points:
| Method | Does |
|---|---|
ConnectToPhoton() |
Connect using the configured App ID |
JoinLobby() |
Enter the default lobby |
CreateRoom(name) |
Create a room |
CreateRoomWithOptions(name, roomOptions, typedLobby) |
Create with explicit RoomOptions |
JoinRoom(name) |
Join a named room |
JoinOrCreateRoom(name) |
Join if it exists, otherwise create |
JoinRandomRoom() |
Matchmake |
LeaveRoom() |
Leave |
DisconnectFromPhoton() |
Disconnect |
AutoStartMultiplayer() |
Connect and matchmake in one call |
ConnectToRegion(regionCode) |
Pin to a Photon region |
DisconnectAndLoadScene(sceneIndex) |
Leave and load a scene cleanly |
SignalPlayerReady() |
Mark this player ready |
IsPlayerLoaded(player) |
Has that player finished loading |
Network rates
In CCDS_Settings.asset:
| Setting | Range | Default | Meaning |
|---|---|---|---|
photonSendRate |
10–60 | 30 |
Updates sent per second |
photonSerializationRate |
10–60 | 20 |
Serializations per second |
Keep serialization below send rate. Raising both smooths remote vehicles and costs bandwidth; the shipped 30/20 pair is tuned for vehicles.
Networked vehicles
A player vehicle needs PhotonView, CCDS_Player and RCCP_PhotonSync. The vehicle setup prompt adds all of them and wires the observed components — see Add Your Own Vehicle.
Authority follows Photon: the owner simulates, remotes interpolate. CCDS adds a control lock on top, so it can hold authority across mission transitions:
vehicle.LockControl();
vehicle.UnlockControl();
vehicle.SetControlWithLock(true);
bool locked = vehicle.ControlLockedByCCDS;
While locked, RCCP_PhotonSync will not override canControl or externalControl. Use SetControlWithLock rather than touching RCCP.SetControl directly whenever CCDS owns the decision — otherwise the sync component silently reverts you next frame.
Mission participation
Multiplayer missions derive from ACCDS_MultiplayerMissionBase.
| Concept | Held in |
|---|---|
| Who is playing | missionPlayers |
| Where each player spawns | playerSpawnAssignments — actor number → spawn index |
| Spawn slots | child transforms under transportToThisLocation |
| Slot resolution | GetPlayerSpawnPosition(), with blocked-slot fallbacks |
An empty missionPlayers cancels the mission.
Mission AI activates locally on each participant. Non-participants never enable the mission, so they never see its AI. Do not "fix" this with an RPC — see Create a Mission.
Markers synchronise isOnCooldown, missionInProgress, remainingCooldown and their state enum through OnPhotonSerializeView, master-authoritative.
Writing networked components
Derive from ACCDS_PunComponent. It extends MonoBehaviourPunCallbacks, requires a PhotonView via [RequireComponent], and caches the view and scene manager.
using BoneCrackerGames.CCDS;
public class MyNetworkedThing : ACCDS_PunComponent {
void OnEnable() {
CCDS_Events.OnNetworkPlayerJoined += HandleJoined;
}
void OnDisable() {
CCDS_Events.OnNetworkPlayerJoined -= HandleJoined;
}
void HandleJoined(string playerName) {
CCDS.DebugLog($"{playerName} joined");
}
}
If you make an existing class inherit MonoBehaviourPunCallbacks, its OnEnable and OnDisable must be public override and call base.OnEnable() / base.OnDisable(). PUN registers its callback targets there. A plain private void OnEnable() hides the base method, no Photon callback ever fires, and the compiler gives at most a warning. This is the single most expensive mistake in networked CCDS code.
Traffic sync is the same story: the master client owns traffic and instantiates it over the network. Clients must not spawn their own.
Chat and nametags
Chat is CCDS_ChatManager plus CCDS_UI_ChatPanel. Return sends, Esc closes.
CCDS.ShowChat();
CCDS.HideChat();
CCDS.ToggleChatVisibility();
CCDS.SetChatVisibilityMode(ChatVisibilityMode.ShowOnNewMessages);
ChatVisibilityMode is AlwaysVisible, ShowOnNewMessages or Hidden.
Nametags come from CCDS_UI_NametagManager using CCDS_Settings.nametagPrefab, which must contain a TextMeshProUGUI and a CanvasGroup.
Hold Tab for the scoreboard (CCDS_UI_MultiplayerIngame).
Progression
Persisted per player: multiplayerLevel (starts at CCDS_Settings.defaultMultiplayerLevel, default 1), multiplayerXP, multiplayerWins, multiplayerLosses, totalMultiplayerMatches, favoriteRoomName, preferredRegion, lastOnlineTime.
Testing two clients on one PC
Enable CCDS_Settings.useProcessIdForSaveFile. Each instance then writes its own save file, keyed by process ID, so two clients do not fight over one file.
Turn it off for production builds. Left on, every launch is a new save and players lose their progress.
Events
Sixteen network events are available — connection, room, player and master-client changes. See Events Catalog.
Common problems
| Symptom | Cause |
|---|---|
| Connection never completes | No App ID, or PUN Wizard never run |
| Photon callbacks never fire | OnEnable/OnDisable not public override calling base |
| Remote vehicles stutter | Send/serialization rates too low, or sync component missing |
| Remote vehicles do not move | PhotonView present but RCCP_PhotonSync absent |
| Traffic desynced between clients | Clients spawning traffic locally; only the master may |
| Non-participants see mission AI | Mission AI being spawned over the network |
| Both test clients share a save | useProcessIdForSaveFile disabled |
| Player spawns inside another | Blocked spawn slot — check transportToThisLocation slots |
| Scene loads out of sync | Use PhotonNetwork.LoadLevel while IsConnectedAndReady && InRoom |