Photon多人游戏开发
连接与其回调
PhotonNetwork.ConnectUsingSettings(); //连接
public override void OnConnectedToMaster() {} //成功连接回调
public override void OnDisconnected(DisconnectCause cause) {} //断开连接回调
要重写回调方法,需要继承MonoBehaviourPunCallbacks
,而此类继承自MonoBehavior
连接到服务器之前可以设置游戏的版本
public class Launcher : MonoBehaviourPunCallbacks
{
private void Start()
{
PhotonNetwork.GameVersion = "0.0.1"; //设置游戏版本
PhotonNetwork.ConnectUsingSetting(); //连接至服务器
}
public override void OnConnectedToMaster()
{
Debug.Log("Connected Succeddfully!");
PhotonNetwork.JoinRandomRoom(); //加入随机房间
}
public override void OnDisconnected(DisconnectCause cause)
{
Debug.Log("Connected Failed: " + cause.ToString()); //输出错误原因
}
}
加入与创建房间及其回调
Photon提供了四个API以供我们使用
PhotonNetwork.JoinRandomRoom();
PhotonNetwork.JoinRoom("RoomName");
PhotonNetwork.CreateRoom("RoomName");
PhotonNetwork.JoinOrCreateRoom("RoomName");
JoinRandomRoom
加入随机房间,若无开放的房间,则抛出异常IMatchmakingCallbacks.OnJoinRandomFailed
JoinRoom
加入指定名称的房间,若匹配不到此房间或房间满员,则抛出异常IMatchmakingCallbacks.OnJoinRoomFailed
CreateRoom
创建指定名称的房间,创建成功后会自动加入该房间。若该房间已经存在,即名称重复,则抛出异常IMatchmakingCallbacks.OnCreateRandomFailed
JoinOrCreateRoom
加入指定名称的房间,若无此房间则以次名称创建一个新房间。若加入时该房间满员,则抛出异常IMatchmakingCallbacks.OnJoinRoomFailed
以JoinOrCreateRoom
为例,我们可以自定义房间的一些参数
RoomOptions roomOptions = new RoomOptions();
roomOptions.IsVisible = false; //将房间设为不可被“匹配”的
roomOptions.MaxPlayers = 4; //房间人数为4
roomOptions.PlayerTtl = 10000; //当某玩家失去连接10000ms后,再将其移除,在此期间该玩家可以重新连接
roomOptions.EmptyRoomTtl = 10000; //当最后一个玩家离开房间10000ms后,再销毁此房间,此期间允许新玩家加入
PhotonNetwork.JoinOrCreateRoom("RoomName", roomOptions, TypedLobby.Default);
若要获取加入或创建房间的回调,则同样需要继承MonoBehaviourPunCallbacks
//同上,以下接口均属于 IMatchmakingCallbacks
public override void OnJoinedRoom() {} //成功加入房间回调
public override void OnCreateRoom() {} //成功创建房间回调
public override void OnLeftRoom() {} //成功离开房间回调
用户均可重写对应的函数以实现自己想要的功能。再次强调,玩家创建房间成功后,会自动加入此房间,且被视为该房间的管理者,即PhotonNetwork.IsMasterClient
为true
。
远程过程调用(RPC)
一个RPC会被在同一个房间里的玩家在其对应的客户端中的相同游戏对象上被执行。
举个例子:一个房间中有迪卢克,刻晴还有公子三名角色。假设我操纵的是迪卢克,那么当我开启大招时,需要将我的行为告诉操纵刻晴和公子的玩家们,以让他们的客户端上做出反应(例如播放音效,播放动画等)。这是多个客户端之间相同游戏对象(都是迪卢克)的过程调用。另一个例子,枪战游戏中若我进行了一次换弹,那么也可以通过RPC来在其他玩家的客户端上同步我的换弹操作
public void RPC(string methodName, RpcTarget target, params object[] parameters);
public void RpcSecure(string methodName, RpcTarget target, bool encrypt, params object[] parameters); //加密
第一个参数传入调用的函数名,第二个选择RPC的接收目标,第三个为调用函数所需要的参数
RpcTarget.All; //所有人都接收
RpcTarget.Others; //除了自己以外的其他人接收
RpcTarget.MasterClient; //房主接收
以上三种发送方式皆不会被缓存到服务器上,也就是说当新玩家们加入时,他们不会接收到他们进来之前其他玩家的RPC。若要让他们能得到那些旧RPC,需要将target
设置为RpcTarget.AllBuffered
或是RpcTarget.OthersBuffered
。那么当后续有新玩家加入时,他们会按照RPC先前发送的先后顺序,依次接收到所有被缓存的RPC
RPC
与RpcSecure
都还提供了直接使用Player
替代RpcTarget
的重载,这中实现能直接指定调用的接收方,更多相关的内容日后有机会再提及,或者可以自行到官网查看
调用之前需要先获取到PhotonView
(前提是有挂载)
//PhotonView photonView = GetComponent<PhotonView>();
PhotonView photonView = PhotonView.Get(this);
photonView.RPC("RPC_SetCarryWeapon", RpcTarget.Others, GunName);
[PunRPC] //需要将函数标记为[PunRPC],且该函数不能为static
private void RPC_SetCarryWeapon(string _name) {}
注意事项
-
具有RPC方法的脚本需要与
PhotonView
组件挂载在同一个GameObject
上,而不能是它的子类或者是父类 -
通用方法不能作为PunRPC,且我们需要保证每个RPC的函数名都是唯一的(不要出现其他同名函数)
-
假设在子类中重写了父类的
RPC_SetCarryWeapon
方法,那么同样需要在子类中标记[PunRPC]
,否则调用到的仍会是父类中的实现 -
如果
[PunRPC]
函数中调用的是object[]
,那么在photonView.RPC
中需要需要进行一次as
转换photonView.RPC("MyFunc", myTarget, objectArr as object);
RaiseEvent
有很多事情并不是RPC能够解决的,同时它也需要物件上挂载PhotonView
组件且需指定一个函数以供调用。这个时候PhotonNetWork
中的静态方法RaiseEvent
可以派上用场
static bool RaiseEvent(byte eventCode, object eventContent, RaiseEventOptions reOpt, SendOptions sendOpt)
-
eventCode
:给事件一个唯一的标识符,可以类比成给每个学生一个唯一的学号,以供区分不同类型的事件。可供选择的区间为[1-199],而[200-256]为提供给Pun内部使用的编号,用户无法操作 -
eventContent
:用户要传递的信息,可以传任何Pun能够序列化的内容(例如string
,int
,float
...,推荐使用以byte
作为键值的HashTable
)。有时为了能传递更多的信息也可以使用object[]
若想了解更多Pun序列化的内容,可以点击查看Serialization in Photon
-
reOpt
:RaiseEventOptions
类中包含了,InterestGroup
,TargetActors
...各种属性,其中常用的一项为Receivers
//与RpcTarget用法类似 ReceiverGroup.All; ReceiverGroup.Others; ReceriverGroup.MasterClient;
若想了解更多
RaiseEventOptions
相关的API,可以点击查看RaiseEventOptions Class Reference -
sendOpt
:选择本消息的发送为是reliable(可靠的/可信的)还是unreliable,换句话说也就是决定本次发送需不需要加密,道理和RpcSecure
类似SendOptions sendOptions = SendOptions.SendReliable; //可信的 SendOptions sendOptions = SendOptions.SendUnreliable; //不可信的,加密
-
bool
:发送成功返回true
,发送失败返回false
大致的调用方式如下图,可供读者参考
//使用各种变量创建raycastHits
Dictionary<byte, object> raycastHits = new Dictionary<byte, object>
{
{ 0, raycastHit.point },
{ 1, raycastHit.normal },
{ 2, raycastHit.collider.tag },
{ 3, BulletTransform.forward }
};
//所有玩家都可以接收
RaiseEventOptions raiseEventOptions = new RaiseEventOptions() { Receivers = ReceiverGroup.All };
//可信的
SendOptions sendOptions = SendOptions.SendReliable;
//发送
PhotonNetwork.RaiseEvent((byte)EventCodes.HitGround, raycastHits, raiseEventOptions, sendOptions);
成功发送之后,有两种方式去接收我们发送的消息。
第一种是继承IOnEventCallBack
接口,然后实现OnEvent
回调
public void OnEvent(EventData photonEvent) {}
使用photonEvent.Code
来获取标识符,判断是否此次发送的消息为我们想接收处理的,再用photonEvent.CustomData
来获取到此次消息中携带的对象数据
使用该方法需要在脚本的OnEnable
和OnDisable
中去注册和取消注册回调(基本上继承IXXCallBack
接口的都得这么干)
private void OnEnable() { PhotonNetwork.AddCallbackTarget(this); }
private void OnDisable() { PhotonNetwork.RemoveCallbackTarget(this); }
第二种方法,不需要去继承接口,只需要自行写一个接收函数(名称随意),然后在事件中对其进行增加和删除就行
private void OnEnable() { PhotonNetwork.NetworkingClient.EventReceived += MyEvent; }
private void OnDisable() { PhotonNetwork.NetworkingClient.EventReceived -= MyEvent; }
private void MyEvent(EventData photonEvent) {}
两种方法等效,根据喜好进行选择
一些其他常用的API
Bool判断相关
PhotonNetwork.IsConnected
用来判断是否连接到服务器,成功连接即为true。
当用户初次进入游戏时,
PhotonNetwork.IsConnected
为false,而当用户PhotonNetwork.ConnectUsingSetting()
后,为true
PhotonNetwork.InRoom
用来判断当前用户是否在房间中,在则为true
Is true while being in a room.
PhotonNetwork.InRoom
实际上会返回这样一个值(NetworkClientState == ClientState.Joined)。在在线游戏中,若当玩家在房间中掉线了,返回的结果即变为falseIn offline mode, you can be in a room too and NetworkClientState then returns Joined like on online mode!
PhotonNetwork.InLobby
用来判断当前用户是否在大厅中,在则为true
True while this client is in a lobby.
PhotonNetwork.IsMasterClient
用来判断用户是否为房主,是则为true
Are we the master client?
函数相关
PhotonNetwork.Disconnect()
用于主动断开连接,断开后的回调void OnDisconnected(DisconnectCause cause)
,上文中有提及
PhotonNetwork.Reconnect()
用于重新连接,重连后的回调void OnConnectedToMaster()
,上文中有提及
PhotonNerwork.RejoinRoom("RoomName")
用于重新加入房间
PhotonNetwork.ReconnectAndRejoin()
使用实例
创建MasterManager单例类,用于管理GameSettings等信息
//SingletonScriptableObject.cs
public abstract class SingletonScriptableObject<T> : ScriptableObject where T : ScriptableObject
{
private static T _instance = null;
public static T Instance
{
get
{
T[] _instances = Resources.FindObjectsOfTypeAll<T>();
if (_instances.Length == 1)
_instance = _instances[0];
else
{
Debug.Log("Wrong Singleton!");
return null;
}
return _instance;
}
}
}
//MasterManager.cs
[CreateAssetMenu(menuName = "Singleton/MasterManager")]
public class MasterManager : SingletonScriptableObject<MasterManager>
{
[SerializeField] private GameSettings gameSettings;
public static GameSettings GameSetting { get { return Instance.gameSettings; } }
}
//GameSettings.cs
[CreateAssetMenu(menuName = "Manager/GameSettings")]
public class GameSettings : ScriptableObject
{
[SerializeField] private string gameVersion;
public string GameVersion { get { return gameVersion; } }
[SerializeField] private string nickName;
public string NickName { get { return nickName + Random.Range(1, 100).ToString(); } }
}
至此,我们可以在文件夹中创建出MasterManager与GameSetting
在Launch.cs中,在设置游戏信息时可以直接调用GameSetting中的信息
public class Launcher : MonoBehaviourPunCallbacks
{
private void Start()
{
PhotonNetwork.GameVersion = MasterManager.GameSetting.GameVersion; //设置游戏版本
PhotonNetwork.NickName = MasterManager.GameSetting.NickName; //设置玩家ID
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
Debug.Log(PhotonNetwork.LocalPlayer.NickName); //连接成功输出玩家ID
}
}
在成功连接至服务器后,玩家可根据自定义的名称创建房间,并显示房间列表
需要InputField
,Button
以及ScrollView
组件以构成简单的UI界面。将CreateRoom脚本挂载在Botton
上,接收InputField
传进的text
,并调用PhotonNetwork.CreateRoom
方法创建房间。
在玩家创建一个新房间之后,我们可以在回调函数中书写逻辑,以在ScrollView
中显示该房间的信息
public override void OnRoomListUpdate(List<RoomInfo> roomList) {}
需要注意的是,若要获得房间更新的回调,玩家需要先加入大厅,即PhotonNetwork.JoinLobby
,且成功创建房间的玩家并不会获得此回调(创建房间的玩家会自动加入该房间)
CreateRoom.cs的实现较为简单,可以参考上文,这里不予列出
//RoomListManager.cs
public class RoomListManager : MonoBehaviourPunCallbacks
{
[SerializeField] private RoomList _roomList;
[SerializeField] private Transform content;
private List<RoomList> roomLists;
private void Awake()
{
roomLists = new List<RoomList>();
}
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
foreach (RoomInfo _roomInfo in roomList)
{
if (_roomInfo.RemovedFromList) { //房间被销毁后将从列表中移除
int index = roomLists.FindIndex(x => x._roomInfo.Name == _roomInfo.Name);
if (index != -1) {
Destroy(roomLists[index].gameObject);
roomLists.RemoveAt(index);
}
}
else {
var _roomListInfoSetting = Instantiate(_roomList, content);
if (_roomListInfoSetting != null)
_roomListInfoSetting.SetRoomInfo(_roomInfo);
roomLists.Add(_roomListInfoSetting);
}
}
}
}
//RoomList
public class RoomList : MonoBehaviour
{
[SerializeField] private Text text;
public RoomInfo _roomInfo;
public void SetRoomInfo(RoomInfo roomInfo)
{
text.text = roomInfo.Name + "
房间容量:" + roomInfo.MaxPlayers.ToString() +
",当前玩家数:" + roomInfo.PlayerCount.ToString();
_roomInfo = roomInfo;
}
}
玩家实例
简单的来说,若已经连接到服务器,且成功的加入了一个房间,我们便可以联网创建出玩家实例以进行游戏了。与本地实例化一个GameObject不同,Photon有自己的实例化方法
static Object Instantiate(Object original, Vector3 position, Quaternion rotation);
static GameObject PhotonNetwork.Instantiate(string prefabName, Vector3 position, Quaternion rotation);
我们的预制件需要存放在Resources文件夹中,以供PhotonNetwork.Instantiate
去查找并实例化
与Unity中的Instantiate
不同,我们需要在预制件上添加PhotonView
组件
Photon Unity Network 接口及类索引
Photon RealTime C# SDK callbacks
interface | 内容 | 实例 |
---|---|---|
IConnectionCallbacks |
连接相关回调 | 连接,上线,断线... |
IMatchmakingCallbacks |
组织相关回调 | 创建,加入,离开房间... |
IInRoomCallbacks |
房间内事件相关回调 | 玩家加入,退出房间... |
ILobbyCallbacks |
大厅内事件相关回调 | 加入,退出大厅,玩家列表更新... |
IOnEventCallback |
接收事件回调 | void OnEvent(EventData photonEvent) |
IWebRpcCallback |
不会 | 不会 |
Pun2 specific General callbacks
interface | 内容 | 实例 |
---|---|---|
IPunOwnershipCallback |
拥有者变更回调 | 不会 |
IPunInstantiateMagicCallback |
NetObj创建回调 | void OnPhotonInstantiate(PhotonMessageInfo info) |
Pun2 specific PhotonView callbacks
interface | 实例 |
---|---|
IOnPhotonViewPreNetDestroy |
void OnPreNetDestroy(PhotonView rootView) |
IPunObservable |
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) |
IOnPhotonViewControllerChange |
void OnControllerChange(Player newController, Player preController |
IOnPhotonViewPreNetDestroy
是NetObj将要执行Destroy()
时的回调。
IPunObservable
Pun每秒会call这个回调数次,类似于Update()
,以供用户写入和读取PhotonView
的同步数据
IOnPhotonViewControllerChange
当PhotonView
的所有者发生变化时的回调
上述Photon RealTime C# SDK callbacks与Pun2 specific General callbacks中除了IPunInstantiateMagicCallback
外,都需要在进行注册与注销回调的操作。通常可以在Awake()
,Start()
,OnEnable()
,OnDisable()
,OnDestroy()
中实现
//读者可以根据情况选择在哪个方法中去实现,这里举OnEnable与OnDisable的例
private void OnEnable() { PhotonNetwork.AddCallbackTarget(this); }
private void OnDisable() { PhotonNetwork.RemoveCallbackTarget(this); }
而Pun2 specific PhotonView callbacks中,除了IPunObservable
外,其他接口也需要进行注册与注销的操作,但方法略有差别
private void OnEnable() { photonView.AddCallbackTarget(this); }
private void OnDisable() { phtonView.RemoveCallbackTarget(this); }
“继承你想要的接口并实现它”。除了接口以外,还可以通过继承MonoBehaviourPunCallbacks
来实现PUN2回调
MonoBehaviorPunCallbacks继承的回调 |
---|
IConnectionCallbacks |
IMatchmakingCallbacks |
IInRoomCallbacks |
ILobbyCallbacks |
IWebRpcCallback |
直接继承接口与继承类有以下几点不同
-
接口能继承多个而类一次只能继承一个
-
类中只包含了五个接口,其余的仍需自行额外继承
-
继承接口后编译器要求强制实现,不实现或是函数名字拼错则会给出ERROR。而继承可以是自行选择是否实现其功能,若需要,
override
即可 -
继承类后,如果要使用
OnEnable
和OnDisable
则需要override
,同时还得给出基类的实现public override OnEnable() { base.OnEnable(); }
除了以上五个接口外,MonoBehaviorPunCallbacks
还继承了类MonoBehaviorPun
简单的来说,MonoBehaviorPun
中有一个PhotonView
组件,当我们继承MonoBehaviorPunCallbacks
,且该脚本挂载的物体上有PhotonView
组件,那么MonoBehaviorPun
类会帮我们自动获取到此组件,并命名为photonView
。而如果挂载的物体上没有PhotonView
组件,那么photonView
会是null
,使用时需要多加注意
最后附上几篇官方文档