网络知识 娱乐 Unity Netcode for GameObject(原MLAPI) 1.0.0 学习笔记(一)—— Hello World Demo

Unity Netcode for GameObject(原MLAPI) 1.0.0 学习笔记(一)—— Hello World Demo

文章目录

  • 📕 Netcode for GameObject 的前世今生
  • 📕 导入 Netcode for GameObject
    • 🔍 2020.3.x 导入教程
    • 🔍 2021 版本导入教程
  • 📕 Hello World Demo
    • 🔍 NetworkManager
    • 🔍 NetworkObject
  • 📕 Demo 中的 HelloWorldPlayer.cs 脚本
    • 🔍NetworkBehaviour
      • ⭐ NetworkVariable
      • ⭐ RPC 同步
  • 📕 Demo 中的 HelloWorldManager.cs 脚本
  • 🌹 总结

📕 Netcode for GameObject 的前世今生

Netcode (for GameObject) 是 Unity 官方基于 GameObject 的多人联机游戏框架。Unity 早期主打的网络框架是 UNet,现已过时被废弃。从 2020 年末开始, Unity 官方宣布 MLAPI(一个第三方开源网络框架)的作者加入官方团队,将 MLAPI 纳入主打的第一方网络框架,后来在 2021 年末将 MLAPI 升级为 Netcode for GameObject,发布了 1.0.0 版本。(本篇博客写于2022 年 6 月,此时 Netcode for GameObject 的版本已为 1.0.0 pre-9,算是比较稳定的版本了)

本篇博客算是本人在学习 Netcode for GameObject 时整理的一篇学习笔记,关注的是官方教程 Hello World Demo 中的操作注意点和相关的知识点,来对 Netcode for GameObject 有一个初步的认识。一些比较深入的知识仍在学习当中。

官方教程:
https://docs-multiplayer.unity3d.com/netcode/current/about

📕 导入 Netcode for GameObject

Netcode 支持的版本:Unity 2020.3.x, 2021.1.x, 2021.2.x(官方教程中给出的版本,本篇博客具有时效性,未来官方会有更多版本支持,以官方文档为主)

🔍 2020.3.x 导入教程

法一:
首先到 Github 下载 Netcode 到本地,链接:https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/tree/develop
然后打开 Unity,点击 Windows->Package Manager,点击左上角的 “+”,选择 Add package from disk)
在这里插入图片描述
选择下载的 Netcode 文件夹中的这个子文件夹:
在这里插入图片描述
然后选择导入 package 文件,这是一个 Json 格式的文件。
在这里插入图片描述
导入成功后就能在 Package Manager 的 Custom 中看到 Netcode For GameObject:
在这里插入图片描述

法二:
选择 Add package from git URL,输入 Netcode 的 github Https 链接
本人是用第一种方法,因为第二种方法可能速度会很慢,暂时还未成功过。

🔍 2021 版本导入教程

2021 版本导入方式和 2020.3.x 一致,只不过包的添加方式多了一种 Add Package by name
在这里插入图片描述
点击后输入 com.unity.netcode.gameobjects 即可导入。

📕 Hello World Demo

官方教程链接:https://docs-multiplayer.unity3d.com/netcode/current/tutorials/helloworld/helloworldintro

这个 Demo 展示了 Netcode for GameObject 的一些基础用法,主要实现了:
1️⃣ 客户端向服务器申请改变位置,服务器随机生成客户端位置后同步给客户端
2️⃣ 服务端改变位置,然后同步给客户端

具体的场景搭建步骤在官方文档中有详细的说明,我这里对 Demo 中涉及的知识点做了一个整合。


🔍 NetworkManager

新建一个空物体,在 Inspector 面板中的 Add Component 中可添加 NetworkManager 组件:

在这里插入图片描述
NetworkManager 相当于服务端/客户端的全局管理器,是个单例脚本,用于处理网络相关配置,网络预设体,网络场景管理等和网络相关的东西。该组件的下方有 Start Host,Start Server,Start Client 三个按钮,用于设置当前的游戏处于哪一种运行模式:

  • Server(服务端)
  • Client(客户端)
  • Host(既是服务端,也是客户端)

在 Unity 编辑器中运行游戏,然后在 Inspector 面板中点击这几个按钮即可在编辑模式中开启对应的网络模式。当然,发布后的游戏是不能使用编辑器下的功能的。因此,我们还可以用代码来开启其中一种运行模式:

NetworkManager.Singleton.StartServer();      
NetworkManager.Singleton.StartHost();        
NetworkManager.Singleton.StartClient();

回看刚刚 NetworkManager 组件的截图,可以发现中间有一个警告标志,这是因为我们还缺少了 transport 的选择。点击 select transport,选择 UNetTransport:
在这里插入图片描述
这里可能有些小伙伴会有疑问:之前不是说 UNet 这个东西已经过时了吗,为什么这里又会用到它呢?其实原本 UNet 分为处理底层逻辑的 LLAPI(Low Level API)和处理上层逻辑的 HLAPI(High Level API)。随着时代的发展,原本的 HLAPI 已经不适合网络开发各式各样的需求,但是底层上的一些处理是类似的。因此 UNet 上层处理逻辑的封装被废弃,但是 Netcode for GameObject 依然支持处理底层逻辑的 LLAPI 。

选择后可以看到面板中多了一个组件:
在这里插入图片描述
可以看到在这里能够设置 ip 地址,端口号等与网络相关的配置。

而 Transport 的另一个选项 UnityTransport 也是对网络底层处理逻辑的封装,只是实现方式和 UNetTransport 有区别。官方教程中是使用 UNetTransport,这里我暂时没对这两种 Transport 进行深入比较。

🔍 NetworkObject

现在我们创建需要网络同步的 GameObject,把它做成 Prefab,注册到 NetworkManager 当中。

在场景中创建一个胶囊体来代表玩家,然后在该游戏物体上挂载 NetworkObject 组件(注:只有挂载了 NetworkObject 组件的物体才能实现网络同步):

在这里插入图片描述
NetworkObject 组件会为这个物体在游戏运行期间动态生成一个 Network Id,用于唯一标识网络中的这个物体。
然后把这个游戏物体做成预设体,在场景中删除。
最后,我们回到 NetworkManager 组件,将这个预设体拖入到下图位置进行注册:
在这里插入图片描述

NetworkPrefabs 就是网络中需要同步的游戏物体。

而当我们给 Player Prefab 赋值以后,只要客户端连接到服务器,游戏会自动为客户端生成这个 Prefab

现在我们已经将 NetworkObject 和 NetworkManager 关联到了一起,能够实现客户端和服务端连接成功时,在客户端自动生成 Player Prefab。这是因为 NetworkManager 会根据 Transport 组件(前面我们选用的是 UNet Transport)中的 ip 地址和端口号等信息进行网络的连接。当客户端和服务端连接成功后,NerworkManager 中的 NetworkSpawnManager 会为该客户端发布 注册在 NetworkManager 的那个 Player Prefab (有点像观察者设计模式),那么这个 Network Object (也就是 Player Prefab)就会通过网络复制到对应的客户端。

可是到现在为止,还是无法很好地看出网络同步的功能。我们想要看到一个客户端中的 Player 移动后,其他客户端中的这个 Player 也会同步发生移动。
那么官方为我们提供了两个样例脚本,运用 Netcode For GameObject 中的功能,实现网络同步。


📕 Demo 中的 HelloWorldPlayer.cs 脚本

创建一个脚本,叫做 HelloWorldPlayer,将它挂到 Player Prefab 上。

using Unity.Netcode;
using UnityEngine;
namespace HelloWorld
{
    public class HelloWorldPlayer : NetworkBehaviour
    {
        public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>();

        public override void OnNetworkSpawn()
        {
            if (IsOwner)
            {
                Move();
            }
        }

        public void Move()
        {
            if (NetworkManager.Singleton.IsServer)
            {
                var randomPosition = GetRandomPositionOnPlane();
                transform.position = randomPosition;
                Position.Value = randomPosition;
            }
            else
            {
                SubmitPositionRequestServerRpc();
            }
        }

        [ServerRpc]
        void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
        {
            Position.Value = GetRandomPositionOnPlane();
        }

        static Vector3 GetRandomPositionOnPlane()
        {
            return new Vector3(Random.Range(-3f, 3f), 1f, Random.Range(-3f, 3f));
        }

        void Update()
        {
            transform.position = Position.Value;
        }
    }
}

这个脚本涉及到 Netcode 中的几个重要知识点:

🔍NetworkBehaviour

NetworkBehavoiur 是继承自 MonoBehaviour 的抽象类,用于网络实体之间的通信。我们可以在继承自 NetworkBehaviour 的类中编写代码,来为我们的 Network Object 添加各式各样的逻辑。可以看出 NetworkBehaviour 所属于 Network Object 。为了实现 Network Object 之间的通信与同步,NetworkBehaviour 提供了两个解决方案:NetworkVariable 和 RPC 。

⭐ NetworkVariable

NetworkVariable 是用于网络同步的变量,它是个泛型类。Demo 中的 HelloWorldPlayer 脚本中使用的是 NetworkVariable ,用于表示位置的同步,对应 Transform 组件中的 Position。
在这里插入图片描述
NetworkVariable 的状态会被周期性地同步(复制)到所有连接到服务端的客户端里,当一个新的客户端加入连接后,其他客户端的 NetworkVariable 的最新状态会被同步到新的客户端。

NetworkVariable 的权限是服务端能写,任何人能读。这意味着只有服务端才有修改 NetworkVariable 的能力,然后客户端能够通过网络同步读取到更新后的 NetworkVariable
在这里插入图片描述
在这里插入图片描述
在官方的 HelloWorld Demo 中,客户端和服务器都能改变 Player 的位置。可是刚刚不是说了客户端没有写的权限吗?实际上,客户端的确不能自己改变 Player 的位置(也就是 NetworkVariable 的值),但是客户端可以借助服务器能写的权限来改变自己的 NetworkVariable 呀。这里,我们就要引出 Netcode 的另一个重要知识点:RPC

⭐ RPC 同步

RPC(Remote Procedure Call) 是网络通信中的一个术语,叫做远程过程调用。它能通过网络从远程计算机中请求服务。放到 Demo 中就是客户端通过网络从服务端中请求改变自己 Player 的位置。

Netcode 有两种 RPC 请求方式:ServerRpc 和 ClientRpc

ServerRpc 就是客户端发送 RPC 到服务端。比如客户端针对一个 Network Object 向服务端发送 RPC,这个 RPC 会先被保存到本地的队列,然后被发送至服务端。服务端会根据 RPC 中记录的这个 Network Object 的信息,通过 NetworkManager 找到属于这个客户端的 Network Object,然后就可以根据 RPC 中要求调用的方法对这个 Network Object 进行状态修改。有点像将一个方法委托给远端进行调用。
在这里插入图片描述
ClientRpc 就是反过来,服务端向客户端发送 RPC 请求。
在这里插入图片描述
回看 HelloWordPlayer 脚本,RPC 请求的部分如下所示:

 [ServerRpc]
 void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
 {
     Position.Value = GetRandomPositionOnPlane();
 }
 static Vector3 GetRandomPositionOnPlane()
 {
      return new Vector3(Random.Range(-3f, 3f), 1f, Random.Range(-3f, 3f));
 }

需要发送 RPC 的方法有几个注意点:
1)方法上方需添加特性,指明是 [ServerRpc] 还是 [ClientRpc]
2)方法名要以 ClientRpc 或 ServerRpc 结尾
3)方法必须声明在继承自 NetworkBehaviour 类的脚本中
4)必须指明当前在服务端还是客户端调用,Demo 中判断方式如下,可借助 NetworkManager 中的 IsServer,IsClient,IsHost

public void Move()
{
    if (NetworkManager.Singleton.IsServer)
    {
         var randomPosition = GetRandomPositionOnPlane();
         transform.position = randomPosition;
         Position.Value = randomPosition;
    }
    else
    {
        SubmitPositionRequestServerRpc();
    }
}

5)如果想直接往 RPC 方法中传递参数,只能传递值类型参数,因为 RPC 方法所需的ServerRpcParams/ClientRpcParams 参数类型是结构体(值类型)
在这里插入图片描述
在这里插入图片描述

回看 HelloWorldPlayer 脚本,实现 Player 位置同步大致的思路就是当触发了 Move 方法后,如果当前是服务端,直接随机出一个位置,修改原有的 NetworkVariable;如果当前是客户端,则向服务端发送 RPC 请求,在服务端修改 NetworkVariable 。那服务端怎么知道要修改的是当前客户端的位置而不是其他客户端呢?这个就是通过 NetworkObject 被赋予的 Network Id 来区分。然后因为客户端和服务端的 NetworkVariable 会定期同步状态,所以此脚本中的 Update 函数用于每帧将同步的位置赋给 Player 的 Transform 组件,以此实现在服务端中改变对应客户端的位置,然后同步到其他客户端。

void Update()
{
     transform.position = Position.Value;
}

因此客户端和服务端的位置改变都会引起另一端的同步改变。

那么什么时候触发 Move 方法呢?可以看到在 OnNetworkSpawn 方法中执行了一次。

 public override void OnNetworkSpawn()
 {
     if (IsOwner)
     {
          Move();
     }
 }

这个方法重写了 NetworkBehaviour 类中的 OnNetworkSpawn,它会在网络建立成功且 Network Object 被生成的时候调用。

IsOwner 是用于判断当前的 Network Object 是否由自己控制以及是否是自己的 Player Object。这个可以理解为当前的这个 Network Obejct 是否是本地客户端的一个物体,如果是,将这个物体随机生成在一个位置。

因此网络中的物体在初始化生成时会调用一次 Move。那如果想之后继续调用呢?

📕 Demo 中的 HelloWorldManager.cs 脚本

这里 Demo 提供了另外一个脚本,叫做 HelloWorldManager。我们在场景中新建一个空物体 HelloWorldManager,然后把此脚本挂载上去。

using Unity.Netcode;
using UnityEngine;
namespace HelloWorld
{
    public class HelloWorldManager : MonoBehaviour
    {
        void OnGUI()
        {
            GUILayout.BeginArea(new Rect(10, 10, 300, 300));
            if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
            {
                StartButtons();
            }
            else
            {
                StatusLabels();

                SubmitNewPosition();
            }

            GUILayout.EndArea();
        }

        static void StartButtons()
        {
            if (GUILayout.Button("Host")) NetworkManager.Singleton.StartHost();
            if (GUILayout.Button("Client")) NetworkManager.Singleton.StartClient();
            if (GUILayout.Button("Server")) NetworkManager.Singleton.StartServer();
        }

        static void StatusLabels()
        {
            var mode = NetworkManager.Singleton.IsHost ?
                "Host" : NetworkManager.Singleton.IsServer ? "Server" : "Client";

            GUILayout.Label("Transport: " +
                NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetType().Name);
            GUILayout.Label("Mode: " + mode);
        }

        static void SubmitNewPosition()
        {
            if (GUILayout.Button(NetworkManager.Singleton.IsServer ? "Move" : "Request Position Change"))
            {
                if (NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient)
                {
                    foreach (ulong uid in NetworkManager.Singleton.ConnectedClientsIds)
                        NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(uid).GetComponent<HelloWorldPlayer>().Move();
                } 
                else
                {
                    var playerObject = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject();
                    var player = playerObject.GetComponent<HelloWorldPlayer>();
                    player.Move();
                }
            }
        }
        
    }
}

因为这个 Demo 要在游戏中选择网络模式,所以用了 GUI 的按钮来表示三种不同模式:
在这里插入图片描述
进入 Server 模式时,界面的 UI 如下:
在这里插入图片描述
进入 Client 模式,界面的 UI 如下:
在这里插入图片描述
进入 Host 模式,界面的 UI 如下:
在这里插入图片描述
回看 HelloWorldManager 脚本,进入任意一种模式后点击对应的按钮会执行 SubmitNewPosition 方法,来调用 Move 方法。

如果是纯服务器(Host 不算),那么它会找到所有连接的客户端的 Id,然后通过NetworkManager 中的 NetworkSpawnManager 找到对应 Id 的 Network Object,执行它的 Move 方法。

如果不是纯服务器,则可以直接找到本地(客户端)里的 Network Object,执行它的 Move 方法。

所以如果是 Clinet 连接 Server ,在 Client 模式点击 Request Position Change,只会改变 Client 中那个 Player 的位置,然后同步给其他端。在 Server 模式点击 Move,服务端和客户端中所有的 Player 位置都会同步发生改变。
在这里插入图片描述

如果是 Clinet 连接 Host ,点击 Move,只有 Host 中的那个 Player 位置会改变,然后同步给其他端。点击 Clinet 中的 Request Position Change 只会改变当前 Client 中那个 Player 的位置,然后同步给其他端。
在这里插入图片描述

注:要想测试服务端和客户端的同步变化,将游戏打包出来运行比较好,因为 Unity 编辑器只能开启一个游戏实例。而打包出的 exe 文件能够开启多个。


🌹 总结

本篇博客是根据 Netcode For GameObject 的官方新手 Demo 整理的学习笔记。大家主要掌握以下几个知识点就行了:

  • NetworkManager
  • NetworkObject
  • NetworkBehaviour(重点展示了 NetworkVariable 和 RPC 的使用)