这里是个人学习photon的一些总结,以及学习photon官方文档和对其的部分翻译和整理,都是些个人觉得基础和常用的部分,有什么错误谢谢指出~
pun2官方文档链接https://doc.photonengine.com/zh-cn/pun/current/getting-started/pun-intro
文章目录
连接和鉴权
区域
Photon提供全球范围内的低延迟游戏,client会初始化连接到photon的name server服务器上以获得一个可用的region列表,每个region彼此完全分离,一个region由若干MasterServer 【用于匹配】和 GameServers组成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SsKVlFCl-1645156427976)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
-
RealTimeApi能够筛选最佳的region并提供连接。当client每次连接时会从Name Server处获取可用区域列表,这用于获取最新区域列表时检查最佳区域是否有变化,在ping通一个servers后,结果会以字符串的形式返回,并进行存储,这个字符串包括当前最佳区域、ping值、当前可用区域列表。当没有旧区域列表结果字符串时会去ping所有region,此操作十分耗时。如果有之前的字符串记录,则会去检查:a.是否区域列表发生变化 b.某个区域ping值是否已无法接受(大于以前ping值的1.5倍);如果这两个条件任一满足,则会去ping所有region并保存一个新结果。可以结合区域筛选器去改变玩家获取的区域列表。
-
PUN会使用unity的playerprefs去自动保存最佳区域的信息,并在下次连接时去再次ping这个region。 为了方便和调试,当前的“最佳区域”及其 ping 显示在 Unity 编辑器的 PhotonServerSettings 中。但是,这仅对 Unity Editor 的播放模式有效。
PhotonNetwork.ConnectUsingSettings() //使用时默认连接最佳区域
- 最佳区域 这个选项不是确定的,有时可能因为相同或者差不多ping值导致随机。可能出现以下情况:1.一个设备对多个region具有相同的ping 2.连接到同一网络上的不同设备对同一region具有不同ping值;从而导致随机。可以通过在线区域白名单去筛选,或者显式选择region
- 从 PUN v2.17 开始,添加了一个名为“Dev Region”的新功能。这是 PhotonServerSettings 中提供的新设置。使用此设置,所有开发版本都将使用相同的区域,避免“最佳区域”选择的初始匹配问题。创建 PhotonServerSettings 并在 Unity 编辑器的第一次运行 (PlayMode) 期间设置“开发区域”时,会自动启用“ Development build”。当您使用连接PhotonNetwork.ConnectUsingSettings()时,“Dev Region”仅在 Unity 编辑器和“ Development”构建中使用。还可以通过简单地删除值来禁用 Unity Editor 和“Development Build”中的“Dev Region”。因此,为避免在开发阶段出现最佳区域选择问题,请确保更新到最新的 PUN 2 版本。在 Unity Editor 中运行一次(进入 PlayMode 并连接)。Unity Editor 的第一个连接将设置“Dev Region”,这可以从 PhotonServerSettings 检查器中看到。 这样,所有客户端(Unity Editor 和builds)都将连接到同一个“开发区域”。
- 每个photon region都有一个令牌,为了使用PhotonNetwork.ConnectToRegion连接到指定region,需要手动设置中的AppId和App Versions等配置(PhotonServerSettings在这种情况下不生效)
PhotonNetwork.ConnectToRegion
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZ2lbexs-1645156427977)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
Region | Hosted in | Token |
---|---|---|
Asia | Singapore | asia |
Australia | Melbourne | au |
Canada, East | Montreal | cae |
Chinese Mainland1 【需要单独申请,要给photon发送邮件解锁appid 】 | Shanghai | cn |
Europe | Amsterdam | eu |
India | Chennai | in |
Japan | Tokyo | jp |
Russia | Moscow | ru |
Russia, East | Khabarovsk | rue |
South Africa | Johannesburg | za |
South America | Sao Paulo | sa |
South Korea | Seoul | kr |
Turkey | Istanbul | tr |
USA, East | Washington D.C. | us |
USA, West | San José | usw |
- 可以为每个app设置各自的region filter,dashboard-》Manage-》Edit
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3GAxNMNv-1645156427978)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
- 中国区Setting设置,可以选择使用PhotonServerSettings或者Code
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wphjrYfG-1645156427978)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
void ConnectToChina(){
AppSettings chinaSettings = new AppSettings();
chinaSettings.UseNameServer = true;
chinaSettings.ServerAddress = "ns.photonengine.cn";
chinaSettings.AppIdRealtime = "ChinaPUNAppId"; // TODO: replace with your own PUN AppId unlocked for China region
chinaSettings.AppVersion = "ChinaAppVersion"; // optional
PhotonNetwork.ConnectUsingSettings(chinaSettings);
}
TCP和UDP
根据 API 的不同,连接到 Photon Cloud 可能需要根据应用程序的协议和目的传递适当的端口。在大多数情况下,不必担心端口号。下表包含每个协议和每个服务器连接要与 Photon Cloud 一起使用的默认端口。
Port Number | Protocol | Purpose |
---|---|---|
5058 or 27000 | UDP | Client to Nameserver (UDP) |
5055 or 27001 | UDP | Client to Master Server (UDP) |
5056 or 27002 | UDP | Client to Game Server (UDP) |
4533 | TCP | Client to Nameserver (TCP) |
4530 | TCP | Client to Master Server (TCP) |
4531 | TCP | Client to Game Server (TCP) |
9090 | TCP | Client to Master Server (WebSockets) |
9091 | TCP | Client to Game Server (WebSockets) |
9093 | TCP | Client to Nameserver (WebSockets) |
19090 | TCP | Client to Master Server (Secure WebSockets) |
19091 | TCP | Client to Game Server (Secure WebSockets) |
19093 | TCP | Client to Nameserver (Secure WebSockets) |
鉴权
STEAM
转到DashBoard Multiplayer Game Development Made Easy | Photon Engine DashBoard -》MANAGE-》Authentication-》STEAM
-
apiKeySecret : Steam 发布者 Web API 密钥。
-
appid : Steam 游戏的 ID。
-
verifyOwnership: 验证用户是否真正拥有(购买了游戏并将其保存在他的库中)该游戏
-
verifyVacBan、 **verifyPubBan:**是否验证用户被ban
-
客户端必须使用Valve’s Steamworks API获取会话ticket用以证明是有效的Steam用户
-
在项目中支持SteamWork.NET Steamworks.NET - Installation
-
获取ticket以及发送
//获取session ticket并转换成UTF8
// hAuthTicket should be saved so you can use it to cancel the ticket as soon as you are done with itpublic string GetSteamAuthTicket(out HAuthTicket hAuthTicket){
byte[] ticketByteArray = new byte[1024];
uint ticketSize;
hAuthTicket = SteamUser.GetAuthSessionTicket(ticketByteArray, ticketByteArray.Length, out ticketSize);
System.Array.Resize(ref ticketByteArray, (int)ticketSize);
StringBuilder sb = new StringBuilder();
for(int i=0; i < ticketSize; i++)
{
sb.AppendFormat("{0:x2}", ticketByteArray[i]);
}
return sb.ToString();}
//发送ticket进行鉴权
PhotonNetwork.AuthValues = new AuthenticationValues();
PhotonNetwork.AuthValues.UserId = SteamUser.GetSteamID().ToString();
PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Steam;
PhotonNetwork.AuthValues.AddAuthParameter("ticket", SteamAuthSessionTicket);// connect
- 在鉴权完毕后建议撤销ticket,建议在IConnectionCallbacks.OnConnected、IConnectionCallbacks.OnConnectedToMaster、IConnectionCallbacks.OnConnectedToMaster、IConnectionCallbacks.OnDisconnected任一中进行
private void OnConnected(){
SteamUser.CancelAuthTicket(hAuthTicket);}
- 完整代码
using Photon.Pun;using Photon.Realtime;using Steamworks;
public class MinimalSteamworksPunAuth : MonoBehaviourPunCallbacks{
private HAuthTicket hAuthTicket;
private void Start()
{
if (SteamManager.Initialized)
{
Connect();
}
}
public void Connect()
{
string SteamAuthSessionTicket = GetSteamAuthTicket(out hAuthTicket);
PhotonNetwork.AuthValues = new AuthenticationValues();
PhotonNetwork.AuthValues.UserId = SteamUser.GetSteamID().ToString();
PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Steam;
PhotonNetwork.AuthValues.AddAuthParameter("ticket", SteamAuthSessionTicket);
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
CancelAuthTicket(hAuthTicket);
}
public override void OnCustomAuthenticationFailed(string errorMessage)
{
CancelAuthTicket(hAuthTicket);
}
// ticket should be saved so you can use it to cancel the ticket as soon as you are done with it
private string GetSteamAuthTicket(out HAuthTicket ticket)
{
byte[] ticketByteArray = new byte[1024];
uint ticketSize;
ticket = SteamUser.GetAuthSessionTicket(ticketByteArray, ticketByteArray.Length, out ticketSize);
System.Array.Resize(ref ticketByteArray, (int)ticketSize);
StringBuilder sb = new StringBuilder();
for(int i=0; i < ticketSize; i++)
{
sb.AppendFormat("{0:x2}", ticketByteArray[i]);
}
return sb.ToString();
}
private void CancelAuthTicket(HAuthTicket ticket)
{
if (ticket != null)
{
SteamUser.CancelAuthTicket(ticket);
}
}}
- 使用 Facepunch.Steamworks
using Photon.Pun;using Photon.Realtime;using Steamworks;using UnityEngine;
public class MinimalSteamworksPunAuth : MonoBehaviourPunCallbacks{
[SerializeField]
private int steamAppId;
private AuthTicket hAuthTicket;
private void Awake()
{
try
{
SteamClient.Init(steamAppId);
}
catch (System.Exception e)
{
// Couldn't init for some reason - it's one of these:
// Steam is closed?
// Can't find steam_api dll?
// Don't have permission to play app?
}
}
private void Start()
{
Connect();
}
public void Connect()
{
string steamAuthSessionTicket = GetSteamAuthTicket(out hAuthTicket);
PhotonNetwork.AuthValues = new AuthenticationValues();
PhotonNetwork.AuthValues.UserId = SteamClient.SteamId.ToString();
PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Steam;
PhotonNetwork.AuthValues.AddAuthParameter("ticket", steamAuthSessionTicket);
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
CancelAuthTicket(hAuthTicket);
}
public override void OnCustomAuthenticationFailed(string errorMessage)
{
CancelAuthTicket(hAuthTicket);
}
// ticket should be saved so you can use it to cancel the ticket as soon as you are done with it
private string GetSteamAuthTicket(out AuthTicket ticket)
{
ticket = SteamUser.GetAuthSessionTicket();
StringBuilder ticketString = new StringBuilder();
for (int i = 0; i < ticket.Data.Length; i++)
{
ticketString.AppendFormat("{0:x2}", ticket.Data[i]);
}
return ticketString.ToString();
}
private void CancelAuthTicket(AuthTicket ticket)
{
if (ticket != null)
{
ticket.Cancel();
}
}
private void Update()
{
SteamClient.RunCallbacks();
}
private void OnApplicationQuit()
{
SteamClient.Shutdown();
}}
OCULUS
转到DashBoard Multiplayer Game Development Made Easy | Photon Engine DashBoard -》MANAGE-》Authentication-》OCULUS
Oculus通过Oculus ID和客户端生成的nonce值去验证身份,nonce是一个只能使用一次的随机数字。 客户端需要登录 Oculus 然后生成一个随机数。此 nonce 证明客户端是有效的 Oculus 用户。
下载 Oculus Platform SDK for Unity 并将其导入您的项目。从编辑器的菜单栏中,转到“Oculus 平台”->“编辑设置”并输入Oculus AppId。使用以下代码获取登录用户的 Oculus ID 并生成随机数:
using UnityEngine;using Oculus.Platform;using Oculus.Platform.Models;
public class OculusAuth : MonoBehaviour{
private string oculusId;
private void Start()
{
Core.AsyncInitialize().OnComplete(OnInitializationCallback);
}
private void OnInitializationCallback(Message<PlatformInitialize> msg)
{
if (msg.IsError)
{
Debug.LogErrorFormat("Oculus: Error during initialization. Error Message: {0}",
msg.GetError().Message);
}
else
{
Entitlements.IsUserEntitledToApplication().OnComplete(OnIsEntitledCallback);
}
}
private void OnIsEntitledCallback(Message msg)
{
if (msg.IsError)
{
Debug.LogErrorFormat("Oculus: Error verifying the user is entitled to the application. Error Message: {0}",
msg.GetError().Message);
}
else
{
GetLoggedInUser();
}
}
private void GetLoggedInUser()
{
Users.GetLoggedInUser().OnComplete(OnLoggedInUserCallback);
}
private void OnLoggedInUserCallback(Message<User> msg)
{
if (msg.IsError)
{
Debug.LogErrorFormat("Oculus: Error getting logged in user. Error Message: {0}",
msg.GetError().Message);
}
else
{
oculusId = msg.Data.ID.ToString(); // do not use msg.Data.OculusID;
GetUserProof();
}
}
private void GetUserProof()
{
Users.GetUserProof().OnComplete(OnUserProofCallback);
}
private void OnUserProofCallback(Message<UserProof> msg)
{
if (msg.IsError)
{
Debug.LogErrorFormat("Oculus: Error getting user proof. Error Message: {0}",
msg.GetError().Message);
}
else
{
string oculusNonce = msg.Data.Value;
// Photon Authentication can be done here
}
}}
客户端需要发送 Oculus ID 和生成的 nonce 作为查询字符串参数,分别带有“userid”和“nonce”键:
PhotonNetwork.AuthValues = new AuthenticationValues();
PhotonNetwork.AuthValues.UserId = oculusId;
PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Oculus;
PhotonNetwork.AuthValues.AddAuthParameter("userid", oculusId);
PhotonNetwork.AuthValues.AddAuthParameter("nonce", oculusNonce);// do not set AuthValues.Token or authentication will fail// connect
大厅和房间
用户id以及好友
-
在 Photon 中,玩家使用唯一的 UserID 来识别。UserID区分大小写。具有相同 UserID 的 Photon 客户端可以连接到同一台服务器,但不能从使用相同 UserID 的两个单独客户端加入同一个 Photon 房间。房间内的每个actor都应该有一个唯一的用户 ID。
-
使用唯一UID的优点:1.可以跨设备保存数据,以及断线重连等;2.便于构建社交系统;3.可以使用另一服务的id(Facebook等)绑定到PhotonUserID上;4.可以通过保留UserID黑名单并使用身份验证来禁止恶意用户的连接;
-
在验证后,photon client会持有相同的UID直到disconnected;可以通过以下三个方式进行UID设置:1.在client connecting之前使用AuthenticationValues.UserId去设置;2.或者在验证成功后会返回一个UID;3.匿名用户将返回被分配一个随机GUID
-
玩家可以在房间内相互分享他们的 UserID。在 C# SDK 中,要启用此功能并使 UserID 对所有人可见,请在创建房间时设置RoomOptions.PublishUserId为true然后,服务器将在每次新加入时广播此信息,可以使用 Player.UserId 访问每个玩家的用户 ID。
-
使用 expectedUsers参数为好友保留房间空位slot( Slot Reservation),可以通过 Room.ExpectedUsers去调整房间的预期玩家列表,要启用Slot Reservation需要开启 房间内UID分享
// create room example
PhotonNetwork.CreateRoom(roomName, roomOptions, typedLobby, expectedUsers);// join room example
PhotonNetwork.JoinRoom(roomName, expectedUsers);// join or create room example
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, typedLobby, expectedUsers);// join random room example
PhotonNetwork.JoinRandomRoom(expectedProperties, maxPlayers, expectedUsers, matchmakingType, typedLobby, sqlLobbyFilter, expectedUsers);
- 可以用以下实例进行一个队伍的匹配,队伍的房主进行实际匹配,并为所有成员预留位置
//房主
//尝试找到一个随机房间
PhotonNetwork.JoinRandomRoom(expectedProperties, maxPlayers, expectedUsers, matchmakingType, typedLobby, sqlLobbyFilter, expectedUsers);
//如果没有找到,则创建一个新的
PhotonNetwork.CreateRoom(roomName, roomOptions, typedLobby, teamMembersUserIds);
//成员
//成员不必进行任何匹配,而是轮询房主的房间名字
roomNameWhereTheLeaderIs = PhotonNetwork.FindFriends(new string[1]{
leaderUserId });
//当房主加入一个房间后成员也加入
PhotonNetwork.JoinRoom(roomNameWhereTheLeaderIs);
- 好友也通过UID判别,只有连接同一个 AppID、同一个 Photon Cloud 区域、同一个 Photon AppVersion 的朋友,无论使用什么设备或平台,都能找到彼此。通过以下代码更新好友列表。 Photon 不会保留朋友列表。 游戏中的任何非现有用户都将被视为离线。只有当他/她在进行 FindFriends 查询并 连接到 Photon 时,他/她才被认为是在线的。如果用户在线并加入同名房间,则每个用户将返回一个房间名称。
using System.Collections.Generic;using UnityEngine;using Photon.Realtime;using Photon.Pun;
public class FindFriendsExample : MonoBehaviourPunCallbacks{
public bool FindFriends(string[] friendsUserIds)
{
return PhotonNetwork.FindFriends(friendsUserIds);
}
public override void OnFriendListUpdate(List<FriendInfo> friendsInfo)
{
for(int i=0; i < friendsInfo.Count; i++)
{
FriendInfo friend = friendsInfo[i];
Debug.LogFormat("{0}", friend);
}
}}
匹配指导
使用 Photon 进入房间与某人一起玩(或对抗)非常容易。基本上有三种方法:告诉服务器找到匹配的房间,跟随朋友进入她的房间,或者获取房间列表让用户选择一个。对于大多数游戏来说,最好使用快速简单的匹配,建议使用随机匹配,并可能使用技能、等级等过滤器。
常见问题:
- 验证
AppId
在所有客户端中使用相同。 - 验证客户端是否连接到相同的
Region
. 无论他们使用什么设备或平台,只有连接到同一区域的玩家才能互相玩。 - 确认所有客户端中使用相同的AppVersion。
- 验证玩家是否有不同的唯一用户UID。具有相同 UserID 的玩家不能加入同一个房间。
- 在尝试按名称加入房间之前,请确保已创建此房间。或者使用
JoinOrCreateRoom
. - 如果您尝试加入随机房间,请确保选择创建时使用的相同大厅(名称和类型)。
- 如果您使用房间属性作为过滤器条件进行随机匹配,请确保在创建房间时将这些属性的键设置为从大厅可见。
- 如果您使用 SQL 过滤器进行随机匹配,请确保将保留的过滤属性键设置为从大厅可见。每次随机匹配尝试时放松 SQL 过滤器或使用链式过滤器或在多次尝试失败后的某个时间点创建新房间也很重要。
- 如果您正在实现异步匹配,请确保使用配置正确的 webhook(启用“AsyncJoin”)或使用 AsyncRandomLobby。
快速匹配
只需调用JoinRandomOrCreateRoom
如果找到一个房间,它将被加入,否则将创建一个新房间。
using Photon.Pun;
using Photon.Realtime;
public class QuickMatchExample : MonoBehaviourPunCallbacks
{
private void QuickMatch()
{
PhotonNetwork.JoinRandomOrCreateRoom();
}
public override void OnJoinedRoom()
{
// joined a room successfully
}
}
如果JoinRandomRoom
不能立即找到房间,则创建一个。如果从不显示房间名称(以及为什么要显示),请不要自定义名字。让服务器执行此操作。创建房间时将空字符串设置为“房间名称”。房间有一个独特的 GUID。为“最大玩家数”应用一个值。这样,当房间已满时,服务器最终会停止添加玩家。开始游戏后为阻止新玩家加入,请关闭房间,服务器会停止玩家加入。
using Photon.Pun;
using Photon.Realtime;
public class QuickMatchExample : MonoBehaviourPunCallbacks
{
[SerializeField]
private maxPlayers = 4;
private void CreateRoom()
{
RoomOptions roomOptions =