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
{
//客户端id
public ulong id;
//网络标识id
public 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);
}
//延时等待 获取NetworkObjectId
IEnumerator 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;
else
AddPlayer(info);
}
}
[ServerRpc(RequireOwnership =false)]
void UpdatePlayerInfoServerRpc(PlayerInfo info)
{
if (IsServer)
{
if (allPlayerInfos.ContainsKey(info.id))
allPlayerInfos[info.id] = info;
else
AddPlayer(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组件
如有错误,欢迎指正!!!