Unity之NetCode for GameObjets 基本使用
- 说明
- 思路
- 相关API
- 代码实现
- Tips
说明
最近项目需要联机,项目方案选用Unity提供的NetCode for GameObjets(以下简称NGO),踩了不少坑,本文不介绍基础使用,围绕双端(主机+客户端)登录大厅展开介绍,这里记录总结一下。
思路
了解到功能需求以后,我有两个疑问:
- 当我某一个客户端上线如何将自身的信息同步给其它在线用户?
- 建立连接后,消息状态是如何同步的?
- 所有玩家的信息(比如玩家身上一个脚本标识)如何维护起来?
带着疑问继续往下走
首先开启主机/服务器/客户端非常简单,只需要对应调用StartHost(),StartClient(),StartServer()
即可。
在每一个客户端创建了一个Dictionary<ulong, PlayerInfo>()
用于保存在线的玩家信息ulong是每个客户端ClientID
,PlayerInfo
是相关玩家信息。
当某个玩家上线后,会本地add一下,并调用RPC方法,告诉其他玩家,我来了
我本地存了一个JSON,每次客户端上线后,调用一个ServerRPC,将本地客户端的消息同步给其它客户端,主机端监听客户端的连接情况,每当有新客户端加入,调用一个ClientRPC,将信息同步给客户端。
离线也是如此
相关API
ServerRPC
RPC 是一个标准的软件行业概念。它们是对不在同一可执行文件中的对象调用方法的一种方式。
客户端可以在 NetworkObject 上调用服务器 RPC。RPC 被放置在本地队列中,然后发送到服务器,在那里它在同一 NetworkObject 的服务器版本上执行。
从客户端调用 RPC 时,SDK 会记录该 RPC 的对象、组件、方法和任何参数,并通过网络发送该信息。服务器或分布式颁发机构服务接收该信息,查找指定对象,查找指定方法,并使用收到的参数在指定对象上调用该方法。
ClientRPC
服务器可以在 NetworkObject 上调用客户端 RPC。RPC 被放置在本地队列中,然后发送到选定的客户端(默认情况下,此选择是所有客户端)。当客户端收到 RPC 时,RPC 将在同一 NetworkObject 的客户端版本上执行。
代码实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using QFramework;
using Unity.Netcode;//用户信息类 同步消息
public struct PlayerInfo : INetworkSerializable
{//客户端idpublic ulong id;//网络标识idpublic ulong networkID;public int typeID;public PlayerInfo(ulong id,ulong networkID,int typeID){this.id = id;this.networkID = networkID; this.typeID = typeID; }public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter{serializer.SerializeValue(ref id);serializer.SerializeValue(ref networkID);serializer.SerializeValue(ref typeID);}
}public class UI_Desk_Ctrl :NetworkController
{[Header("角色1"),SerializeField] UI_DeskUserItemCtrl win_deskUserItemCtrl;[Header("角色2"), SerializeField] UI_DeskUserItemCtrl lif_deskUserItemCtrl;IInitModel_DeckRescue deckRescue_InitModel;//玩家列表Dictionary<ulong, PlayerInfo> allPlayerInfos;void Awake(){deckRescue_InitModel = this.GetModel<IInitModel_DeckRescue>();allPlayerInfos=new Dictionary<ulong, PlayerInfo>();}public override void OnNetworkSpawn(){base.OnNetworkSpawn();if (this.IsServer){NetworkManager.OnClientConnectedCallback += OnClientConn;NetworkManager.OnClientDisconnectCallback += OnClientDis;}else{NetworkManager.OnClientDisconnectCallback += OnClientDisInClient;}deckRescue_InitModel.CurUserType.RegisterWithInitValue(type => {WaitPlayerInit((int)type).ToAction().Start(this);}).UnRegisterWhenGameObjectDestroyed(this);}void OnClientDisInClient(ulong obj){RemovePlayer(obj);}//当客户端连接时 服务端执行void OnClientConn(ulong obj){//服务端更新客户端的玩家foreach (var item in allPlayerInfos){UpdatePlayerInfoClientRpc(item.Value);}}//当客户端断开连接void OnClientDis(ulong obj){RemovePlayer(obj);}//延时等待 获取NetworkObjectIdIEnumerator WaitPlayerInit(int typeID){while (NetworkManager.LocalClient.PlayerObject == null){yield return null; }if (!this.IsServer){UpdatePlayerInfoServerRpc(new PlayerInfo(NetworkManager.LocalClientId, NetworkManager.LocalClient.PlayerObject.NetworkObjectId, typeID));}AddPlayer(new PlayerInfo(NetworkManager.LocalClientId, NetworkManager.LocalClient.PlayerObject.NetworkObjectId, typeID));}[ClientRpc]void UpdatePlayerInfoClientRpc(PlayerInfo info){if (!this.IsServer){if (allPlayerInfos.ContainsKey(info.id))allPlayerInfos[info.id] = info;elseAddPlayer(info);}}[ServerRpc(RequireOwnership =false)]void UpdatePlayerInfoServerRpc(PlayerInfo info){if (IsServer){if (allPlayerInfos.ContainsKey(info.id))allPlayerInfos[info.id] = info;elseAddPlayer(info);}}//添加玩家void AddPlayer(PlayerInfo info){if (!allPlayerInfos.ContainsKey(info.id)){Debug.Log("服务端添加客户端的 clientID: " + info.id);allPlayerInfos.Add(info.id, info);var netwoObj = NetworkManager.Singleton.SpawnManager.SpawnedObjects[info.networkID];UserType type = (UserType)info.typeID;switch (type){case UserType.None:break;case UserType.Winchman:win_deskUserItemCtrl.UpdateData("角色1", "上线", true);break;case UserType.Lifeguard:lif_deskUserItemCtrl.UpdateData("角色2", "上线", true);break;}}else{Debug.Log("玩家已经存在 存在id:" + info.id);}}//移除玩家void RemovePlayer(ulong clientID){if (allPlayerInfos.ContainsKey(clientID)){Debug.Log("服务端接收到客户端退出:"+clientID +" netwoid:"+ allPlayerInfos[clientID].networkID);UserType type = (UserType)allPlayerInfos[clientID].typeID;switch (type){case UserType.None:break;case UserType.Winchman:win_deskUserItemCtrl.UpdateData("绞车手", "下线", false);break;case UserType.Lifeguard:lif_deskUserItemCtrl.UpdateData("救生员", "下线", false);break;}allPlayerInfos.Remove(clientID);}}public override void OnNetworkDespawn(){base.OnNetworkDespawn();if (this.IsServer){Debug.Log("服务端关闭:"+NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject().OwnerClientId);RemovePlayer(NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject().OwnerClientId);allPlayerInfos = new Dictionary<ulong, PlayerInfo>();NetworkManager.Shutdown();Debug.Log("服务器关闭");}}
}
Tips
NetworkObjectId
和 ClientId
在 Unity Netcode 中是两个不同的概念,它们用于不同的目的:
-
ClientId:
ClientId
是用于标识每个连接的客户端的唯一标识符。
每个客户端连接到服务器时都会分配一个唯一的 ClientId,在整个会话期间保持不变。
主要用于管理客户端连接、客户端之间的通信,以及区分各个连接的客户端。 -
NetworkObjectId:
NetworkObjectId
是用于标识每个网络对象的唯一标识符。
每个被网络管理的对象(例如玩家角色、物品等)都有一个NetworkObject
组件,该组件自动生成一个NetworkObjectId
,用于唯一标识这个对象。
NetworkObjectId
是在所有客户端和服务器之间同步的,主要用于查找和管理网络中生成的 GameObject 实例。相关
在 Unity Netcode 中,要确保传递给 RPC 或 NetworkVariable 的数据类型是可序列化的,遵循以下规则来判断数据类型是否可以序列化:
-
内置可序列化类型
以下类型可以直接在ServerRpc
或ClientRpc
中使用,因为 Netcode 已经支持它们的序列化:基本数据类型:
int, float, double, bool, char
整型数据:byte, sbyte, short, ushort, long, ulong
结构体:Vector2, Vector3, Quaternion, Color, Color32
字符串:string
数组:所有基本数据类型和上述结构体类型的 一维数组,例如int[], float[], string[], Vector3[]
枚举:枚举类型可以直接用于 RPC 参数 -
实现了
INetworkSerializable
的类型
如果类型没有被 Netcode 内置支持(比如自定义的复杂对象),需要通过实现INetworkSerializable
接口来自定义序列化方式。Netcode 提供的INetworkSerializable
接口定义了序列化和反序列化方法,使自定义类型可以通过网络传输。
如果想获取到某个网络组件,可在同步的信息中保存NetcodeID,然后根据NetworkManager.Singleton.SpawnManager.SpawnedObjects
获取对应NetcodeObj组件
如有错误,欢迎指正!!!