Building Multiplayer Games with Photon & Unity

multiplayer-photon-unity.md
Building Multiplayer Games with Photon and Unity by Dharmik Gohil

Building Multiplayer Games with Photon & Unity: A Complete Guide

Hey there! I'm Dharmik Gohil, and multiplayer game development is where I found my true passion. When I built Mavericks Battlegrounds — a multiplayer third-person shooter that was selected for CHARUSAT Expo 3.0 — I spent months learning Photon PUN2 networking from scratch. In this comprehensive guide, I'll share everything I learned about building real-time multiplayer games with Unity and Photon.

"Multiplayer is the hardest thing you'll ever build in game development. But when you see two players interacting in real-time for the first time, the feeling is absolutely magical." — Dharmik Gohil

Why Photon PUN2?

There are several networking solutions for Unity — Netcode for GameObjects, Mirror, Fishnet, and Photon. Here's why I chose Photon PUN2 (Photon Unity Networking 2):

  • Cloud-hosted servers: No need to manage your own game servers. Photon's cloud handles matchmaking, room management, and relay
  • Free tier: 20 CCU (Concurrent Users) for free — perfect for indie devs and prototypes
  • Cross-platform: Works on PC, mobile, console, and WebGL out of the box
  • Battle-tested: Used in commercial games with millions of players
  • Excellent documentation: Photon's docs and community forums are top-notch

Setting Up Photon PUN2

  1. Create a Photon account at photonengine.com
  2. Create a new Photon PUN application in the dashboard — you'll get an App ID
  3. Import PUN2 into Unity — search "PUN 2" in the Asset Store or via Package Manager
  4. Enter your App ID in the PUN Wizard that appears after import
  5. Configure server region — choose the closest to your target audience (I used Asia for Indian players)
C# — Connecting to Photon
using Photon.Pun; using Photon.Realtime; using UnityEngine; public class NetworkManager : MonoBehaviourPunCallbacks { void Start() { // Connect to Photon cloud PhotonNetwork.ConnectUsingSettings(); Debug.Log("Connecting to Photon..."); } public override void OnConnectedToMaster() { Debug.Log("Connected to Master Server!"); // Now we can join or create rooms PhotonNetwork.JoinLobby(); } public override void OnJoinedLobby() { Debug.Log("Joined Lobby. Ready to " + "create/join rooms."); } }

Room Management: Creating and Joining Games

In Photon, a "Room" is a game session where players interact. Here is what I used in Mavericks Battlegrounds:

C# — Room Creation and Joining
public void CreateRoom(string roomName, int maxPlayers) { RoomOptions options = new RoomOptions { MaxPlayers = (byte)maxPlayers, IsVisible = true, IsOpen = true, // Custom properties for matchmaking CustomRoomProperties = new ExitGames.Client .Photon.Hashtable { { "map", "Dustbowl" }, { "mode", "TeamDeathmatch" } }, CustomRoomPropertiesForLobby = new string[] { "map", "mode" } }; PhotonNetwork.CreateRoom(roomName, options); } public void JoinRandomRoom() { PhotonNetwork.JoinRandomRoom(); } public override void OnJoinRandomFailed( short returnCode, string message) { // No room found — create a new one CreateRoom("Room_" + Random.Range(1000, 9999), 8); } public override void OnJoinedRoom() { Debug.Log("Joined room: " + PhotonNetwork.CurrentRoom.Name); // Load the game scene for all players if (PhotonNetwork.IsMasterClient) { PhotonNetwork.LoadLevel("GameScene"); } }

Player Synchronization

This is the core of multiplayer — keeping all players in sync. Photon offers two main approaches:

PhotonView & PhotonTransformView

The simplest approach — add a PhotonView component to your player prefab, then add PhotonTransformView to sync position and rotation automatically.

IPunObservable (Custom Serialization)

For more control, implement the IPunObservable interface to send custom data:

C# — Custom Player Sync
public class PlayerSync : MonoBehaviourPun, IPunObservable { private Vector3 networkPosition; private Quaternion networkRotation; private float networkHealth; public float lerpSpeed = 10f; public void OnPhotonSerializeView( PhotonStream stream, PhotonMessageInfo info) { if (stream.IsWriting) { // We own this player — send data stream.SendNext(transform.position); stream.SendNext(transform.rotation); stream.SendNext(GetComponent<PlayerHealth>() .currentHealth); } else { // Network player — receive data networkPosition = (Vector3)stream.ReceiveNext(); networkRotation = (Quaternion)stream .ReceiveNext(); networkHealth = (float)stream.ReceiveNext(); } } void Update() { if (!photonView.IsMine) { // Smoothly interpolate network player transform.position = Vector3.Lerp( transform.position, networkPosition, Time.deltaTime * lerpSpeed); transform.rotation = Quaternion.Lerp( transform.rotation, networkRotation, Time.deltaTime * lerpSpeed); } } }

Remote Procedure Calls (RPCs)

RPCs let you call functions on other players' machines. I used RPCs extensively in Mavericks Battlegrounds for shooting, damage, animations, and chat:

C# — RPC Examples
// Call this when a player shoots public void Shoot() { // Execute locally first for responsiveness SpawnBulletLocal(); // Tell everyone else about the shot photonView.RPC("RPC_Shoot", RpcTarget.Others, transform.position, transform.forward); } [PunRPC] void RPC_Shoot(Vector3 origin, Vector3 direction, PhotonMessageInfo info) { // Calculate lag compensation float lag = (float)(PhotonNetwork.Time - info.SentServerTimestamp); // Spawn bullet with lag compensation SpawnNetworkBullet(origin, direction, lag); } // Damage notification [PunRPC] void RPC_TakeDamage(float damage, string attackerName) { currentHealth -= damage; ShowDamageIndicator(attackerName); if (currentHealth <= 0) { photonView.RPC("RPC_PlayerDied", RpcTarget.All, attackerName); } }
💡 Dharmik's Tip: Always process actions locally first for instant feedback, then sync via RPCs. This is called "client-side prediction" and it makes your game feel responsive even with 100ms+ latency. I learned this after players complained about input lag in the first version of Mavericks Battlegrounds.

Spawning Network Players

Use PhotonNetwork.Instantiate instead of regular Instantiate for network objects:

C# — Network Spawning
public void SpawnPlayer() { // Prefab MUST be in a "Resources" folder Vector3 spawnPoint = GetRandomSpawnPoint(); GameObject player = PhotonNetwork.Instantiate( "PlayerPrefab", // Prefab name in Resources spawnPoint, Quaternion.identity ); // Set up local player camera if (player.GetComponent<PhotonView>().IsMine) { player.GetComponent<PlayerSetup>() .InitializeLocalPlayer(); } }

Handling Latency and Lag Compensation

This is the hardest part of multiplayer development. Here are the techniques I used in Mavericks Battlegrounds:

  • Client-Side Prediction: Process input immediately on the local client, then reconcile with server state
  • Interpolation: Smoothly move network players between received positions using Vector3.Lerp
  • Lag Compensation: Use PhotonMessageInfo.SentServerTimestamp to account for network delay
  • Dead Reckoning: Predict where a player will be based on their last known velocity
  • Send Rate Optimization: Reduce serialization rate for distant objects (Photon default is 10 times/second)
⚠️ Common Pitfall: Don't sync everything! Only sync what other players need to see. In Mavericks Battlegrounds, I initially synced every animation parameter and particle effect — the bandwidth usage was insane. Strip it down to position, rotation, and essential states only.

Building a Lobby System

A good lobby system is crucial for player experience. Here's the architecture I built:

  1. Main Menu: Connect to Photon on launch, show loading indicator
  2. Lobby Screen: Display available rooms with player count, map, and game mode
  3. Room Screen: Show connected players, ready status, team selection
  4. Countdown: When all players are ready, Master Client starts the countdown
  5. Game Load: Master Client calls PhotonNetwork.LoadLevel() to synchronize scene loading

Lessons from Mavericks Battlegrounds

Building Mavericks Battlegrounds — a multiplayer third-person shooter with team deathmatch — taught me invaluable lessons about netcode. Here are my biggest takeaways as Dharmik Gohil:

  • Test with real latency: Use Photon's built-in network simulation to add artificial lag during development
  • Master Client authority: Let the Master Client handle game state decisions (score, round end, spawns) to prevent cheating
  • Graceful disconnection: Handle players leaving mid-game gracefully — clean up their objects, redistribute roles
  • Optimize bandwidth: Compress data, reduce send rates, use delta compression for state
  • Playtest early: Network bugs only appear with real players. I organized LAN testing sessions at CHARUSAT with my classmates

"The first time I saw four players running around my game world simultaneously, I literally jumped out of my chair. Multiplayer is hard, but the payoff is incredible. Start simple — make a multiplayer chat, then pong, then work your way up." — Dharmik Gohil

Next Steps

  • Explore Photon Fusion for tick-based networking (better for competitive games)
  • Learn about dedicated servers vs relay servers for anti-cheat
  • Implement matchmaking with Photon's custom properties and SQL filters
  • Study Valve's networking model (Source Engine) for advanced lag compensation