当前位置:   article > 正文

Unity多人游戏开发-Netcode for GameObject-官方文档中文翻译_unity netcode 教程

unity netcode 教程

文章目录

官方文档

首先亮出文档,可以直接去看官方文档: 官方文档
翻译如有错误,恳请指正。
命令行运行教程链接: 教程

目前正在翻译的部分:NetWorking Components > NetworkTransform

文章版本

本文章基于Netcode for GameObject 1.5.2、1.6.0进行翻译。
关于Netcode for GameObject:1.5.2;
发行说明:1.5.2;
API参考:1.5.2;
开始使用\将Unity UNet迁移到Netcode for GameObjects(此部分内容及其之前内容的版本):1.5.2;
开始使用\开始使用NGO(此部分内容及其之后内容的版本):1.6.0;

关于Netcode for GameObject

Netcode for GameObjects(NGO)是一个为Unity构建的高级网络库,用于抽象化网络逻辑。它使你能够将游戏对象和世界数据通过网络会话发送给多个玩家。通过使用NGO,你可以专注于构建游戏,而不必关注低级协议和网络框架。

要了解更多关于Netcode for GameObjects的功能和能力,请查看下面的内容:

开始入门

安装Unity Netcode
从UNet迁移到Netcode
升级到Unity Netcode Package

开始项目

开始使用Netcode

教程和示例

Boss Room(首领房间)
Bite Size Samples(小示例)
Dilmer’s Tutorials(Dilmer的教程)

核心概念

网络
组件
对象
消息系统
序列化
场景

调试

日志记录
故障排除
错误消息

术语和常见问题解答

高级术语
多人游戏架构
常见问题解答

别忘了查看发行说明API文档

开始之前

Netcode支持以下版本:

Netcode支持以下平台:

  • Windows、macOS和Linux
  • iOS和Android
  • 运行在Windows、Android和iOS操作系统上的XR平台
  • 大多数封闭平台,如游戏主机。关于特定封闭平台的更多信息,请联系我们。这些内容通常受到保密协议的保护。
    • 当在游戏主机(如PlayStation、Xbox或任天堂Switch)上运行时,你在测试和正式发布游戏前应了解特定的Netcode政策。请参考游戏主机的内部文档以获取更多信息。此类内容通常受到保密协议的保护。

最后更新日期:2023年8月2日

发行说明

Netcode for GameObjects

Netcode for GameObjects的更新日志

以下内容跟踪了下一个版本的Unity Netcode的功能、更新、错误修复和重构。因为Netcode for GameObjects是开源的,你可以在com.unity.netcode.gameobjects的GitHub存储库中访问完整的发行说明和变更日志。

发布 |日期 | 更新日志
1.5.2 2023-07-24 1.5.2
1.5.1 2023-06-09 1.5.1
1.4.0 2023-04-10 1.4.0
1.3.1 2023-03-27 1.3.1
1.2.0 2022-11-21 1.2.0
1.1.0 2022-10-18 1.1.0
1.0.2 2022-09-08 1.0.2
1.0.1 2022-08-23 1.0.1
1.0.0 2022-06-27 1.0.0

最后更新日期:2023年8月14日

Boss Room(首领房间)

Boss Room更新日志

以下内容跟踪了Boss Room示例项目的下一个版本的功能、更新、错误修复和重构。由于Boss Room是开源的,你可以在com.unity.multiplayer.samples.coop的GitHub存储库中访问完整的发行说明和变更日志。
发布 | 日期 | 更新日志
2.1.0 2023-04-27 2.1.0
2.0.4 2022-12-14 2.0.4
2.0.3 2022-12-09 2.0.3
全部内容在此,我就不一个一个复制了,太累了

最后更新日期:2023年6月13日

Bitesize

多人Bitesize示例

以下内容跟踪了多人Bitesize示例项目的下一个版本的功能、更新、错误修复和重构。由于多人Bitesize是开源的,你可以在com.unity.multiplayer.samples.bitesize的GitHub存储库中访问完整的发行说明和变更日志。
发布 | 日期 | 更新日志
1.1.0 2022-12-13 1.1.0

最后更新日期:2023年8月14日

API参考

API参考

开始使用

安装Netcode for GameObjects

你可以使用本指南来帮助你在Unity项目中设置Netcode for GameObjects (NGO)。

前提条件

在开始安装Netcode之前,请确保你具备以下条件:

  • 一个有效的Unity账户和许可证。
  • 支持的Unity版本。请查看Netcode的要求以获取具体的Unity版本详情。
  • 现有的Unity项目。如果你对Unity还不熟悉,可以参考Get started with NGO部分以获得指南。

兼容性

  • Unity编辑器版本2021.3或更高
  • Mono和IL2CPP脚本后端

支持的平台

  • Windows、MacOS和Linux
  • iOS和Android
  • Windows、Android和iOS上的XR平台
  • 大多数封闭平台,例如游戏主机。关于特定封闭平台的更多信息,请联系NGO开发团队
  • WebGL(需要NGO 1.2.0+和UTP 2.0.0+)

注意
当运行在游戏主机等封闭平台(PlayStation、Xbox、Nintendo Switch)上时,可能会存在特定的政策和注意事项。请参考你所用游戏主机的文档以获取更多信息。

使用包管理器安装NGO

  1. Unity编辑器中选择“Window”>“Package Manager”。
  2. 在包管理器中,点击“+”Add符号>“Add package by name…”。
  3. 将com.unity.netcode.gameobjects输入到包名称字段中,然后选择“Add”。

适用于Unity编辑器版本2020.3 LTS或更早版本

  1. 从Unity编辑器中选择“Window”>“Package Manager”。
  2. 在包管理器中,点击“+”Add符号>“Add package by git URL…”
  3. 在git URL字段中输入或粘贴https://github.com/Unity-Technologies/com.unity.netcode.gameobjects,然后选择“Add”。

下一步

在安装了Netcode for GameObjects (NGO)之后,请参阅以下内容以继续你的旅程:

最后更新时间:2023年8月14日

将 MLAPI 升级为 Netcode for GameObjects

使用本指南将 MLAPI 0.1.0 升级至 Netcode for GameObjects (Netcode) 1.0.0

升级至 Netcode for GameObjects
强烈建议尽快从 MLAPI 升级至 Netcode for GameObjects。MLAPI 不再得到维护,未来将不会再更新。MLAPI 被视为废弃的产品。

备份你的 MLAPI 项目

信息
请将此步骤视为必要步骤:升级到 Netcode 的包版本可能会导致你当前项目出现问题。新版本修改了文件和位置,与之前的 MLAPI 版本有很大的不同。

使用以下推荐的方法备份你的项目:

  • 创建你整个项目文件夹的副本。
  • 使用源代码控制软件,比如Git。

最佳实践
我们建议使用这两种方法来备份你的项目。这将为你提供一个复制的项目,并通过已提交的更改和历史记录进行跟踪。

在你的MLAPI项目上使用升级工具

手动从MLAPI安装的.dll版本升级到新的包版本会导致场景和预制体中所有的MLAPI组件引用中断。Netcode使用不同于.dll的GUID来引用组件。

为了帮助你进行到Netcode的升级过程,我们创建了一个升级工具。

开始升级,通过在Package Manager窗口中使用 “Add package from git URL…” 选项,将升级工具添加到你的项目中。使用以下URL:

https://github.com/Unity-Technologies/multiplayer-community-contributions.git?path=/com.unity.multiplayer.mlapi-patcher#release-0.1.0
  • 1

安装更新程序包后,你可以继续进行升级过程。

安装Netcode包

按照安装指南安装Netcode。

安装完成后,你会在控制台中看到错误消息,这是预期的行为,因为你的项目现在同时包含了MLAPI和Netcode。这个问题将在本指南结束时得到解决。

安装Netcode包还会安装其他一些包,如Unity Transport、Unity Collections、Unity Burst等。

Burst包需要重新启动编辑器。因此在安装后重新启动Unity。Unity将在下次启动时要求你进入故障安全模式,这是正常行为,因为你的所有网络代码不再编译。

危险
暂时不要删除旧版的MLAPI。在接下来的步骤中,它仍然会被使用到。

更新脚本引用

通过在菜单栏中选择 Window > Netcode Patcher 打开 Netcode Patcher 窗口。该修补程序将会问你是否在使用 MLAPI 的安装版或源码版。

以前在项目中使用 MLAPI 有两种主要方式。你可以通过使用 MLAPI 安装程序下载发布版本的 MLAPI,或者手动将源文件复制到你的项目中。

提示
如果你不确定你正在使用哪种 MLAPI 方式,请检查你的项目中是否有 Assets/MLAPI/Lib/MLAPI.dll 文件。如果是这样的话,你正在使用安装版。

从安装版升级。
  1. 选择Installer。
  2. 选择Update Script References。
从源码版升级。
  1. 选择 Source。
  2. 窗口会提示你链接一个 MLAPI 源码目录。
  3. 获取包含 MLAPI 源码的项目目录,拖放到该字段中。
  4. 选择 Update Script References。

在完成补丁程序的“更新脚本引用(Update Script References)”过程后,你的预制体(Prefabs)和游戏对象(GameObject)上的网络代码组件应该已经被更新为它们的新名称。

Patcher 窗口中还有一个“Replace Type Names(替换类型名称)”按钮。这一步是可选的。它会自动将你的脚本中的旧类型名称重命名为 Netcode 中的 API 更改,节省了手动重命名的时间。它会对一些类型名称进行全局替换。如果你想对更改有更多的控制,你可以手动执行此过程。

移除旧的 MLAPI 版本

从你的项目中移除所有包含现有非包版本 MLAPI 的文件夹。通常意味着从项目中移除 Assets/MLAPI 和 Assets/Editor/MLAPI 文件夹。

将你的代码升级到新的 Netcode APIs

信息
代码升级是一个手动而且漫长的过程。如果在升级过程中遇到困难,请加入我们的 Discord,我们会为你提供支持。

Unity 多人游戏团队尽力保持了大部分 MLAPI 在 Netcode 中的完整性。然而,为了成功编译,仍然需要进行一些更改。

NetworkVariable 变化

NetworkVariable 类型现在只支持泛型,并且泛型中指定的类型必须是值类型。首先,将所有的 NetworVariable* 类型更改为泛型对应类型。例如,NetworkVariableInt 变为 NetworkVariable,NetworkVariableFloat 变为 NetworkVariable,依此类推。现在,一些类型(例如字符串)将不符合 NetworkVariable 的新类型要求。如果你的类型是字符串,你可以使用 FixedString32Bytes。需要注意的是,这种类型不允许你更改字符串的大小。对于只包含值类型的自定义结构体,你可以实现 INetworkSerializable 接口,这样就可以正常工作。最后,对于其他类型,你将需要创建自己的 NetworkVariable。为此,创建一个新的类,继承自 NetworkVariableBase,并实现所有抽象成员。如果你之前已经有了自定义的 NetworkVariable,现在的读取和写入函数会使用我们的 FastBuffer 从流中读取或写入。

场景管理的变化

场景管理有一些变化,统一了用户的使用方式。首先,它现在在 NetworkManager 单例下。因此,你可以直接通过以下方式访问它:

var sceneManager = NetworkManager.Singleton.SceneManager;
  • 1

接下来,现在只有一个场景事件:OnSceneEvent。你可以订阅它,从SceneEvent类中获取场景事件的信息。在该类中,你将找到SceneEventType,它会告诉你来自场景管理器的事件类型的详细信息。最后,用于在场景之间切换的主要函数已更改以匹配Unity场景管理器。现在,你需要使用两个参数调用LoadScene函数:场景名称和LoadSceneMode,这是Unity中加载场景的标准方式。而不是使用SwitchScene函数。

NetworkBehavior 的变化

NetworkBehavior 有两个主要的变化。首先,NetworkStart 方法变成了 OnNetworkSpawn,我们引入了 OnNetworkDespawn 来保持对称。其次,现在需要重写 OnDestroy 方法,因为 NetworkBehavior 已经在使用它。

行为变化

我们尽量将行为变化降到最低,但其中两个变化可能会导致你的脚本出现错误。首先,当应用程序退出时,NetworkManager 现在会自行关闭连接。如果你自己实现了关闭连接的操作,你将会得到一个错误,提示你尝试了两次断开连接。其次,库现在会在 OnNetworkSpawn(之前称为 NetworkStart)方法返回后触发所有 NetworkVariable 的 OnValueChanged 事件。你需要相应地重构依赖于这个顺序的脚本。

升级 RPCs

Netcode 版本的 RPCs 调用方式发生了变化。请阅读我们关于 RPC 的新文档,并使用新系统替换你现有的 RPCs。

序列化

我们用新的 INetworkSerializable 接口替换了旧的 INetworkSerializable 接口。这个接口工作方式有些不同。详见 INetworkSerializable

页面还提供了关于嵌套序列化类型的信息。

SyncVars

在 Netcode 中,SyncVars 已经被移除。将你现有的 SyncVars 转换为 NetworkVariables

移除 Patcher Package

在升级项目完成后,你可以在 Unity 包管理器中移除 Netcode Patcher 包,因为它不再需要。

故障排除

错误:找不到类型或命名空间名为 ‘MLAPI’

如果你的项目使用了程序集定义(.asmdef)文件,在切换到包版本后,你的程序集定义文件需要引用 com.unity.multiplayer.mlapi.runtime。

错误:找不到类型或命名空间名为 ‘NetworkedBehaviour’

如果在控制台中得到这样的错误消息(或者与 NetworkedBehaviour 不同的其他 Netcode 类型),那很可能是因为你的代码使用了过时的 API。打开错误消息中指示的脚本,并将所有 API 更新为新的命名。

错误:SerializedObjectNotCreatableException:索引 0 处的对象为空

如果每次进入游玩模式或保存场景时出现这种情况,关闭 Unity 编辑器,然后重新打开,问题应该就解决了。

下一步

在迁移和更新至 Netcode 包后,我们建议考虑以下事项:

最后更新时间为2023年2月2日

将Unity UNet迁移到Netcode for GameObjects

使用这个逐步指南来将你的项目从Unity UNet迁移到Netcode for GameObjects(Netcode)。如果需要帮助,请在Unity Multiplayer Networking Discord上与我们联系。

UNet已弃用

UNet是一个完全被弃用的产品,你应该尽快升级到Netcode for GameObjects。

当前限制事项

请查看以下关于从先前版本的Unity UNet迁移到Netcode的限制事项:

  • 命名约束可能会引起问题。UNet的方法以CmdRpc为前缀,而Netcode要求后缀。这可能需要复杂的多行正则表达式来查找和替换,或手动更新。例如,CommandAttribute已更名为ServerRpcAttributeClientRPCAttribute已更名为ClientRpcAttribute
  • 在IDE中无法显示RPC后缀命名模式的错误。
  • 客户端和服务器在UNet中有不同的表示形式。UNet包含一些在Netcode中不存在的回调函数。
  • 需要将预制体添加到Netcode的预制体注册列表中。
  • Netcode目前不支持匹配功能。

备份你的项目

在进行迁移之前,建议你备份你的项目。例如:

  • 创建你整个项目文件夹的副本。
  • 使用类似Git的源代码版本控制软件。

最佳实践

建议同时使用这两种方法备份你的项目。这将为你提供一个复制的项目,并通过提交的更改和历史进行跟踪。

安装Netcode并重新启动Unity

请参考Netcode安装指南获取更多信息。

注意
如果你第一次安装Git,你需要重新启动系统。

RPC调用

调用RPC的方式与UNet相同。只需调用函数,它将发送一个RPC。

将NetworkIdentity替换为NetworkObject

在Netcode中,UNet的NetworkIdentity被称为NetworkObject,工作方式类似。

将UNet的NetworkTransform替换为Netcode的NetworkTransform

在Netcode中,UNet的NetworkTransform也被称为NetworkTransform,工作方式类似。

NetworkTransform与UNET的NetworkTransform在功能上不完全对等。它缺少诸如刚体的位置同步等功能。

将UNet的NetworkAnimator替换为Netcode的NetworkAnimator

在项目的各处,将UNet的NetworkAnimator替换为Netcode的NetworkAnimator组件。

更新NetworkBehaviour

在项目的所有地方将UNet的NetworkBehaviour替换为Netcode的NetworkBehaviour

UNet示例
public class MyUnetClass : NetworkBehaviour
{
    [SyncVar]
    public float MySyncFloat;
    public void Start()
    {
        if (isClient)
        {
            CmdExample(10f);
        }
        else if (isServer)
        {
            RpcExample(10f);
        }
    }
    [Command]
    public void CmdExample(float x)
    {
        Debug.Log(“Runs on server”);
    }
    [ClientRpc]
    public void RpcExample(float x)
    {
        Debug.Log(“Runs on clients”);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
Netcode for GameObjects示例项目
public class MyNetcodeExample : NetworkBehaviour
{
    public NetworkVariable<float> MyNetworkVariable = new NetworkVariable<float>();
    public override void OnNetworkSpawn()
    {
        ExampleClientRpc(10f);
        ExampleServerRpc(10f);
    }
    [ServerRpc]
    public void ExampleServerRpc(float x)
    {
        Debug.Log(“Runs on server”);
    }
    [ClientRpc]
    public void ExampleClientRpc(float x)
    {
        Debug.Log(“Runs on clients”);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

查看NetworkBehaviour获取更多信息。

将SyncVar替换

SyncVar替换为NetworkVariable,并在你的项目的所有地方使用。

为了实现与SyncVar钩子相等的功能,请将一个函数订阅到NetworkVariableOnValueChanged回调中。UNet钩子和Netcode的OnValueChanged回调之间的一个明显区别是,Netcode会给你旧值和新值,而UNet只给你旧值。在UNet中,你还需要手动分配SyncVar的值。

UNET示例
public class SpaceShip : NetworkBehaviour
{
    [SyncVar]
    public string PlayerName;


    [SyncVar(hook = "OnChangeHealth"))]
    public int Health = 42;

    void OnChangeHealth(int newHealth){
        Health = newHealth; //在 Netcode 中不再需要这一行代码。
        Debug.Log($"我的新health是 {newHealth}.");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
Netcode for GameObjects示例
// 不要忘记使用一个初始值初始化 NetworkVariable。
public NetworkVariable<string> PlayerName = new NetworkVariable<string>();

public NetworkVariable<int> Health = new NetworkVariable<int>(42);

// 这是如何更新 NetworkVariable 的值的方法,你也可以使用 .Value 来访问 NetworkVariable 的当前值。
void MyUpdate()
{
    Health.Value += 30;
}


void Awake()
{
  //  在 Awake 或 Start 中调用此方法来订阅 NetworkVariable 的更改。
    Health.OnValueChanged += OnChangeHealth;
}

void OnChangeHealth(int oldHealth, int newHealth){
    //现在不再需要手动赋值给变量,Netcode 会自动为你完成。
    Debug.Log($"我的新health是 {newHealth}. 之前我的health是 {oldHealth}");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在你的项目中替换所有使用SyncVar的后缀递增和递减操作。Netcode的NetworkVariable.Value公开了一个值类型,这就是为什么不支持后缀递增/递减的原因。

UNET示例

public int Health = 42;

public void Update(){
  Health++;
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
Netcode for GameObjects示例项目

public NetworkVariable<int> Health = new NetworkVariable<int>(42);

public void Update(){
  Health.Value = Health.Value + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

请参考NetworkVariable获取更多信息。

将SyncList替换为NetworkList

请在你的项目中的所有地方将SyncList替换为NetworkListNetworkList具有一个OnListChanged事件,它类似于UNet的回调

UNET 示例
public SyncListInt m_ints = new SyncListInt();

private void OnIntChanged(SyncListInt.Operation op, int index)
{
    Debug.Log("列表更改 " + op);
}


public override void OnStartClient()
{
    m_ints.Callback = OnIntChanged;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
Netcode for GameObjects 示例
NetworkList<int> m_ints = new NetworkList<int>();

// 在 Awake 或 Start 中调用此方法来订阅 NetworkList 的更改。
void ListenChanges()
{
    m_ints.OnListChanged += OnIntChanged;
}

// NetworkListEvent 包含关于操作和更改的索引的信息。
void OnIntChanged(NetworkListEvent<int> changeEvent)
{

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

替换Command/ClientRPC

UNet的Command/ClientRPC在Netcode中被替换成了Server/ClientRpc,其工作方式类似。

UNET 示例
    [Command]
    public void CmdExample(float x)
    {
        Debug.Log(“Runs on server”);
    }
    [ClientRpc]
    public void RpcExample(float x)
    {
        Debug.Log(“Runs on clients”);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
Netcode for GameObjects 示例
    [ServerRPC]
    public void ExampleServerRpc(float x)
    {
        Debug.Log(“Runs on server”);
    }
    [ClientRPC]
    public void ExampleClientRpc(float x)
    {
        Debug.Log(“Runs on clients”);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意
在Netcode中,RPC函数的名称必须以ClientRpc/ServerRpc后缀结尾。

请查看消息系统以获取更多信息。

替换OnServerAddPlayer

在你的项目中,将每个地方的OnServerAddPlayer替换为ConnectionApproval

UNET 示例
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.NetworkSystem;

class MyManager : NetworkManager
{
    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)
    {
        if (extraMessageReader != null)
        {
            var s = extraMessageReader.ReadMessage<StringMessage>();
            Debug.Log("我的名字是" + s.value);
        }
        OnServerAddPlayer(conn, playerControllerId, extraMessageReader);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
Netcode for GameObjects 示例

仅服务器示例:

using Unity.Netcode;

private void Setup() 
{
    NetworkManager.Singleton.ConnectionApprovalCallback += ApprovalCheck;
    NetworkManager.Singleton.StartHost();
}

private void ApprovalCheck(byte[] connectionData, ulong clientId, NetworkManager.ConnectionApprovedDelegate callback)
{
    // 在这里编写你的逻辑
    bool approve = true;
    bool createPlayerObject = true;

    // 预制体哈希。使用null以使用默认玩家预制体
    // 如果使用这个哈希,请将"MyPrefabHashGenerator"替换为添加到场景中NetworkManager对象的NetworkPrefabs字段的预制体的名称
    ulong? prefabHash = NetworkpawnManager.GetPrefabHashFromGenerator("MyPrefabHashGenerator");
    
    //如果approve为true,则添加连接。如果为false,则断开客户端连接
    callback(createPlayerObject, prefabHash, approve, positionToSpawnAt, rotationToSpawnWith);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

请查看连接批准以获得更多信息。

将 NetworkServer.Spawn 替换为 NetworkObject.Spawn

将项目中的所有NetworkServer.Spawn替换为NetworkObject.Spawn。

UNET 示例

using UnityEngine;
using UnityEngine.Networking;

public class Example : NetworkBehaviour
{
    //在Inspector面板中分配预制体
    public GameObject m_MyGameObject;
    GameObject m_MyInstantiated;

    void Start()
    {
        //实例化预制体
        m_MyInstantiated = Instantiate(m_MyGameObject);
        //生成在Inspector面板中分配的游戏对象
        NetworkServer.Spawn(m_MyInstantiated);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
Netcode for GameObjects 示例
GameObject go = Instantiate(myPrefab, Vector3.zero, Quaternion.identity);
go.GetComponent<NetworkObject>().Spawn();
  • 1
  • 2

请查看对象生成以获取更多信息。

自定义生成处理程序

Netcode具有自定义生成处理程序,用于替代UNet的自定义生成函数。请查看对象池以获取更多信息。

替换 NetworkContextProperties

Netcode拥有 IsLocalPlayerIsClientIsServerIsHost 来替代 UNet 的 isLocalPlayerisClientisServer。在 Netcode 中,每个对象都可以由特定的对等方拥有。可以使用 IsOwner 来检查这一点,它类似于 UNet 的 hasAuthority

网络接近性检查器 / 使用 Netcode 可见性的 OnCheckObserver

Netcode 中没有 NetworkPromimityChecker UNet 组件的直接等效组件。对于客户端的网络可见性与 UNet 类似。Netcode 没有 UNet 中 ObjectHide 消息的等效物。在 Netcode 中,主机上的网络对象始终可见。Netcode 没有 UNet 中 OnSetLocalVisibility 函数的等效函数。通过使用 NetworkObject.CheckObjectVisibility,可以将使用 OnCheckObserver 的手动网络接近性实现移植到 Netcode 中。对于 Netcode 的可见性系统,不需要 OnRebuildObservers

UNET 示例
public override bool OnCheckObserver(NetworkConnection conn)
{
 return IsvisibleToPlayer(getComponent<NetworkIdentity>(), coon);
}

public bool IsVisibleToPlayer(NetworkIdentity identity, NetworkConnection conn){
    // 任何接近性函数。
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
Netcode for GameObjects 示例
public void Start(){
    NetworkObject.CheckObjectVisibility = ((clientId) => {
        return IsVisibleToPlayer(NetworkObject, NetworkManager.Singleton.ConnectedClients[clientId]);
    });
}

public bool IsVisibleToPlayer(NetworkObject networkObject, NetworkClient client){
    // 任何接近性函数。
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

请参阅 Object Visibility 以了解更多关于 Netcode 网络可见性检查的信息。

更新场景管理

在 Netcode 中,场景管理不像 UNet 那样通过 NetworkManager 完成。NetworkSceneManager 提供了相同的功能,用于切换场景。

UNET 示例
public void ChangeScene()
{
    MyNetworkManager.ServerChangeScene("MyNewScene");
}
  • 1
  • 2
  • 3
  • 4
Netcode for GameObjects 示例
public void ChangeScene()
{
    NetworkSceneManager.LoadScene("MyNewScene", LoadSceneMode.Single);
}
  • 1
  • 2
  • 3
  • 4

更新 ClientAttribute/ClientCallbackAttribute 和 ServerAttribute/ServerCallbackAttribute

目前,Netcode 没有提供使用属性标记函数只在服务器或客户端上运行的替代方案。你可以手动在函数中返回以实现这个功能。

UNET 示例
[Client]
public void MyClientOnlyFunction()
{
    Debug.Log("我是客户端!");
}
  • 1
  • 2
  • 3
  • 4
  • 5
Netcode for GameObjects 示例
public void MyClientOnlyFunction()
{
    if (!IsClient) { return; }

    Debug.Log("我是客户端!");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

用 RPC 事件替代 SyncEvent

Netcode 没有为 SyncEvent 提供等效的功能。要将 SyncEvent 从 UNet 移植到 Netcode,可以发送一个 RPC 来在另一端触发事件。

UNET 示例
public class DamageClass : NetworkBehaviour
{
    public delegate void TakeDamageDelegate(int amount);

    [SyncEvent]
    public event TakeDamageDelegate EventTakeDamage;

    [Command]
    public void CmdTakeDamage(int val)
    {
        EventTakeDamage(val);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
Netcode for GameObjects 示例
public class DamageClass : NetworkBehaviour
{
    public delegate void TakeDamageDelegate(int amount);

    public event TakeDamageDelegate EventTakeDamage;

    [ServerRpc]
    public void TakeDamageServerRpc(int val)
    {
        EventTakeDamage(val);
        OnTakeDamageClientRpc(val);
    }

    [ClientRpc]
    public void OnTakeDamageClientRpc(int val){
        EventTakeDamage(val);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

网络发现

Netcode 并没有提供网络发现功能。贡献库提供了一个用于网络发现的示例实现。

下一步

在迁移并更新到 Netcode 包之后,我们建议你参考以下内容:

最后更新于2023年2月1日

开始使用NGO(Get started with NGO)

使用本指南学习如何创建你的第一个NGO项目。它将引导你创建一个简单的Hello World项目,实现Netcode for GameObjects(NGO)的基本功能。

参考命令行测试助手(Testing the command line helper),了解如何使用命令行助手测试你的程序。

先决条件

在开始之前,请确保具备以下先决条件:

  • 一个有效授权的活跃Unity账户。
  • Unity Hub
  • 一个支持NGO的Unity编辑器版本。请参考NGO要求

在继续之前,请使用Unity编辑器版本2021.3或更高版本创建一个新项目。

提示
如果你还没有Assets / Scripts/文件夹,请现在创建一个:

  1. Projects选项卡中右键单击Assets文件夹,然后选择Create > Folder
  2. 将新文件夹命名为Scripts

这里是你将保存所有脚本的地方。

安装Netcode for GameObjects

请参考安装Netcode for GameObjects

添加基本组件

本部分指导你添加网络游戏的基本组件:

创建NetworkManager组件

本部分指导你创建一个NetworkManager组件。

首先,创建NetworkManager组件:

  1. Hierarchy选项卡中右键单击,然后选择Create Empty来创建一个空的GameObject。
    请添加图片描述
  2. 将空的 GameObject 重命名为 NetworkManager。

请添加图片描述
3. 选择 NetworkManager,然后在 Inspector 选项卡中选择Add Component

请添加图片描述
4. 从组件列表中选择 Netcode > NetworkManager
请添加图片描述
5. 在 Inspector 选项卡中,找到 Unity Transport 部分,然后选择 UnityTransport 作为协议类型(Protocol type)。

请添加图片描述
请添加图片描述
6. 按下 Ctrl/Cmd + S 保存场景(或选择File > Save)。

为每个连接的玩家创建一个要生成的角色物体

提示信息
当你将预制体放入 PlayerPrefab槽时,你是在告诉库当客户端连接到游戏时,它会自动将该预制体生成为连接客户端的角色。如果你没有设置任何预制体作为 PlayerPrefab,NGO将不会生成玩家对象。请参考玩家对象

本节指导你创建一个为每个连接的玩家生成的对象。

  1. 在 Unity 编辑器中,在Hierarchy(层级)选项卡中右键单击,然后选择3D Object(3D 对象) > Capsule (胶囊)。
  2. 将胶囊对象命名为 Player。
  3. 选中 Player,通过在 Inspector 选项卡中选择Add Component(添加组件) > Netcode > NetworkObject 添加一个 NetworkObject 组件。
  4. Project(项目)选项卡下的Assets(资产)文件夹中右键单击,然后选择Create(创建) > Folder(文件夹)。
  5. 将文件夹命名为 Prefabs
  6. 通过将其从Hierarchy选项卡拖动到 Prefabs 文件夹中,将之前创建的 Player 对象变成一个预制体。

请添加图片描述

  1. 通过在scene选项卡中选择玩家胶囊体,然后按下Delete键(或 macOS 上的 Cmd + Delete键)来从场景中删除玩家。

提示
你可以从场景中删除玩家游戏对象,因为你在 NetworkManager 组件的 Player prefab属性中分配了这个网络预制体。该库不支持将玩家对象定义为在场景中放置的 NetworkObject。

  1. 选择 NetworkManager
  2. Inspector 选项卡中,找到 PlayerPrefab 字段。
    请添加图片描述
  3. Project选项卡中将Player预制体拖动到你在 Inspector 选项卡中创建的 PlayerPrefab 槽中。
    请添加图片描述
  4. 通过在 Hierarchy 选项卡中右键单击,然后选择 3D Object > Plane,向场景中添加一个3D平面(位于0,0,0处)。

注意

添加平面可以提供一个可视化的参考点来显示玩家预制体的位置,但这并不是必需的。

请添加图片描述

  1. 按下 Ctrl/Cmd + S(选择File > Save)来保存场景。

将你的场景添加到构建(build)中

本节指导你将你的场景添加到构建中。

启用 NetworkManager场景管理设置允许服务器控制哪些场景加载给客户端。然而,你必须将当前场景添加到构建中以进入Play模式。

注意
默认情况下,NetworkManager 的场景管理选项已启用。

  1. 通过选择File > Build Settings打开构建设置窗口。
  2. 选择Add Open Scenes(添加打开的场景)。

Scenes/SampleScene 会列在构建的场景中。你可以关闭构建设置窗口。

添加 RPCs

本节指导你向项目中添加基本的RPCs。

提示
如果你还没有 Assets/Scripts/ 文件夹,现在创建一个:

  1. Projects选项卡中右键单击 Assets 文件夹,然后选择Create > Folder(创建 > 文件夹)。
  2. 将新文件夹命名为 Scripts
    这里将是你保存所有脚本的地方。

创建一个名为 RpcTest.cs 的脚本:

  1. Project选项卡中选择 Assets > Scripts
  2. Scripts 文件夹中右键单击,然后选择Create > C# Script
  3. 将脚本命名为 RpcTest

RpcTest.cs 脚本添加到玩家预制体中:

  1. Assets > Prefabs 中选择Player预制体。
  2. Inspector 选项卡中(选择了玩家预制体),选择Add Component
  3. 选择 Scripts > Rpc Test

编辑 RpcTest.cs 脚本:

  1. Project 选项卡中选择 Assets > Scripts > RpcTest
  2. Inspector 选项卡中(选择了脚本),选择Open。这会在默认的本地文本编辑器中打开此脚本。
  3. 编辑 RpcTest.cs 脚本,使其与以下内容匹配:
using Unity.Netcode;
using UnityEngine;

public class RpcTest : NetworkBehaviour
{
    public override void OnNetworkSpawn()
    {
        if (!IsServer && IsOwner) //只在拥有此 NetworkBehaviour 实例的 NetworkObject 的客户端向服务器发送 RPC
        {
            TestServerRpc(0, NetworkObjectId);
        }
    }

    [ClientRpc]
    void TestClientRpc(int value, ulong sourceNetworkObjectId)
    {
        Debug.Log($"客户端接收到 RPC #{value} on NetworkObject #{sourceNetworkObjectId}");
        if (IsOwner) //只在拥有此 NetworkBehaviour 实例的 NetworkObject 的客户端向服务器发送 RPC
        {
            TestServerRpc(value + 1, sourceNetworkObjectId);
        }
    }

    [ServerRpc]
    void TestServerRpc(int value, ulong sourceNetworkObjectId)
    {
        Debug.Log($"服务器接收到 RPC #{value} on NetworkObject #{sourceNetworkObjectId}");
        TestClientRpc(value, sourceNetworkObjectId);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  1. 按下Ctrl/Cmd + S保存场景(或选择File > Save)。

测试RPCs

本节将指导你测试前一节中添加的RPCs。

  1. 选择File > Build And Run
  2. 关闭运行的游戏。
  3. 在终端中同时启动客户端和服务器,如命令行测试助手中所示。

提示
你可以使用多人游戏模式(Multiplayer Play Mode,MPPM)软件包,而不是使用命令行辅助脚本,它允许你运行多个Unity编辑器实例以测试多人游戏功能。请参阅多人游戏模式以了解更多信息。
注意:MPPM仅支持Unity编辑器版本2023.1及更高版本。

在客户端和服务器生成后,在客户端和服务器的控制台中会显示彼此发送RPC消息的日志。

客户端在其OnNetworkSpawn调用中首次发起交换,计数器值为0。然后它使用下一个值向服务器发起RPC调用。服务器接收到此调用并调用客户端。控制台分别显示服务器和客户端的以下内容。

Server Received the RPC #0 on NetworkObject #1
Server Received the RPC #1 on NetworkObject #1
Server Received the RPC #2 on NetworkObject #1
Server Received the RPC #3 on NetworkObject #1
...
  • 1
  • 2
  • 3
  • 4
  • 5
Client Received the RPC #0 on NetworkObject #1
Client Received the RPC #1 on NetworkObject #1
Client Received the RPC #2 on NetworkObject #1
Client Received the RPC #3 on NetworkObject #1
...
  • 1
  • 2
  • 3
  • 4
  • 5

只有拥有 RpcTest 脚本的 NetworkObject 的客户端才会在服务器上发送RPCs,但它们都会从服务器接收RPCs。这意味着如果你使用多个客户端进行测试,控制台将记录每个迭代中服务器和所有客户端接收到的每个 NetworkObject 的RPCs。如果使用主机和客户端进行测试,你将在主机的控制台上看到以下内容。这是因为作为服务器,它会接收到其他客户端的服务器RPCs,并且作为客户端,它也会接收到自己的客户端RPCs。

Server Received the RPC #0 on NetworkObject #2
Client Received the RPC #0 on NetworkObject #2
Server Received the RPC #1 on NetworkObject #2
Client Received the RPC #1 on NetworkObject #2
Server Received the RPC #2 on NetworkObject #2
Client Received the RPC #2 on NetworkObject #2
Server Received the RPC #3 on NetworkObject #2
Client Received the RPC #3 on NetworkObject #2
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意
这里的 NetworkObjectId2,因为主机也有一个具有 RpcTest 脚本的 NetworkObject 生成,但它不会发送启动链的初始RPC,因为它是服务器。

通过脚本扩展功能

本节将展示如何使用两个脚本:HelloWorldPlayer.csHelloWorldManager.cs扩展Hello World项目的功能。

HelloWorldManager.cs脚本
  1. Scripts文件夹中创建一个名为HelloWorldManager.cs的新脚本。
  2. 在场景中创建一个新的空对象HelloWorldManager,并将脚本附加为其组件。
  3. 将以下代码复制到HelloWorldManager.cs脚本中:
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();
                }
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  1. 继续阅读以理解示例代码的含义:

在之前的 Hello World 项目中,你通过添加预先创建的 NetworkManager 组件来创建了一个 NetworkManager。该组件允许你在 Play 模式下通过检查组件来启动 Host、Client 或 Server。HelloWorldManager.cs 脚本在进入 Play 模式时创建了一个在屏幕上显示的按钮菜单,这可以简化操作。

提示

  • Host 启动服务器并以客户端的形式加入。
  • Client 以客户端玩家的形式加入服务器。
  • Server 以服务器的形式启动游戏,不会实例化玩家角色。

HelloWorldManager.cs 脚本通过 StartButtons() 方法实现了这个菜单功能。在选择按钮后,StatusLabels() 方法会在屏幕上添加一个标签,显示选择的模式。当测试你的多人游戏时,这有助于区分不同的游戏视图窗口。

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);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如前面的代码片段中所示,HelloWorldManager.cs 脚本还通过其单例使用 NetworkManager 的实例来获取属性,例如 IsClientIsServerIsLocalClientIsClientIsServer 属性指示了已建立的连接状态。

HelloWorldManager.cs 脚本引入了一个名为 SubmitNewPosition() 的新方法,HelloWorldPlayer 脚本使用该方法创建一个简单的 RPC 调用

:::

HelloWorldPlayer.cs 脚本

提示:
如果你还没有 Assets/Scripts/ 文件夹,请现在创建一个:

  1. Projects 选项卡中,右键单击 Assets 文件夹,然后选择 Create > Folder
  2. 将新文件夹命名为 Scripts

这是你将保存所有脚本的位置。

  1. Scripts 文件夹中创建一个名为 HelloWorldPlayer.cs 的新脚本。
  2. 将该脚本作为组件添加到你的 Player 预制体中。
  3. 将下面的代码复制到 HelloWorldPlayer.cs 脚本中:
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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  1. 继续阅读以理解代码的含义:

HelloWorldPlayer.cs 脚本为 Hello World 项目的玩家角色添加了一些基本的移动功能。服务器玩家和客户端玩家都可以开始移动。然而,移动是通过服务器的 position NetworkVariable 进行的,这意味着服务器玩家可以立即移动,但客户端玩家必须向服务器请求移动,等待服务器更新 position NetworkVariable,然后在本地复制变化。

HelloWorldPlayer 类继承自 Unity.NetcodeNetworkBehaviour,而不是 MonoBehaviour。这允许你自定义网络代码,并在玩家生成时对发生的事件进行覆盖。

public class HelloWorldPlayer : NetworkBehaviour
  • 1

对于多人游戏,每个对象都在至少两台机器上运行:玩家一和玩家二。因此,你需要确保两台机器具有相同的行为,并且对对象的信息是正确的。其中一个要考虑的情况是理解玩家如何移动。只有一个玩家能控制玩家对象的移动。以下代码通过验证正在运行代码的机器是否是玩家的所有者来强制执行这一点。

public override void OnNetworkSpawn()
 {
     if (IsOwner)
     {
         Move();
     }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

任何实现了 NetworkBehaviour 组件的 MonoBehaviour 都可以重写 OnNetworkSpawn() 方法。OnNetworkSpawn() 方法会在 NetworkObject 生成后触发。HelloWorldPlayer 类重写了 OnNetworkSpawn 方法,因为客户端和服务器运行不同的逻辑。

注意
你可以在任何 NetworkBehaviour 组件上重写此行为。

由于服务器和客户端可以是同一台机器,并且玩家的所有者(也称为主机)可能不同,你希望进一步区分这两者,并为每个角色设置不同的移动行为。

如果当前玩家是服务器,代码会确定一个随机的位置来生成玩家。如果当前玩家是客户端,则无法找到生成位置,你必须从服务器获取。

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();
}

void Update()
{
    transform.position = Position.Value;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
HelloWorldPlayer.cs 脚本添加到 Player 预制体中:

本部分将指导你将 HelloWorldPlayer.cs 脚本添加到 Player 预制体中。

选择 Player 预制体:

  1. Project选项卡中选择 Assets > Prefabs
  2. 选择 Player

HelloWorldPlayer.cs 脚本添加到 Player 预制体作为一个组件:

  1. 选中 Player 预制体后,在 Inspector 选项卡中选择Add Component
  2. 选择 Scripts > Hello World > Hello World Player
关于 HelloWorldPlayer 脚本

HelloWorldPlayer 类继承自 NetworkBehaviour,而不是 MonoBehaviour

public class HelloWorldPlayer : NetworkBehaviour
  • 1

HelloWorldPlayer 类定义了一个 NetworkVariable 来表示玩家的网络位置。

public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>();
  • 1

HelloWorldPlayer 类覆盖了 OnNetworkSpawn 方法。

       public override void OnNetworkSpawn()
        {
            if (IsOwner)
            {
                Move();
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

该脚本在玩家的客户端和服务器实例上调用 Move() 方法。Move() 方法执行以下操作:

       public void Move()
        {
            if (NetworkManager.Singleton.IsServer)
            {
                var randomPosition = GetRandomPositionOnPlane();
                transform.position = randomPosition;
                Position.Value = randomPosition;
            }
            else
            {
                SubmitPositionRequestServerRpc();
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

添加简单的 RPC 使用(Add simple RPC use)

本部分将为你详细介绍如何添加一个简单的 RPC 示例代码。

如果玩家是在 OnNetworkSpawn() 中由服务器拥有的玩家,你可以立即移动该玩家,如下面的示例代码所示。

           if (NetworkManager.Singleton.IsServer)
            {
                var randomPosition = GetRandomPositionOnPlane();
                transform.position = randomPosition;
                Position.Value = randomPosition;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果玩家是客户端,脚本会调用一个 ServerRpc。客户端可以调用 ServerRpc 在服务器上执行操作。

           else
            {
                SubmitPositionRequestServerRpc();
            }
  • 1
  • 2
  • 3
  • 4

ServerRpc 在服务器上的玩家实例中设置位置 NetworkVariable,只是在平面上随机选择了一个点。

       [ServerRpc]
        void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
        {
            Position.Value = GetRandomPositionOnPlane();
        }
  • 1
  • 2
  • 3
  • 4
  • 5

玩家的服务器实例通过 ServerRpc 修改了Position NetworkVariable。如果玩家是客户端,它必须在 Update 循环中本地应用这个位置。

       void Update()
        {
            transform.position = Position.Value;
        }
  • 1
  • 2
  • 3
  • 4

因为 HelloWorldPlayer.cs 脚本处理位置 NetworkVariable,所以 HelloWorldManager.cs 脚本可以定义 SubmitNewPosition() 的内容。

       static void SubmitNewPosition()
        {
            if (GUILayout.Button(NetworkManager.Singleton.IsServer ? "Move" : "Request Position Change"))
            {
                var playerObject = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject();
                var player = playerObject.GetComponent<HelloWorldPlayer>();
                player.Move();
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的代码块中的方法会添加一个上下文按钮,该按钮的行为取决于客户端是服务器还是客户端。当你按下这个方法创建的按钮时,它会找到你的本地玩家并调用 Move() 方法。

现在,你可以创建一个演示上述概念的构建。

创建两个构建实例:一个用于主机,另一个用于客户端(加入主机的游戏)。

这两个构建实例都可以通过 GUI 按钮移动玩家。服务器会立即移动玩家并在客户端上复制移动。

客户端可以请求一个新的位置,指示服务器更改该实例的位置 NetworkVariable。服务器更新位置 NetworkVariable 后,客户端会在其 Update() 方法中应用该 NetworkVariable 位置。

添加一个 NetworkTransform(Add a NetworkTransform)

本节指导你如何添加一个移动玩家的 NetworkTransform 组件。

将 NetworkTransform 组件添加到 Player 预制体中:

  1. 在Assets> Prefabs中选择 Player 预制体。
  2. Inspector 选项卡中(选择 Player 预制体),选择添加组件(Add Component)。
  3. 选择 Netcode > NetworkTransform

创建一个名为 NetworkTransformTest.cs 的脚本。

  1. Project选项卡中,转到Assets> Scripts
  2. 右键单击,然后选择Create> C# Script
  3. 将其命名为 NetworkTransformTest

NetworkTransformTest 脚本添加到 Player 预制体中:

  1. Assets> Prefabs中选择 Player 预制体。
  2. Inspector 选项卡中(选择 Player 预制体),选择Add Component
  3. 选择 Scripts > Network Transform Test

编辑 NetworkTransformTest.cs 脚本:

  1. Project选项卡中,选择Assets> Scripts> NetworkTransformTest
  2. Inspector 选项卡中(选择脚本),选择Open。这将在默认的本地文本编辑器中打开此脚本。
  3. 编辑 NetworkTransformTest.cs 脚本以匹配以下内容:
using System;
using Unity.Netcode;
using UnityEngine;

public class NetworkTransformTest : NetworkBehaviour
{
    void Update()
    {
        if (IsServer)
        {
            float theta = Time.frameCount / 10.0f;
            transform.position = new Vector3((float) Math.Cos(theta), 0.0f, (float) Math.Sin(theta));
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  1. 按下 Ctrl/Cmd + S 保存场景(或选择File > Save)。

测试 NetworkTransform

本节将指导你测试你在之前添加的 NetworkTransform。

  1. 选择 File > Build And Run
  2. 关闭游戏。
  3. 在终端中同时启动客户端和服务器,如在测试命令行助手中所示。

在客户端和服务器生成后,玩家胶囊在客户端和服务器上以圆形移动。

测试 Hello World(Test Hello World)

要检查是否一切按预期工作,请尝试在 Unity 编辑器中启动主机。主机同时扮演服务器和客户端的角色。

你可以通过 Unity 编辑器或命令行助手来测试你的 Hello World 项目。如果选择后者,请参考创建命令行助手。否则,请按照以下指示通过 Unity 编辑器进行测试。直到第一个客户端连接之前,只有平面出现在服务器上。然后,NGO 为每个连接的客户端生成一个新的 Player 预制体;但在游戏视图中它们会重叠。

  1. 在 Unity 编辑器顶部选择Play 开始场景。
    请添加图片描述
  2. Hierarchy列表中选择 NetworkManager
    请添加图片描述
  3. 在选择的 NetworkManager 上(在 Hierarchy 选项卡中),从 Inspector 选项卡中选择 Start Host。或者,你可以使用游戏内的 GUI 按钮。
    请添加图片描述
    如果工作正常,Stop Host的选项将显示在 Inspector 选项卡中。

最后更新于 2023 年 9 月 15 日

网络组件(Networking Components)

网络管理器(NetworkManager)

NetworkManager 是一个必需的 Netcode for GameObjectsNetcode)组件,它包含了你项目中所有与网络代码相关的设置。将其视为你的网络代码启用项目的“中央网络代码中心”。

NetworkManager在Inspector面板的属性(NetworkManager Inspector Properties)

  • LogLevel(日志级别):设置网络日志级别
  • PlayerPrefab:当指定预制体时,该预制体将作为玩家对象实例化,并分配给新连接和授权的客户端。
  • NetworkPrefabs:在这里注册你的网络预设。你还可以为每个已注册的网络预设创建一个单独的网络预设覆盖。
  • Protocol Version(协议版本):设置该值以帮助区分构建版本,当前最新版本具有可能导致旧版本连接问题的新资源时,这里进行区分。
  • Network Transport(网络传输):设置你的网络特定设置和传输类型的位置。该字段接受任何INetworkTransport的实现。不过,除非你有特殊的传输需求,否则UnityTransport是与Netcode for GameObjects一起使用的推荐传输方式。
  • Tick Rate:此值控制网络tick更新速率。
  • Ensure Network Variable Length Safety(确保网络变量长度安全):(增加CPU处理和带宽)当勾选此属性时,Netcode for GameObjects将防止用户代码写入超出NetworkVariable边界的部分。
  • Connection Approval(连接批准):当勾选此项并分配NetworkManager.ConnectionApprovalCallback时,启用连接批准
  • Client Connection Buffer Timeout(客户端连接缓冲超时):该值设置连接客户端完成连接批准过程所需的时间。如果超过指定的时间,连接的客户端将被断开。
  • Force Same Prefabs(强制相同预设):勾选后,将始终验证连接的客户端是否具有与服务器相同的已注册网络预设。如果未勾选,Netcode for GameObjects将忽略任何差异。
  • Recycle Network Ids(回收网络Ids):勾选后,在指定的时间段后将重新使用先前分配的NetworkObject.NetworkObjectIds
  • Network Id Recycle Delay(网络Id回收延迟):之前分配但当前未分配的标识符(identifier)可供使用所需的时间。
  • Enable Scene Management(启用场景管理):勾选后,Netcode for GameObjects将处理场景管理和客户端同步。如果未勾选,用户将需要创建自己的场景管理脚本并处理客户端同步。
  • Load Scene Time Out(加载场景超时):当勾选了启用场景管理后,该选项指定NetworkSceneManager在加载场景异步进行时等待的时间段,超过这个时间段后NetworkSceneManager将认为加载/卸载场景事件失败/超时。

网络管理器子系统(NetworkManager Sub-Systems)

NetworkManager还包含其他与网络代码相关的管理系统的引用:

注意
所有NetworkManager子系统在NetworkManager启动后实例化(即,NetworkManager.IsListening == true)。一个好的一般"经验法则"是在启动NetworkManager之前不要尝试访问下面的子系统,否则它们尚未初始化。

NetworkManager.PrefabHandler: 提供了访问用于网络对象池和覆盖网络预设的NetworkPrefabHandler的功能。
NetworkManager.SceneManager: 当启用场景管理时,用于加载和卸载场景、注册场景事件和其他与场景管理相关的操作。
NetworkManager.SpawnManager: 处理与网络对象生成相关的功能。
NetworkManager.NetworkTimeSystem: 一个可以用来处理客户端和服务器之间延迟问题的同步时间。
NetworkManager.NetworkTickSystem: 使用此系统来调整更新NetworkVariables的频率。
NetworkManager.CustomMessagingManager: 使用此系统创建和发送自定义消息。

启动服务器、主机或客户端(Starting a Server, Host, or Client)

为了执行任何涉及发送消息的网络代码相关操作,首先你必须启动一个服务器并监听至少一个已连接的客户端(当作为主机运行时,服务器可以向自己发送RPCs)。为了实现这一目标,你首先要将NetworkManager作为服务器、主机或客户端启动。你可以调用三个NetworkManager的方法来实现:

NetworkManager.Singleton.StartServer();      // 将NetworkManager作为仅服务器启动(即无本地客户端)。
NetworkManager.Singleton.StartHost();        // 将NetworkManager作为既是服务器又是客户端启动(即具有本地客户端)。
NetworkManager.Singleton.StartClient();      // 将NetworkManager作为仅客户端启动。   
  • 1
  • 2
  • 3

危险
不要在NetworkBehaviour的Awake方法中启动NetworkManager,因为这可能会导致不良结果,取决于项目的设置!

注意
当作为服务器启动或加入已经启动的会话时,NetworkManager可以生成一个属于客户端的"玩家对象(Player Object)"。
有关玩家预制件的更多信息,请参阅:

连接(Connecting)

当启动客户端时,NetworkManager使用Transport组件中提供的IP和端口进行连接。虽然你可以在编辑器中设置IP地址,但很多时候你可能希望能够在运行时设置IP地址和端口。

下面的示例使用Unity Transport展示了几种通过NetworkManager.Singleton以编程方式配置项目网络设置来访问UnityTransport组件的几种方法:

如果你只是设置IP地址和端口号,你可以使用UnityTransport.SetConnectionData方法:

NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(
    "127.0.0.1",  // IP地址是一个字符串
    (ushort)12345 // 端口号是一个unsigned short类型
);
  • 1
  • 2
  • 3
  • 4

如果你在同一代码块中配置服务器和客户端,并且希望将服务器配置为监听分配给它的所有IP地址,那么你也可以向SetConnectionData方法传递一个“监听地址”的值为“0.0.0.0”,像这样:

NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(
    "127.0.0.1",  // IP地址是一个字符串
    (ushort)12345, // 端口号是一个unsigned short类型
    "0.0.0.0" // 服务器监听地址是一个字符串。
);
  • 1
  • 2
  • 3
  • 4
  • 5

注意
使用IP地址0.0.0.0作为服务器监听地址将使服务器或主机监听分配给本地系统的所有IP地址。如果你在同一系统上测试客户端实例以及一个或多个连接到本地局域网上的其他系统的客户端实例,这将特别有帮助。另一个场景是在开发和调试过程中,你可能有时会在同一系统上测试本地客户端实例,有时会测试在外部系统上运行的客户端实例。

在运行时,可以通过NetworkManager.Singleton.GetComponent<UnityTransport>().ConnectionData访问当前连接数据。这将返回一个ConnectionAddressData结构,保存着这些信息。强烈建议你使用SetConnectionData方法来更新这些信息。

然而,如果你正在使用Unity Relay来处理连接,请不要使用SetConnectionData。主机应调用SetHostRelayData,而客户端应调用SetClientRelayData。通过输入IP/端口号(通过SetConnectionData)试图加入通过Relay托管的游戏将不起作用

关于Netcode for GameObjects Transports的更多信息

断开连接和关闭(Disconnecting and Shutting Down)

断开连接相当简单,但是一旦NetworkManager停止,你将无法使用/访问任何子系统(即NetworkSceneManager),因为它们将不再可用。对于客户端、主机或服务器模式,你只需要调用NetworkManager.Shutdown方法,它将在关闭时断开连接。

信息
当没有网络会话处于活动状态且NetworkManager已关闭时,你将需要使用UnityEngine.SceneManagement加载任何与网络会话无关的场景。

public void Disconnect()
{
    NetworkManager.Singleton.Shutdown();
    // 在这个阶段,我们必须使用UnityEngine的SceneManager切换回到主菜单
    UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

断开客户端连接(仅服务器)(Disconnecting Clients (Server Only))

有时候,由于各种原因,你可能需要在不关闭服务器的情况下断开一个客户端的连接。为了做到这一点,你可以调用NetworkManager.DisconnectClient方法,并将你希望断开连接的客户端的标识符作为唯一参数传递。客户端标识符可以在以下位置找到:

  • 使用客户端标识符作为键,NetworkClient作为值的NetworkManager.ConnectedClients字典。
  • 通过NetworkManager.ConnectedClientsList作为只读的NetworkClients列表。
  • 可以通过NetworkManager.ConnectedClientsIds访问所有连接的客户端标识符的完整列表。
  • 客户端标识符作为参数传递给NetworkManager.OnClientConnected事件的所有订阅者。
  • 玩家的NetworkObject具有NetworkObject.OwnerClientId属性。

提示
获取玩家的主NetworkObject的一种方法是通过NetworkClient.PlayerObject

void DisconnectPlayer(NetworkObject player)
{   
    // 注意:如果客户端调用此方法,将会抛出一个异常。
    NetworkManager.DisconnectClient(player.OwnerClientId);
}
  • 1
  • 2
  • 3
  • 4
  • 5
客户端断开连接通知(Client Disconnection Notifications)

客户端和服务器都可以订阅NetworkManager.OnClientDisconnectCallback事件,以便在客户端断开连接时收到通知。客户端断开连接的通知是“相对”的,取决于接收通知的是哪一方。

有两种常见的“断开连接”类型:
  • 逻辑断开:自定义的服务器端代码(你可能为你的项目编写的代码)会调用NetworkManager.DisconnectClient
    • 例如:主机玩家可能会踢出某个玩家,或者有玩家长时间处于“不活跃状态”而被断开连接。
  • 网络中断:传输层检测到已不再存在有效的网络连接。
当断开连接通知触发时:
  • 客户端会在被服务器断开连接时收到通知。
  • 如果客户端断开连接(即玩家主动退出游戏会话),服务器会收到通知。
  • 当服务器或客户端由于意外原因断开网络连接时(如网络中断),双方都会收到通知。
不会触发断开连接通知的场景:
  • 当服务器“逻辑上”断开客户端时。
    • 原因:此时服务器已经知道客户端已断开连接。
  • 当客户端“逻辑上”自行断开连接时。
    • 原因:此时客户端自身已经知道其已断开连接。
连接通知管理器示例(Connection Notification Manager Example)

以下是你可以向任何类型的NetworkBehaviour或MonoBehaviour派生组件提供客户端连接和断开通知的一个示例。

提示:
为了确保这个ConnectionNotificationManager实例能够一直有效运行,你得把它挂载到和NetworkManager相同的GameObject上,只要NetworkManager的单例实例存在,它就能持续工作。

using System;
using UnityEngine;
using Unity.Netcode;

/// <summary>
/// 仅将此示例组件附加到NetworkManager GameObject上。
/// 这将为你提供一个集中注册处理客户端连接和断开连接事件的地方。  
/// </summary>
public class ConnectionNotificationManager : MonoBehaviour
{
    public static ConnectionNotificationManager Singleton { get; internal set; }

    public enum ConnectionStatus
    {
        Connected,
        Disconnected
    }

    /// <summary>
    /// 当客户端连接或断开游戏时,此委托会被调用。
    /// 第一个参数是客户端的ID(ulong类型)。
    /// 第二个参数表示该客户端正在连接还是断开连接。
    /// </summary>
    public event Action<ulong, ConnectionStatus> OnClientConnectionNotification;

    private void Awake()
    {
        if (Singleton != null)
        {
            // 只要不创建多个NetworkManager实例,就抛出异常。
            //(***当前调用栈的位置将停在这里***)
            throw new Exception($"检测到不止一个 {nameof(ConnectionNotificationManager)}实例! " +
                $"是否在某个 {nameof(GameObject)}上附加了多个组件?");
        }
        Singleton = this;
    }

    private void Start()
    {
        if (Singleton != this){
            return; // 如果这是重复实例,则防止问题进一步恶化 >:(
        }

        if (NetworkManager.Singleton == null)
        {
            // 不能监听不存在的对象 >:(
            throw new Exception($"当前没有能让{nameof(ConnectionNotificationManager)}干活用的{nameof(NetworkManager)}!" +
                $"请在场景中添加一个{nameof(NetworkManager)}");
        }

        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnectedCallback;
        NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnectCallback;
    }

    private void OnDestroy()
    {
        // 由于NetworkManager可能在本组件之前被销毁,所以只有在其单例仍然存在的情况下才移除订阅。
        if (NetworkManager.Singleton != null)
        {
            NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnectedCallback;
            NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnectCallback;
        }
    }

    private void OnClientConnectedCallback(ulong clientId)
    {
        OnClientConnectionNotification?.Invoke(clientId, ConnectionStatus.Connected);
    }

    private void OnClientDisconnectCallback(ulong clientId)
    {
        OnClientConnectionNotification?.Invoke(clientId, ConnectionStatus.Disconnected);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

网络变换(NetworkTransform)

简介(Introduction)

在如今的多人游戏中,对象transform(变换)的同步是最常见的网络编码任务之一。这个概念看似简单:

  • 确定你希望同步的transform轴
  • 序列化这些值
  • 将序列化的值作为消息发送给所有其他已连接客户端
  • 处理消息并反序列化这些值
  • 将这些值应用到相应的轴上

乍一看,上述高级概述的任务似乎相对简单,但当你开始逐项实现时,几乎任何资深网络编码软件工程师都会认同:这一过程很快就会变得复杂起来。

例如,上述列举的任务并未考虑到以下问题:

  • 谁来控制同步(也就是说,是每个客户端、服务器端,还是根据被同步对象的不同可能由两者共同控制)?
  • 你应当多久同步一次这些值?又该使用什么样的逻辑来判断何时需要进行同步呢?
  • 如果有复杂的父子层级结构(父对象带有一个或多个子变换(transforms)),应该同步世界空间坐标轴的值还是局部空间坐标轴的值?
  • 怎样才能优化每次transform更新时所需的带宽成本?

幸运的是,NGO 提供了“NetworkTransform”组件实现,它能够处理transform同步中一些较复杂的问题,并且可以通过编辑器内的inspector视图访问和轻松配置其属性。

添加(Adding)

在给GameObject添加NetworkTransform组件时,你应该确保该GameObject已经附加了NetworkObject组件,或者该GameObject的transform父对象被分配给了一个已经附加了NetworkObject组件的GameObject,就像下图所示:请添加图片描述
你也可以像下图所示那样,创建一个带有NetworkObject组件的父级GameObject,并在其下挂载一个带有NetworkTransform组件的子级GameObject:
请添加图片描述

每周更新

很好,接下来你可以尝试看看官方文档了:
官方文档链接

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/116517
推荐阅读
相关标签
  

闽ICP备14008679号