Unity 使用Netcode实现用户登录和登出

Unity之NetCode for GameObjets 基本使用

说明

最近项目需要联机,项目方案选用Unity提供的NetCode for GameObjets(以下简称NGO),踩了不少坑,本文不介绍基础使用,围绕双端(主机+客户端)登录大厅展开介绍,这里记录总结一下。

思路

了解到功能需求以后,我有两个疑问:

  1. 当我某一个客户端上线如何将自身的信息同步给其它在线用户?
  2. 建立连接后,消息状态是如何同步的?

带着疑问继续往下走

首先开启主机/服务器/客户端非常简单,只需要对应调用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 的客户端版本上执行。


代码实现

csharp 复制代码
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

NetworkObjectIdClientId 在 Unity Netcode 中是两个不同的概念,它们用于不同的目的:

  • ClientId:
    ClientId 是用于标识每个连接的客户端的唯一标识符。

    每个客户端连接到服务器时都会分配一个唯一的 ClientId,在整个会话期间保持不变。

    主要用于管理客户端连接、客户端之间的通信,以及区分各个连接的客户端。

  • NetworkObjectId:
    NetworkObjectId 是用于标识每个网络对象的唯一标识符。

    每个被网络管理的对象(例如玩家角色、物品等)都有一个 NetworkObject 组件,该组件自动生成一个 NetworkObjectId,用于唯一标识这个对象。
    NetworkObjectId 是在所有客户端和服务器之间同步的,主要用于查找和管理网络中生成的 GameObject 实例。

    相关


在 Unity Netcode 中,要确保传递给 RPC 或 NetworkVariable 的数据类型是可序列化的,遵循以下规则来判断数据类型是否可以序列化:

  1. 内置可序列化类型

    以下类型可以直接在 ServerRpcClientRpc 中使用,因为 Netcode 已经支持它们的序列化:

    基本数据类型:int, float, double, bool, char

    整型数据:byte, sbyte, short, ushort, long, ulong

    结构体:Vector2, Vector3, Quaternion, Color, Color32

    字符串:string

    数组:所有基本数据类型和上述结构体类型的 一维数组,例如 int[], float[], string[], Vector3[]

    枚举:枚举类型可以直接用于 RPC 参数

  2. 实现了 INetworkSerializable 的类型

    如果类型没有被 Netcode 内置支持(比如自定义的复杂对象),需要通过实现 INetworkSerializable 接口来自定义序列化方式。Netcode 提供的 INetworkSerializable 接口定义了序列化和反序列化方法,使自定义类型可以通过网络传输。


如果想获取到某个网络组件,可在同步的信息中保存NetcodeID,然后根据NetworkManager.Singleton.SpawnManager.SpawnedObjects获取对应NetcodeObj组件


相关推荐
学习!!!1 小时前
游戏引擎中的颜色科学
游戏引擎
这不比博人传燃?1 小时前
传奇996_3——使用补丁添加怪物
游戏引擎
tealcwu14 小时前
【Unity基础】Unity中的UI系统
ui·unity·lucene
北冥没有鱼啊16 小时前
UE5 射线折射
游戏·ue5·游戏引擎·ue4
※※冰馨※※17 小时前
Unity3D 鼠标移动到按钮上显示信息
开发语言·unity·c#
云围20 小时前
Gitlab 官方推荐自动化cache服务器Minio的安装
git·unity·ci/cd·自动化·gitlab·devops
魔法自动机1 天前
Unity3D学习FPS游戏(3)玩家第一人称视角转动和移动
unity·1024程序员节·fps
tealcwu1 天前
【Unity基础】初识UI Toolkit - 运行时UI
ui·unity·编辑器·游戏引擎
我们一起学前端1 天前
利用游戏引擎的优势
游戏引擎