Photon with Unity 随笔

Intro

Photon Unity Networking (PUN) is a Unity package for multiplayer games. Flexible matchmaking gets your players into rooms where objects can be synced over the network. RPCs, Custom Properties or “low level” Photon events are just some of the features. The fast and (optionally) reliable communication is done through dedicated Photon server(s), so clients don’t need to connect one to one.

相关代码可

Refer to the link below to view in the Asset Store (PUN2)

PUN 2 - FREE | 网络 | Unity Asset Store

This note is based on PUN2 Version 2.41 - August 2, 2022

Dev Region

一般的,如果配置中空缺此项,游戏会自动连接延迟低的服务器,可能会导致无法观察到彼此的房间。推荐将Dev Region设置为kr或者jp

General Functions

Common Use Variables

PhotonNetwork.CurrentRoom记录了当前连接的房间的信息

PhotonNetwork.PlayerList记录了连接到当前房间的玩家信息列表

PhotonNetwork.IsMasterClient当前客户端是房主

PhotonNetwork.NickName本地玩家的昵称

General Process

导入Photon的包后,可以使用PUN Wizard完成初始化,需要有自己的AppID。稍后可以通过修改Assets/Photon/PhotonUnityNetworking/Resources/PhotonServerSettings.asset路径下的配置文件进行配置。

一般的,连接的级别大致分为Server(Master)-Lobby-Room

下列代码给出了连接服务器、创建、加入房间的代码流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//MonoBehaviourPunCallbacks

PhotonNetwork.ConnectUsingSettings();
public override void OnConnectedToMaster(){}

PhotonNetwork.CreateRoom(roomNameInputField.text);
public override void OnCreatedRoom(){}
public override void OnCreateRoomFailed(short returnCode, string message){}

PhotonNetwork.JoinRoom(info.Name);
public override void OnJoinedRoom(){}

PhotonNetwork.LeaveRoom();

public override void OnRoomListUpdate(List<RoomInfo> roomList){}
public override void OnMasterClientSwitched(Player newMasterClient){}
public override void OnPlayerEnteredRoom(Player newPlayer){}

下面给出了实际的代码应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
public class Launcher : MonoBehaviourPunCallbacks
{
void Start()
{
//通过配置连接到服务器
PhotonNetwork.ConnectUsingSettings();
}

public void CreateRoom()
{
//创建房间,需要指定房间名
if (string.IsNullOrEmpty(roomNameInputField.text))
{
return;
}
PhotonNetwork.CreateRoom(roomNameInputField.text);
}

//加入房间,以房间名为参数
public void JoinRoom(RoomInfo info)
{
PhotonNetwork.JoinRoom(info.Name);
MenuManager.Instance.OpenMenu("Loading");
}

//退出房间
public void LeaveRoom()
{
PhotonNetwork.LeaveRoom();
}

//重要的回调方法:
//成功连接到服务器后调用
public override void OnConnectedToMaster()
{
Debug.Log("[Network]Connected to Master");
PhotonNetwork.JoinLobby();

//同步场景
PhotonNetwork.AutomaticallySyncScene = true;
}

//本地客户端连接到房间时调用
//PhotonNet
public override void OnJoinedRoom()
{
roomNamePUN.text = PhotonNetwork.CurrentRoom.Name;

Player[] players = PhotonNetwork.PlayerList;

foreach (Transform trans in playerListContent)
{
Destroy(trans.gameObject);
}

for (int i = 0; i < players.Count(); i++)
{
Instantiate(playerListItemPrefab, playerListContent).GetComponent<PlayerListItem>().SetUp(players[i]);
}

//使得按钮只对创建客户端可见
startGameBtn.SetActive(PhotonNetwork.IsMasterClient);
}

public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
//PhotonNetwork.countOfRooms != PhotonNetwork.GetRoomList();
Debug.Log("[Network]Room List is Updated: " + roomList.Count() + " in total");

//先清理已有的房间列表
foreach (Transform item in roomListContent)
{
Destroy(item.gameObject);
}


for (int i = 0; i < roomList.Count(); i++)
{
if (roomList[i].RemovedFromList)
continue;
Instantiate(roomListItemPrefab, roomListContent).GetComponent<RoomListItem>().SetUp(roomList[i]);
}
}

//当房主发生了变化
public override void OnMasterClientSwitched(Player newMasterClient)
{
startGameBtn.SetActive(PhotonNetwork.IsMasterClient);
}

//离开房间时调用
public override void OnLeftRoom()
{

}

//房间消息列表更新时调用(如创建了新房,已有的房间关闭)
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
//PhotonNetwork.countOfRooms != PhotonNetwork.GetRoomList();
Debug.Log("[Network]Room List is Updated: " + roomList.Count() + " in total");

//先清理已有的房间列表
foreach (Transform item in roomListContent)
{
Destroy(item.gameObject);
}


for (int i = 0; i < roomList.Count(); i++)
{
if (roomList[i].RemovedFromList)
continue;
Instantiate(roomListItemPrefab, roomListContent).GetComponent<RoomListItem>().SetUp(roomList[i]);
}
}

//当一个远程玩家进入房间时调用
public override void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.Log("[Network]" + newPlayer.NickName + " connected, in room: " + PhotonNetwork.NickName);
Instantiate(playerListItemPrefab,playerListContent).GetComponent<PlayerListItem>().SetUp(newPlayer);
}
}

当玩家都进入同一个房间时,可以考虑使用来同步场景

1
PhotonNetwork.LoadLevel(1); 

PlayerProperties

PlayerProperties 是一种同步玩家间状态的方法,以下是代码示例,目的为同步玩家手中所持有的武器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//区分C#内置的ashtable与Photon提供的Hashtable
using Hashtable = ExitGames.Client.Photon.Hashtable;

public class PlayerController : MonoBehaviourPunCallbacks
{
PhotonView PV;

private void Awake()
{
PV = GetComponent<PhotonView>();
}

//同步武器的实质是同步所持武器的itemIndex
void EquipItem(int _index)
{
itemIndex = _index;

//当我们装备一个武器时,首先查看是否是localPlayer,若是,将把这个切换到的武器index发出去
if (PV.IsMine)
{
Hashtable hash = new Hashtable();
hash.Add("itemIndex", itemIndex);
PhotonNetwork.LocalPlayer.SetCustomProperties(hash);
}
}

//所有在房间内玩家会收到同步信息,调用此回调。参数中给出了属性被改变的玩家,属性新值
public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
//本地已经切换了武器,无需再次同步 && 在客户端看,仅仅改变对应玩家的index,而不是所有玩家
if (changedProps.ContainsKey("itemIndex")&&!PV.IsMine && targetPlayer == PV.Owner)
{
EquipItem((int)changedProps["itemIndex"]);
}
}
}

[PunRPC]

Example

需要Pun远程调用的方法,应当给与PunRPC标签,下面给出造成伤害、收到伤害的流程:

单发枪类,能够开火

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingleShotGun : Gun
{
//枪开火,进行射线检测
void Shoot()
{
//从摄像机的近剪裁面的中点向着远剪裁面的中点绘制一条射线
Ray ray = cam.ViewportPointToRay(new Vector3(0.5f, 0.5f));
ray.origin = cam.transform.position;
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log("[Weapon]Ray cast hit " + hit.collider.gameObject.name);
hit.collider.gameObject.GetComponent<IDamageable>()?.TakeDamage(((GunInfo)itemInfo).damage);

//生成 Bullet Impact
PV.RPC(nameof(RPC_Shoot),RpcTarget.All, hit.point,hit.normal);
}
}
}

PlayerController类,每一个玩家角色GameObject都挂载此类

使用nameof()可以避免可能的拼写错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//本地被击打的物体实现IDamageable接口,调用TakeDamage()方法
public class PlayerController : MonoBehaviourPunCallbacks, IDamageable
{
public void TakeDamage(float damage)
{
//PV.Owner指定了被打击的角色,damage为此次命中的伤害
PV.RPC(nameof(RPC_TakeDamage), PV.Owner, damage);
Debug.Log("[Combat]" + "I(" + PhotonNetwork.NickName + ") Try to hurt " + PV.Owner.NickName + " at damage " + damage);
}

//对于被打击的玩家来说,其RPC方法被调用了
[PunRPC]
void RPC_TakeDamage(float damage, PhotonMessageInfo info)
{
Debug.Log("[RPC][Combat]" + PV.Owner.NickName + " Take damage " + damage);
currentHealth -= damage;

healthbarImage.fillAmount = currentHealth / maxHealth;

if (currentHealth <= 0)
{
Die();
//找到发送信息者,追溯伤害来源
PlayerManager.Find(info.Sender).GetKill();
}
}
}

Bullet Impact

通过Photon同步大量生成的GameObject是昂贵的,一般使用RPC调用对应方法,生成物体。

1
2
3
4
5
6
7
8
9
10
11
12
[PunRPC]
void RPC_Shoot(Vector3 hitPosition,Vector3 hitNormal)
{
Collider[] colliders = Physics.OverlapSphere(hitPosition, 0.3f);
Debug.Log("[RPC][Combat]" + hitPosition);
if (colliders.Length != 0)
{
GameObject bulletImpactObj = Instantiate(bulletImpactPrefab, hitPosition + hitNormal * 0.005f, Quaternion.LookRotation(hitNormal, Vector3.up) * bulletImpactPrefab.transform.rotation);
Destroy(bulletImpactObj,10f);
bulletImpactObj.transform.SetParent(colliders[0].transform);
}
}

Photon View

View ID[1..999] 对于每个Photon View 应当是 unique 的

Photon.Instantiate

Instantiate prefabs from Photon is not the same as instantiate a prefab from Unity normally

为了能够在多种平台上成功的加载预制体,尤其是此挂载了PhotonView的预制体(而不是仅仅在Editor中),

PhotonPrefabs must be in the resources folder.

Because Unity automatically excludes any file not referenced in the editor from the final build, and we don’t reference PhotonPrefabs. We use strings.

1
PhotonNetwork.Instantiate(Path.ComPhotonNetwork.Instantiate(Path.Combine("PhotonPrefabs", nameof(PlayerManager)), Vector3.zero, Quaternion.identity);bine("PhotonPrefabs", "PlayerManager"), Vector3.zero, Quaternion.identity);

为了防止可能的拼写错误,可以使用nameof()

Unity 能够将在Resources文件夹中的文件正常打包

可以在Instantiate的时候为对应GameObject的PV赋值

以创建PlayerController为例,将需要传递的参数封装到最后一个参数object[] 种

1
2
3
4
5
6
7
8
9
10
11
12
13
void CreateController() 
{
Transform spawnpoint = SpawnManager.Instance.GetSpawnpoint();
Debug.Log("[Player]Instantiated Player Controller: " + PhotonNetwork.NickName);
controller = PhotonNetwork.Instantiate(System.IO.Path.Combine("PhotonPrefabs",nameof(PlayerController)),spawnpoint.position, spawnpoint.rotation, 0, new object[] {PV.ViewID});
}


void GetParam()
{
//拆箱时,需要指定cast的类型
int id = (int)PV.InstantiationData[0];
}

Photon Transform View

需要通过Photon同步Transform信息的物体,应当在Photon View之外额外挂在此组件。通过Photon同步时,不应当继续使用本地的物理等等逻辑计算的Transform信息,否则会导致GameObject的抖动等不稳定现象。

在初始化之初关闭即可

1
2
3
4
if(!PV.isMine)
{
Destroy(rb);
}

PUN(Not recommanded)

Photon Unity Networking Classic - FREE | 网络 | Unity Asset Store

Photon.MonoBehaviour vs. Photon.PunBehaviour

For PunBehaviour:

This class provides a .photonView and all callbacks/events that PUN can call. Override the events/methods you want to use.

多与回调相关时候,应当使用 Photon.PunBehaviour