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

news2024/12/27 10:22:59

Unity之NetCode for GameObjets 基本使用

  • 说明
    • 思路
    • 相关API
    • 代码实现
    • Tips

说明

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

思路

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

  1. 当我某一个客户端上线如何将自身的信息同步给其它在线用户?
  2. 建立连接后,消息状态是如何同步的?
  3. 所有玩家的信息(比如玩家身上一个脚本标识)如何维护起来?

带着疑问继续往下走

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

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组件


如有错误,欢迎指正!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2231291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++(类和对象-运算符重载)

运算符重载概念&#xff1a; 对已有的运算符重新进行定义&#xff0c;赋予其另一种功能&#xff0c;以适应不同的数据类型 运算符重载的同时也可以发生函数重载 1.加号运算符重载 1.1加号运算符重载的本质 1.2运算符重载也可以发生函数重载 总结1&#xff1a;对于内置的数据类型…

Flink CDC 同步 Mysql 数据

文章目录 一、Flink CDC、Flink、CDC各有啥关系1.1 概述1.2 和 jdbc Connectors 对比 二、使用2.1 Mysql 打开 bin-log 功能2.2 在 Mysql 中建库建表准备2.3 遇到的坑2.4 测试 三、番外 一、Flink CDC、Flink、CDC各有啥关系 Flink&#xff1a;流式计算框架&#xff0c;不包含 …

Sigrity Power SI VR noise Metrics check模式如何进行电源噪声耦合分析操作指导

SSigrity Power SI VR noise Metrics check模式如何进行电源噪声耦合分析操作指导 Sigrity Power SI的VR noise Metrics check模式本质上是用来评估和观测器件的电源网络的耦合对于信号的影响,输出S参数以及列出具体的贡献值。 以下图为例

Vue computed watch

computed watch watch current prev

恋爱脑学Rust之智能指针Rc,RefCell和Weak指针

小明和小丽为了维系彼此的关系&#xff0c;一起探索了智能指针的奥秘。通过 Rc、RefCell 和 Weak 的帮助&#xff0c;他们得以克服情感中遇到的种种困境。 第一章&#xff1a;Rc 智能指针的共生 小明和小丽搬进了一个共同的小屋&#xff0c;他们彼此相爱&#xff0c;决定共用…

Matlab车牌识别课程设计报告(附源代码)

Matlab车牌识别系统 分院&#xff08;系&#xff09; 信息科学与工程 专业 学生姓名 学号 设计题目 车牌识别系统设计 内容及要求&#xff1a; 车牌定位系统的目的在于正确获取整个图像中车牌的区域&#xff0c; 并识别出车牌号。通过设计实现车牌识别系…

Java 文件操作与IO流

文件 文件有两个概念&#xff0c;在广义来看就是操作系统上对硬件和软件资源抽象为文件。 在侠义上来看&#xff0c;就是我们保存在硬盘上的文件 在这里我们讨论的是狭义的文件&#xff0c;在外面的硬盘上的文件细分又可以分为二进制文件和文本文件&#xff0c;文本文件可以通…

C++ 优先算法 —— 有效三角形的个数(双指针)

目录 题目&#xff1a;有效三角形个数 1. 题目解析 2. 算法原理 解法一&#xff1a; 暴力枚举 解法二&#xff1a; 双指针算法 3. 代码实现 暴力枚举 双指针算法 题目&#xff1a;有效三角形个数 1. 题目解析 题目截图&#xff1a; 题目的意思就是在一个数组中&#x…

前端拖拽库方案之react-beautiful-dnd

近期&#xff0c;知名 React 拖拽库 react-beautiful-dnd 宣布了项目弃用的决定&#xff0c;未来将不再维护。这一决定源于其存在的缺陷与局限性&#xff0c;促使作者转向开发一个更加现代化的拖拽解决方案——Pragmatic drag and drop&#xff08;下面会介绍&#xff09;&…

《高频电子线路》—— 调制

文章内容来源于【中国大学MOOC 华中科技大学通信&#xff08;高频&#xff09;电子线路精品公开课】&#xff0c;此篇文章仅作为笔记分享。 调制 调制的原因 第一个原因 是为了要做出切实可行的天线。 无线电波能够从天线发射出去&#xff0c;以及正常的接收&#xff0c;需要…

第二十四章 v-model原理及v-model简化表单类组件封装

目录 一、v-model 原理 二、表单类组件封装 三、v-model简化组件封装代码 一、v-model 原理 原理&#xff1a;v-model本质上是一个语法糖。例如应用在输入框上&#xff0c;就是 value属性 和 input事件 的合写。 作用&#xff1a;提供数据的双向绑定 ① 数据变&#x…

机器学习中的数据可视化:常用库、单变量图与多变量图绘制方法

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

SELS-SSL/TLS

一、了解公钥加密&#xff08;非对称加密&#xff09; 非对称加密中&#xff0c;用于加密数据的密钥与用于解密数据的密钥不同。私钥仅所有者知晓&#xff0c;而公钥则可自由分发。发送方使用接收方的公钥对数据进行加密&#xff0c;数据仅能使用相应的私钥进行解密。 你可以将…

Kubernetes中的secrets存储

华子目录 2.secrets2.1secrets功能介绍2.2secrets的创建2.2.1从文件创建2.2.2编写yaml文件 2.3secret的使用案例2.3.1将secret挂载到volume中2.3.2设置子目录映射secret密钥2.3.3将secret设置为环境变量2.3.4存储docker register的认证信息spec.imagePullSecrets[] 2.secrets …

软件设计师笔记-数据结构

数据结构 数据元素的集合及元素间的相互关系和构造方法。 线性表的存储结构 顺序存储链式存储 单链表节点 typedef struct node { int data; struct node *link; }NODE, *LinkList; 双向链表 每个节点有两个指针&#xff0c;分别指出直接前驱和直接后继。 循环链表 尾…

「Mac畅玩鸿蒙与硬件22」鸿蒙UI组件篇12 - Canvas 组件的动态进阶应用

在鸿蒙应用中,Canvas 组件可以实现丰富的动态效果,适合用于动画和实时更新的场景。本篇将介绍如何在 Canvas 中实现动画循环、动态进度条、旋转和缩放动画,以及性能优化策略。 关键词 Canvas 组件动态绘制动画效果动态进度条旋转和缩放性能优化一、使用定时器实现动画循环 …

通俗易懂的理解递归 回溯 DFS

文章目录 递归概念递归例子1&#xff1a;递归打印链表递归例子2&#xff1a;求n数之和 回溯概念回溯例子1&#xff1a;组合问题 DFS概念DFS例子1&#xff1a;不同路径DFS例子2&#xff1a;岛屿数量总结 递归 概念 “方法自己调用自己&#xff0c;每一次调用都会更加接近递归的…

【AD】1-7 AD24软件扩展插件的设置与安装

1.如图所示打开扩展 2.点击齿轮后&#xff0c;确保离线安装位置关联了软件安装包的路径位置后&#xff0c;进行勾选选择后&#xff0c;点击应用即可安装。 注意&#xff1a;如果位置关联错误&#xff0c;则显示如图

Window on ARM解锁所有的TTS语音包供python调用

Window on ARM解锁所有的TTS语音包供python调用 可用的语音包查看查看TTS可用的语音包解锁语音包设置升级系统打开注册表导出注册表修改注册表导入新的注册表可用的语音包查看 微软的Windows 10操作系统为设备上安装的每种语言提供了一套语音。但只有部分已安装的语音能在整个…

pandas数据处理高级系列003---什么是交叉表(Cross Tabulation)以及pandas如何生成

做ab测试的时候遇到了一个新的知识点&#xff0c;交叉表以及如何用pandas生成交叉表 交叉表&#xff08;Cross Tabulation&#xff09;&#xff0c;也称为列联表&#xff08;Contingency Table&#xff09;&#xff0c;是一种用于统计分析的表格&#xff0c;用于显示两个或多个…