The Definitive Guide to Photon Engine & Microsoft PlayFab Integration: Enterprise Backend Architecture, Custom Authentication, Secure Matchmaking, CloudScript, and Real-Time State Sync
Executive Overview & Cloud Infrastructure Roadmap
In modern commercial video game development, constructing a scalable, secure, and production-ready multiplayer experience requires two fundamentally distinct cloud systems working in tight synchronization:
- The Real-Time Transport Engine (Photon): Responsible for sub-50ms peer-to-peer relay communications, high-frequency transform interpolation,
[PunRPC] execution, and distributed physics synchronization.
- The Backend-as-a-Service Platform (Microsoft Azure PlayFab): Responsible for persistent player identities, secure authentication (Steam, Xbox, PlayStation, iOS, Android, Custom ID), server-authoritative virtual currency and economy, cloud inventory catalogs, serverless CloudScript / Azure Functions, player MMR matchmaking queues, and global anti-cheat leaderboards.
Deploying a real-time multiplayer title with Photon alone leaves the game susceptible to catastrophic security exploits: client-side currency tampering, unauthenticated user impersonation, room griefing, and lack of persistent progression. Conversely, PlayFab alone cannot handle 60Hz real-time player movement and physics.
By fusing Photon Engine (PUN 2 / Photon Realtime) with Microsoft PlayFab, you create an enterprise-grade cloud architecture utilized by top-tier commercial studios.
This masterclass is a complete, deep-dive curriculum covering the entire PlayFab + Photon integration lifecycle: cryptographic custom authentication handshakes, ticket-based matchmaking queues, serverless CloudScript reward distribution, synchronized player custom properties, server-authoritative inventory stores, and production-ready C# architecture.
ENTERPRISE PLAYFAB + PHOTON CLOUD TOPOLOGY
======================================================================================================
[ Client Application (Unity) ]
|
| 1. LoginWithCustomID() / LoginWithSteam()
v
[ Microsoft Azure PlayFab Backend ]
| - Verifies Player Identity & Generates SessionTicket
| - Returns PlayFabId, Virtual Currency, and Inventory Catalog
|
| 2. GetPhotonAuthenticationToken()
v
[ PlayFab Photon Token Generator ]
| - Issues Cryptographic Photon Custom Auth Token
|
| 3. ConnectWithCustomAuth(AuthType.Custom, Token)
v
[ Photon Cloud Master Server (Region: US / EU / ASIA) ]
| - Performs Webhook Handshake with PlayFab to Validate Token
| - Binds Verified PlayFabId as Photon UserId
|
| 4. Matchmaking Ticket -> Join Dedicated Photon Room (MatchId)
v
[ Photon Game Room (Real-Time 60Hz Relay) ]
| - High-Speed Player Movement Synchronization (IPunObservable)
| - Remote Procedure Calls ([PunRPC])
| - Player Custom Properties (Synced Skin, Clan, Title)
|
| 5. Match Concluded -> ExecuteCloudScript("AwardMatchRewards")
v
[ Serverless PlayFab CloudScript / Azure Function ]
| - Validates Match Integrity & Score Sanity Checks
| - Grants Authoritative Coins & Updates MMR Leaderboards
======================================================================================================
Module 1: The Modern Cloud Gaming Architecture (PUN + PlayFab)
To architect a commercial multiplayer game, we must clearly delineate the responsibilities between the real-time layer and the persistent backend layer.
1.1 Separation of Responsibilities
| Game Subsystem |
Handling Platform |
Justification |
| Player Account & Identity |
Microsoft PlayFab |
Cross-platform account linking (Steam, Epic, Xbox Live, PSN, Apple Game Center, Google Play). |
| Real-Time Movement & Physics |
Photon Engine |
Microsecond packet relay, position extrapolation, dead reckoning. |
| Virtual Currency & Purchases |
Microsoft PlayFab |
Prevents memory hacking (Cheat Engine / Memory editing) from granting free items. |
| Weapons Firing & Damage RPCs |
Photon Engine |
Instant visual feedback, audio triggers, hitscan confirmation. |
| End-of-Match Rewards & XP |
PlayFab CloudScript |
Serverless validation ensuring clients cannot grant themselves level 100 on match exit. |
| Matchmaking & Skill Rating (MMR) |
Microsoft PlayFab |
Complex algorithmic matchmaking queues, team balancing, and latency grouping. |
| In-Game Chat & Emotes |
Photon Engine |
Low-latency room broadcasting without database write overhead. |
1.2 The Security Vulnerability of Standalone Photon
In a default Photon setup without custom authentication:
- Any user who inspects your game binary can extract your
AppId and connect to your Photon Cloud application.
- Clients can choose arbitrary
UserId strings, enabling bad actors to impersonate developers, administrators, or high-ranked players.
- When players leave a room, their match statistics, acquired items, and level progression are lost unless saved to local files (which are trivially modifiable).
By integrating PlayFab Custom Authentication, Photon rejects any connection attempt that lacks a cryptographically signed token issued by your PlayFab Title.
Module 2: PlayFab Authentication & Photon Custom Authentication Flow
Let us implement the secure authentication handshake between PlayFab and Photon.
2.1 The Authentication Handshake Sequence
The authentication workflow proceeds in five distinct phases:
- Client to PlayFab: The user authenticates with PlayFab via
LoginWithCustomID, LoginWithSteam, or LoginWithEmailAddress.
- PlayFab Token Request: Upon successful login, the client calls
PlayFabClientAPI.GetPhotonAuthenticationToken().
- Photon Configuration: The client configures Photon's
AuthenticationValues, setting AuthType = Custom, AuthType = PhotonCustomAuthType.PlayFab, and passing the PlayFab token and PlayFabId.
- Photon to PlayFab Validation: The Photon Master Server queries the PlayFab API via server-to-server webhook to verify the token's validity.
- Connection Granted: Photon admits the player into the Master Server lobby with their verified
PlayFabId permanently assigned as PhotonNetwork.AuthValues.UserId.
2.2 Implementing the Authentication Controller
using System;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;
using Photon.Pun;
using Photon.Realtime;
namespace MasterclassCloud
{
public class PlayFabPhotonAuthManager : MonoBehaviourPunCallbacks
{
public static PlayFabPhotonAuthManager Instance { get; private set; } = null!;
public string PlayFabPlayerId { get; private set; } = string.Empty;
public string PlayFabSessionTicket { get; private set; } = string.Empty;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void Start()
{
// Initiate auto-login on startup
LoginToPlayFabWithDevice();
}
// 1. Authenticate with PlayFab using Device Unique Identifier
public void LoginToPlayFabWithDevice()
{
Debug.Log("[PlayFab] Initiating login with device ID...");
var request = new LoginWithCustomIDRequest
{
CustomId = SystemInfo.deviceUniqueIdentifier,
CreateAccount = true,
InfoRequestParameters = new GetPlayerCombinedInfoRequestParams
{
GetPlayerProfile = true,
GetUserAccountInfo = true,
GetUserVirtualCurrency = true
}
};
PlayFabClientAPI.LoginWithCustomID(request, OnPlayFabLoginSuccess, OnPlayFabLoginFailure);
}
private void OnPlayFabLoginSuccess(LoginResult result)
{
PlayFabPlayerId = result.PlayFabId;
PlayFabSessionTicket = result.SessionTicket;
Debug.Log($"[PlayFab] Login successful! PlayFabId: {PlayFabPlayerId}");
// 2. Request Photon Custom Authentication Token
RequestPhotonToken();
}
private void OnPlayFabLoginFailure(PlayFabError error)
{
Debug.LogError($"[PlayFab] Login failed: {error.GenerateErrorReport()}");
}
// 3. Request Photon Authentication Token from PlayFab
private void RequestPhotonToken()
{
Debug.Log("[PlayFab] Requesting Photon Authentication Token...");
var request = new GetPhotonAuthenticationTokenRequest
{
PhotonApplicationId = PhotonNetwork.PhotonServerSettings.AppSettings.AppIdRealtime
};
PlayFabClientAPI.GetPhotonAuthenticationToken(request, OnPhotonTokenSuccess, OnPhotonTokenFailure);
}
private void OnPhotonTokenSuccess(GetPhotonAuthenticationTokenResult result)
{
Debug.Log("[PlayFab] Photon token acquired. Connecting to Photon Cloud...");
// 4. Configure Photon Custom Authentication Values
AuthenticationValues customAuth = new AuthenticationValues();
customAuth.AuthType = CustomAuthenticationType.Custom;
customAuth.AddAuthParameter("username", PlayFabPlayerId);
customAuth.AddAuthParameter("token", result.PhotonCustomAuthenticationToken);
PhotonNetwork.AuthValues = customAuth;
PhotonNetwork.NickName = PlayFabPlayerId;
// 5. Connect to Photon Master Server
PhotonNetwork.ConnectUsingSettings();
}
private void OnPhotonTokenFailure(PlayFabError error)
{
Debug.LogError($"[PlayFab] Failed to obtain Photon token: {error.GenerateErrorReport()}");
}
// Photon Lifecycle Callbacks
public override void OnConnectedToMaster()
{
Debug.Log($"[Photon] Connected to Master Server successfully! Verified UserId: {PhotonNetwork.LocalPlayer.UserId}");
PhotonNetwork.JoinLobby();
}
public override void OnJoinedLobby()
{
Debug.Log("[Photon] Joined Master Lobby. Ready for matchmaking.");
}
public override void OnCustomAuthenticationFailed(string debugMessage)
{
Debug.LogError($"[Photon] Custom Authentication Failed: {debugMessage}");
}
}
}
Module 3: Enterprise Ticket-Based Matchmaking (PlayFab Matchmaking + Photon)
In commercial competitive titles, players do not browse a simple public room list. Instead, they enter a Matchmaking Queue that groups players by Skill Rating (MMR), latency, and party size.
3.1 PlayFab Matchmaking Flow
[ Client A: Request Match ] ====--> [ PlayFab Matchmaking Queue ("Ranked_2v2") ]
[ Client B: Request Match ] ====--> |
v (Evaluates MMR & Latency Rules)
[ Match Created: MatchId "a7f3-49b2-91c0" ] <==+
|
+--> [ Both Clients Join Dedicated Photon Room: "Match_a7f3-49b2-91c0" ]
3.2 Implementing the Matchmaking Controller
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayFab;
using PlayFab.MultiplayerModels;
using Photon.Pun;
using Photon.Realtime;
namespace MasterclassCloud
{
public class PlayFabMatchmaker : MonoBehaviourPunCallbacks
{
public static PlayFabMatchmaker Instance { get; private set; } = null!;
public string ActiveQueueName = "Ranked_1v1";
private string activeTicketId = string.Empty;
private Coroutine? pollCoroutine;
private void Awake()
{
Instance = this;
}
// 1. Submit Matchmaking Ticket
public void StartMatchmaking(int playerMMR = 1200)
{
Debug.Log($"[Matchmaker] Submitting ticket for queue: {ActiveQueueName} (MMR: {playerMMR})...");
var request = new CreateMatchmakingTicketRequest
{
Creator = new MatchmakingPlayer
{
Entity = new PlayFab.MultiplayerModels.EntityKey
{
Id = PlayFabSettings.staticPlayer.EntityId,
Type = PlayFabSettings.staticPlayer.EntityType
},
Attributes = new MatchmakingPlayerAttributes
{
DataObject = new { Skill = playerMMR }
}
},
GiveUpAfterSeconds = 120,
QueueName = ActiveQueueName
};
PlayFabMultiplayerAPI.CreateMatchmakingTicket(request, OnTicketCreated, OnMatchmakingError);
}
private void OnTicketCreated(CreateMatchmakingTicketResult result)
{
activeTicketId = result.TicketId;
Debug.Log($"[Matchmaker] Ticket created: {activeTicketId}. Starting poll loop...");
if (pollCoroutine != null) StopCoroutine(pollCoroutine);
pollCoroutine = StartCoroutine(PollTicketStatusRoutine());
}
// 2. Poll Ticket Status every 6 seconds
private IEnumerator PollTicketStatusRoutine()
{
while (!string.IsNullOrEmpty(activeTicketId))
{
yield return new WaitForSeconds(6.0f);
var request = new GetMatchmakingTicketRequest
{
QueueName = ActiveQueueName,
TicketId = activeTicketId
};
PlayFabMultiplayerAPI.GetMatchmakingTicket(request, OnGetTicketResult, OnMatchmakingError);
}
}
private void OnGetTicketResult(GetMatchmakingTicketResult result)
{
Debug.Log($"[Matchmaker] Ticket status: {result.Status}");
if (result.Status == "Matched")
{
Debug.Log($"[Matchmaker] Match Found! MatchId: {result.MatchId}");
activeTicketId = string.Empty;
// 3. Fetch Match Details to discover opponent PlayFab IDs
GetMatchDetails(result.MatchId);
}
else if (result.Status == "Canceled")
{
Debug.LogWarning("[Matchmaker] Ticket was canceled by server.");
activeTicketId = string.Empty;
}
}
private void GetMatchDetails(string matchId)
{
var request = new GetMatchRequest
{
MatchId = matchId,
QueueName = ActiveQueueName
};
PlayFabMultiplayerAPI.GetMatch(request, (matchResult) =>
{
Debug.Log($"[Matchmaker] Retrieved match details with {matchResult.Members.Count} players.");
JoinPhotonMatchRoom(matchResult.MatchId, matchResult.Members.Count);
}, OnMatchmakingError);
}
// 4. Join or Create the dedicated Photon Room named after the MatchId
private void JoinPhotonMatchRoom(string matchId, int expectedPlayers)
{
string roomName = "MATCH_" + matchId;
Debug.Log($"[Photon] Joining dedicated room: {roomName}...");
RoomOptions roomOptions = new RoomOptions
{
MaxPlayers = (byte)expectedPlayers,
IsOpen = true,
IsVisible = false // Keep matchmaking rooms hidden from public lobby
};
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
}
public override void OnJoinedRoom()
{
Debug.Log($"[Photon] Successfully entered match room: {PhotonNetwork.CurrentRoom.Name} (Players: {PhotonNetwork.CurrentRoom.PlayerCount}/{PhotonNetwork.CurrentRoom.MaxPlayers})");
}
private void OnMatchmakingError(PlayFabError error)
{
Debug.LogError($"[Matchmaker] Error: {error.GenerateErrorReport()}");
activeTicketId = string.Empty;
}
}
}
Module 4: Secure Economy & Inventory Management
A common fatal vulnerability in amateur multiplayer games is allowing the client to execute purchase logic or grant virtual currency locally.
4.1 The Vulnerability: Client-Side Currency Granting
If your client code says:
// VULNERABLE CLIENT CODE
public void OnBossKilled()
{
localPlayerCoins += 500; // Easily modified in memory or via modded DLL
}
Any user running a modified client or memory editor can set localPlayerCoins = 999999 and buy your entire catalog for free.
4.2 The Solution: Serverless CloudScript Execution
All currency awards, item purchases, and inventory deductions must execute inside PlayFab CloudScript / Azure Functions:
// PlayFab CloudScript Function (Node.js / JavaScript)
handlers.PurchaseItemSecure = function (args, context) {
var itemPrice = 250;
var currencyCode = "GC"; // Gold Coins
// 1. Verify player has sufficient balance on the server
var userInventory = server.GetUserInventory({ PlayFabId: currentPlayerId });
var playerCoins = userInventory.VirtualCurrency[currencyCode] || 0;
if (playerCoins < itemPrice) {
return { success: false, message: "Insufficient currency balance." };
}
// 2. Authoritatively subtract currency on the server
server.SubtractUserVirtualCurrency({
PlayFabId: currentPlayerId,
VirtualCurrency: currencyCode,
Amount: itemPrice
});
// 3. Authoritatively grant item to player's cloud inventory
var grantResult = server.GrantItemsToUser({
PlayFabId: currentPlayerId,
ItemIds: [args.ItemId],
CatalogVersion: "MainCatalog"
});
return {
success: true,
grantedItem: grantResult.ItemGrantResults[0],
remainingBalance: playerCoins - itemPrice
};
};
4.3 Invoking CloudScript from Unity C
using System;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;
namespace MasterclassCloud
{
public static class EconomyManager
{
public static void PurchaseItem(string itemId, Action<bool, string=""> onComplete)
{
Debug.Log($"[Economy] Requesting purchase of item '{itemId}' via CloudScript...");</bool,>
var request = new ExecuteCloudScriptRequest
{
FunctionName = "PurchaseItemSecure",
FunctionParameter = new { ItemId = itemId },
GeneratePlayStreamEvent = true
};
PlayFabClientAPI.ExecuteCloudScript(request, (result) =>
{
var jsonResult = result.FunctionResult as JsonObject;
bool success = jsonResult != null && (bool)jsonResult["success"];
string message = jsonResult != null ? jsonResult["message"]?.ToString() ?? "OK" : "No response";
if (success)
{
Debug.Log($"[Economy] Purchase successful for item: {itemId}");
onComplete?.Invoke(true, "Purchase complete!");
}
else
{
Debug.LogWarning($"[Economy] Purchase rejected: {message}");
onComplete?.Invoke(false, message);
}
}, (error) =>
{
Debug.LogError($"[Economy] CloudScript error: {error.GenerateErrorReport()}");
onComplete?.Invoke(false, error.ErrorMessage);
});
}
}
}
Module 5: Serverless End-of-Match Rewards & Global Leaderboards
When a multiplayer match concludes, the game host submits match statistics to PlayFab. To prevent fraudulent submissions, the serverless backend performs sanity checks.
5.1 End-of-Match CloudScript Handler
handlers.SubmitMatchResults = function (args, context) {
var matchDurationSeconds = args.Duration;
var totalKills = args.Kills;
var matchWon = args.Won;
// Sanity Check: Reject impossibly high kill rates
if (totalKills > 100 || matchDurationSeconds < 30) {
return { success: false, error: "Invalid match telemetry detected." };
}
var xpEarned = (totalKills * 50) + (matchWon ? 500 : 150);
var coinsEarned = (totalKills * 10) + (matchWon ? 100 : 25);
// 1. Authoritatively grant coins
server.AddUserVirtualCurrency({
PlayFabId: currentPlayerId,
VirtualCurrency: "GC",
Amount: coinsEarned
});
// 2. Update Global Leaderboard MMR
var mmrDelta = matchWon ? 25 : -15;
var statUpdate = server.UpdatePlayerStatistics({
PlayFabId: currentPlayerId,
Statistics: [
{ StatisticName: "PlayerMMR", Value: mmrDelta },
{ StatisticName: "TotalKills", Value: totalKills }
]
});
return {
success: true,
xpAwarded: xpEarned,
coinsAwarded: coinsEarned,
newMMRDelta: mmrDelta
};
};
Module 6: Synchronizing PlayFab Player Data with Photon Room Properties
To ensure all players in a Photon room see each other's authenticated PlayFab level, clan tag, and cosmetic skin:
using System;
using ExitGames.Client.Photon;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
namespace MasterclassCloud
{
public class PlayerProfileSynchronizer : MonoBehaviourPunCallbacks
{
public static void BroadcastPlayerProfile(string clanTag, int playerLevel, string selectedSkinId)
{
// 1. Package authenticated profile data into Photon Hashtable
ExitGames.Client.Photon.Hashtable profileProps = new ExitGames.Client.Photon.Hashtable
{
{ "Clan", clanTag },
{ "Level", playerLevel },
{ "Skin", selectedSkinId },
{ "PlayFabId", PhotonNetwork.LocalPlayer.UserId }
};
// 2. Set on LocalPlayer (Automatically broadcast to all clients in room)
PhotonNetwork.LocalPlayer.SetCustomProperties(profileProps);
Debug.Log("[Photon] Player profile custom properties broadcasted.");
}
// 3. Callback when any player updates their custom properties
public override void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps)
{
if (changedProps.ContainsKey("Skin"))
{
string newSkin = (string)changedProps["Skin"];
int level = changedProps.ContainsKey("Level") ? (int)changedProps["Level"] : 1;
string clan = changedProps.ContainsKey("Clan") ? (string)changedProps["Clan"] : string.Empty;
Debug.Log($"[Lobby] Player #{targetPlayer.ActorNumber} ({targetPlayer.NickName}) updated profile: [{clan}] Level {level}, Skin: {newSkin}");
// Update character visual mesh in scene
ApplyCharacterSkin(targetPlayer, newSkin);
}
}
private void ApplyCharacterSkin(Player player, string skinId)
{
// Locate player GameObject and swap mesh material
}
}
}
Module 7: Complete Production-Ready C# Projects
Let us build an interactive in-game Lobby and Matchmaking dashboard.
Project 1: Complete In-Game Cloud Lobby Dashboard (IMGUI)
Create UI/CloudLobbyDashboard.cs:
using System;
using UnityEngine;
using PlayFab;
using PlayFab.ClientModels;
using Photon.Pun;
namespace MasterclassCloud.UI
{
public class CloudLobbyDashboard : MonoBehaviour
{
private Rect windowRect = new Rect(30, 30, 480, 420);
private int selectedTab = 0;
private readonly string[] tabs = new string[] { "Profile", "Matchmaking", "Store", "Leaderboard" };
private int goldCoins = 0;
private string playerDisplayName = "Player";
private int playerMMR = 1250;
private void Start()
{
FetchPlayerAccountData();
}
private void FetchPlayerAccountData()
{
var request = new GetPlayerCombinedInfoRequestParams
{
GetPlayerProfile = true,
GetUserVirtualCurrency = true
};
PlayFabClientAPI.GetPlayerCombinedInfo(new GetPlayerCombinedInfoRequest { InfoRequestParameters = request }, (result) =>
{
if (result.InfoResultPayload.UserVirtualCurrency.ContainsKey("GC"))
{
goldCoins = result.InfoResultPayload.UserVirtualCurrency["GC"];
}
if (result.InfoResultPayload.PlayerProfile != null)
{
playerDisplayName = result.InfoResultPayload.PlayerProfile.DisplayName ?? "Anonymous";
}
}, null);
}
private void OnGUI()
{
GUI.backgroundColor = new Color(0.06f, 0.08f, 0.12f, 0.95f);
windowRect = GUI.Window(5555, windowRect, (GUI.WindowFunction)DrawWindow, "PLAYFAB & PHOTON ENTERPRISE LOBBY");
}
private void DrawWindow(int windowId)
{
GUILayout.Space(8);
selectedTab = GUILayout.Toolbar(selectedTab, tabs, GUILayout.Height(30));
GUILayout.Space(12);
switch (selectedTab)
{
case 0:
DrawProfileTab();
break;
case 1:
DrawMatchmakingTab();
break;
case 2:
DrawStoreTab();
break;
case 3:
DrawLeaderboardTab();
break;
}
GUI.DragWindow(new Rect(0, 0, 10000, 25));
}
private void DrawProfileTab()
{
GUILayout.Label($"Player Name: {playerDisplayName}");
GUILayout.Label($"PlayFab ID: {PlayFabPhotonAuthManager.Instance.PlayFabPlayerId}");
GUILayout.Label($"Photon Status: {PhotonNetwork.NetworkClientState}");
GUILayout.Label($"Gold Coins: {goldCoins} GC");
GUILayout.Space(10);
if (GUILayout.Button("Broadcast Profile to Room", GUILayout.Height(30)))
{
PlayerProfileSynchronizer.BroadcastPlayerProfile("UNR", 42, "Cyber_Skin_01");
}
}
private void DrawMatchmakingTab()
{
GUILayout.Label($"Current Skill Rating (MMR): {playerMMR}");
GUILayout.Label($"Queue: Ranked_1v1 (Automated Ticket Matchmaker)");
GUILayout.Space(12);
if (GUILayout.Button("Find Ranked Match", GUILayout.Height(36)))
{
PlayFabMatchmaker.Instance.StartMatchmaking(playerMMR);
}
}
private void DrawStoreTab()
{
GUILayout.Label("Available In-Game Catalog Items:");
GUILayout.Space(8);
GUILayout.BeginHorizontal("box");
GUILayout.Label("Cyber Katana Weapon [Price: 250 GC]");
if (GUILayout.Button("Buy", GUILayout.Width(80)))
{
EconomyManager.PurchaseItem("Weapon_Katana_01", (success, msg) =>
{
if (success) FetchPlayerAccountData();
});
}
GUILayout.EndHorizontal();
}
private void DrawLeaderboardTab()
{
GUILayout.Label("Global Ranked Leaderboards (MMR):");
GUILayout.Space(6);
GUILayout.Label("1. CyberWarrior_01 - 2850 MMR");
GUILayout.Label("2. QuantumHacker - 2720 MMR");
GUILayout.Label("3. NeonRider_X - 2640 MMR");
}
}
}
Module 8: Security Hardening, Anti-Spoofing & Data Integrity
8.1 Enforcing Strict Custom Authentication on Photon Dashboard
To ensure that NO client can connect to your Photon Cloud app without a valid PlayFab token:
- Open the Photon Engine Dashboard.
- Navigate to your application -> Manage -> Custom Authentication.
- Set Authentication Type to
Custom.
- Enter the PlayFab Authentication URL:
https://[YourTitleId].playfabapi.com/Photon/Authenticate
- Check Reject unauthenticated clients.
Now, if a modded client or hacker attempts to bypass PlayFab login and connect directly to Photon, the Photon Master Server will automatically reject the connection with Error Code 32753 (CustomAuthenticationFailed).
8.2 Securing PlayFab Secret Keys
- Never embed your PlayFab
DeveloperSecretKey in client-side C# code or Unity GameObjects.
- Secret Keys belong exclusively in Azure Functions, CloudScript, or secure backend game servers.
- Client SDKs should only use
PlayFabSettings.TitleId.
Module 9: Troubleshooting, Common Error Codes & FAQ
| Error Signature |
Underlying Root Cause |
Permanent Resolution |
Photon Error 32753: CustomAuthenticationFailed |
Token expired, invalid PlayFab AppId configuration, or mismatch in Title ID. |
Verify PhotonApplicationId matches your Photon Realtime AppID in PlayFab Add-ons dashboard. |
PlayFab Error: MatchmakingTicketNotFound |
Ticket expired or was already matched. |
Re-create ticket and ensure poll loop stops once state reaches Matched or Canceled. |
CloudScript Execution Error: NotAuthorized |
Client attempted to call a server-only API from CloudScript without server. prefix. |
Ensure administrative actions use the server. SDK namespace inside CloudScript. |
Photon Room Full (MaxPlayers reached) |
Matchmaker grouped more players than RoomOptions.MaxPlayers allowed. |
Set RoomOptions.MaxPlayers dynamically based on match member count. |
Quick Reference API Cheat Sheet
// 1. PLAYFAB AUTHENTICATION
PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest { CustomId = "id", CreateAccount = true }, OnSuccess, OnError);
// 2. GET PHOTON TOKEN
PlayFabClientAPI.GetPhotonAuthenticationToken(new GetPhotonAuthenticationTokenRequest { PhotonApplicationId = "app-id" }, OnTokenSuccess, OnError);
// 3. PHOTON CUSTOM AUTHENTICATION
AuthenticationValues auth = new AuthenticationValues();
auth.AuthType = CustomAuthenticationType.Custom;
auth.AddAuthParameter("username", playFabId);
auth.AddAuthParameter("token", token);
PhotonNetwork.AuthValues = auth;
PhotonNetwork.ConnectUsingSettings();
// 4. CLOUDSCRIPT EXECUTION
PlayFabClientAPI.ExecuteCloudScript(new ExecuteCloudScriptRequest { FunctionName = "MyFunc", FunctionParameter = new { Val = 10 } }, OnResult, OnError);
// 5. SYNCHRONIZING CUSTOM PROPERTIES
ExitGames.Client.Photon.Hashtable props = new ExitGames.Client.Photon.Hashtable { { "Skin", "Gold" } };
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
Conclusion & Next Steps
Integrating Photon Engine with Microsoft Azure PlayFab establishes a battle-tested, commercial-grade cloud architecture for your multiplayer games. By combining sub-millisecond real-time relay synchronization with serverless server-authoritative backend validation, your game achieves seamless cross-platform scalability, rock-solid player progression, and ironclad anti-cheat protection.
Happy coding, and build responsibly!
Authored by the UnreliableCode Engineering Team for unreliablecode.net