赞
踩
首先亮出文档,可以直接去看官方文档: 官方文档
翻译如有错误,恳请指正。
命令行运行教程链接: 教程
目前正在翻译的部分: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 GameObjects(NGO)是一个为Unity构建的高级网络库,用于抽象化网络逻辑。它使你能够将游戏对象和世界数据通过网络会话发送给多个玩家。通过使用NGO,你可以专注于构建游戏,而不必关注低级协议和网络框架。
要了解更多关于Netcode for GameObjects的功能和能力,请查看下面的内容:
安装Unity Netcode
从UNet迁移到Netcode
升级到Unity Netcode Package
Boss Room(首领房间)
Bite Size Samples(小示例)
Dilmer’s Tutorials(Dilmer的教程)
Netcode支持以下版本:
Netcode支持以下平台:
最后更新日期:2023年8月2日
以下内容跟踪了下一个版本的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是开源的,你可以在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是开源的,你可以在com.unity.multiplayer.samples.bitesize的GitHub存储库中访问完整的发行说明和变更日志。
发布 | 日期 | 更新日志
1.1.0 2022-12-13 1.1.0
最后更新日期:2023年8月14日
你可以使用本指南来帮助你在Unity项目中设置Netcode for GameObjects (NGO)。
在开始安装Netcode之前,请确保你具备以下条件:
注意
当运行在游戏主机等封闭平台(PlayStation、Xbox、Nintendo Switch)上时,可能会存在特定的政策和注意事项。请参考你所用游戏主机的文档以获取更多信息。
在安装了Netcode for GameObjects (NGO)之后,请参阅以下内容以继续你的旅程:
最后更新时间:2023年8月14日
使用本指南将 MLAPI 0.1.0 升级至 Netcode for GameObjects (Netcode) 1.0.0
升级至 Netcode for GameObjects
强烈建议尽快从 MLAPI 升级至 Netcode for GameObjects。MLAPI 不再得到维护,未来将不会再更新。MLAPI 被视为废弃的产品。
信息
请将此步骤视为必要步骤:升级到 Netcode 的包版本可能会导致你当前项目出现问题。新版本修改了文件和位置,与之前的 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
安装更新程序包后,你可以继续进行升级过程。
按照安装指南安装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 文件。如果是这样的话,你正在使用安装版。
在完成补丁程序的“更新脚本引用(Update Script References)”过程后,你的预制体(Prefabs)和游戏对象(GameObject)上的网络代码组件应该已经被更新为它们的新名称。
Patcher 窗口中还有一个“Replace Type Names(替换类型名称)”按钮。这一步是可选的。它会自动将你的脚本中的旧类型名称重命名为 Netcode 中的 API 更改,节省了手动重命名的时间。它会对一些类型名称进行全局替换。如果你想对更改有更多的控制,你可以手动执行此过程。
从你的项目中移除所有包含现有非包版本 MLAPI 的文件夹。通常意味着从项目中移除 Assets/MLAPI 和 Assets/Editor/MLAPI 文件夹。
信息
代码升级是一个手动而且漫长的过程。如果在升级过程中遇到困难,请加入我们的 Discord,我们会为你提供支持。
Unity 多人游戏团队尽力保持了大部分 MLAPI 在 Netcode 中的完整性。然而,为了成功编译,仍然需要进行一些更改。
NetworkVariable 类型现在只支持泛型,并且泛型中指定的类型必须是值类型。首先,将所有的 NetworVariable* 类型更改为泛型对应类型。例如,NetworkVariableInt 变为 NetworkVariable,NetworkVariableFloat 变为 NetworkVariable,依此类推。现在,一些类型(例如字符串)将不符合 NetworkVariable 的新类型要求。如果你的类型是字符串,你可以使用 FixedString32Bytes。需要注意的是,这种类型不允许你更改字符串的大小。对于只包含值类型的自定义结构体,你可以实现 INetworkSerializable 接口,这样就可以正常工作。最后,对于其他类型,你将需要创建自己的 NetworkVariable。为此,创建一个新的类,继承自 NetworkVariableBase,并实现所有抽象成员。如果你之前已经有了自定义的 NetworkVariable,现在的读取和写入函数会使用我们的 FastBuffer 从流中读取或写入。
场景管理有一些变化,统一了用户的使用方式。首先,它现在在 NetworkManager 单例下。因此,你可以直接通过以下方式访问它:
var sceneManager = NetworkManager.Singleton.SceneManager;
接下来,现在只有一个场景事件:OnSceneEvent。你可以订阅它,从SceneEvent类中获取场景事件的信息。在该类中,你将找到SceneEventType,它会告诉你来自场景管理器的事件类型的详细信息。最后,用于在场景之间切换的主要函数已更改以匹配Unity场景管理器。现在,你需要使用两个参数调用LoadScene函数:场景名称和LoadSceneMode,这是Unity中加载场景的标准方式。而不是使用SwitchScene函数。
NetworkBehavior 有两个主要的变化。首先,NetworkStart 方法变成了 OnNetworkSpawn,我们引入了 OnNetworkDespawn 来保持对称。其次,现在需要重写 OnDestroy 方法,因为 NetworkBehavior 已经在使用它。
我们尽量将行为变化降到最低,但其中两个变化可能会导致你的脚本出现错误。首先,当应用程序退出时,NetworkManager 现在会自行关闭连接。如果你自己实现了关闭连接的操作,你将会得到一个错误,提示你尝试了两次断开连接。其次,库现在会在 OnNetworkSpawn(之前称为 NetworkStart)方法返回后触发所有 NetworkVariable 的 OnValueChanged 事件。你需要相应地重构依赖于这个顺序的脚本。
Netcode 版本的 RPCs 调用方式发生了变化。请阅读我们关于 RPC 的新文档,并使用新系统替换你现有的 RPCs。
我们用新的 INetworkSerializable 接口替换了旧的 INetworkSerializable 接口。这个接口工作方式有些不同。详见 INetworkSerializable。
页面还提供了关于嵌套序列化类型的信息。
在 Netcode 中,SyncVars 已经被移除。将你现有的 SyncVars 转换为 NetworkVariables。
在升级项目完成后,你可以在 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(Netcode)。如果需要帮助,请在Unity Multiplayer Networking Discord上与我们联系。
UNet已弃用
UNet是一个完全被弃用的产品,你应该尽快升级到Netcode for GameObjects。
请查看以下关于从先前版本的Unity UNet迁移到Netcode的限制事项:
在进行迁移之前,建议你备份你的项目。例如:
最佳实践
建议同时使用这两种方法备份你的项目。这将为你提供一个复制的项目,并通过提交的更改和历史进行跟踪。
请参考Netcode安装指南获取更多信息。
注意
如果你第一次安装Git,你需要重新启动系统。
调用RPC的方式与UNet相同。只需调用函数,它将发送一个RPC。
在Netcode中,UNet的NetworkIdentity被称为NetworkObject,工作方式类似。
在Netcode中,UNet的NetworkTransform也被称为NetworkTransform,工作方式类似。
NetworkTransform与UNET的NetworkTransform在功能上不完全对等。它缺少诸如刚体的位置同步等功能。
在项目的各处,将UNet的NetworkAnimator替换为Netcode的NetworkAnimator组件。
在项目的所有地方将UNet的NetworkBehaviour替换为Netcode的NetworkBehaviour。
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”);
}
}
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”);
}
}
查看NetworkBehaviour获取更多信息。
将SyncVar替换为NetworkVariable,并在你的项目的所有地方使用。
为了实现与SyncVar钩子相等的功能,请将一个函数订阅到NetworkVariable的OnValueChanged回调中。UNet钩子和Netcode的OnValueChanged回调之间的一个明显区别是,Netcode会给你旧值和新值,而UNet只给你旧值。在UNet中,你还需要手动分配SyncVar的值。
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}.");
}
}
// 不要忘记使用一个初始值初始化 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}");
}
在你的项目中替换所有使用SyncVar的后缀递增和递减操作。Netcode的NetworkVariable.Value公开了一个值类型,这就是为什么不支持后缀递增/递减的原因。
public int Health = 42;
public void Update(){
Health++;
}
public NetworkVariable<int> Health = new NetworkVariable<int>(42);
public void Update(){
Health.Value = Health.Value + 1;
}
请参考NetworkVariable获取更多信息。
请在你的项目中的所有地方将SyncList替换为NetworkList。NetworkList具有一个OnListChanged事件,它类似于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;
}
NetworkList<int> m_ints = new NetworkList<int>();
// 在 Awake 或 Start 中调用此方法来订阅 NetworkList 的更改。
void ListenChanges()
{
m_ints.OnListChanged += OnIntChanged;
}
// NetworkListEvent 包含关于操作和更改的索引的信息。
void OnIntChanged(NetworkListEvent<int> changeEvent)
{
}
UNet的Command/ClientRPC在Netcode中被替换成了Server/ClientRpc,其工作方式类似。
[Command]
public void CmdExample(float x)
{
Debug.Log(“Runs on server”);
}
[ClientRpc]
public void RpcExample(float x)
{
Debug.Log(“Runs on clients”);
}
[ServerRPC]
public void ExampleServerRpc(float x)
{
Debug.Log(“Runs on server”);
}
[ClientRPC]
public void ExampleClientRpc(float x)
{
Debug.Log(“Runs on clients”);
}
注意
在Netcode中,RPC函数的名称必须以ClientRpc/ServerRpc后缀结尾。
请查看消息系统以获取更多信息。
在你的项目中,将每个地方的OnServerAddPlayer替换为ConnectionApproval。
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);
}
}
仅服务器示例:
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);
}
请查看连接批准以获得更多信息。
将项目中的所有NetworkServer.Spawn替换为NetworkObject.Spawn。
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);
}
}
GameObject go = Instantiate(myPrefab, Vector3.zero, Quaternion.identity);
go.GetComponent<NetworkObject>().Spawn();
请查看对象生成以获取更多信息。
Netcode具有自定义生成处理程序,用于替代UNet的自定义生成函数。请查看对象池以获取更多信息。
Netcode拥有 IsLocalPlayer、IsClient、IsServer 和 IsHost 来替代 UNet 的 isLocalPlayer、isClient 和 isServer。在 Netcode 中,每个对象都可以由特定的对等方拥有。可以使用 IsOwner 来检查这一点,它类似于 UNet 的 hasAuthority。
Netcode 中没有 NetworkPromimityChecker UNet 组件的直接等效组件。对于客户端的网络可见性与 UNet 类似。Netcode 没有 UNet 中 ObjectHide 消息的等效物。在 Netcode 中,主机上的网络对象始终可见。Netcode 没有 UNet 中 OnSetLocalVisibility 函数的等效函数。通过使用 NetworkObject.CheckObjectVisibility,可以将使用 OnCheckObserver 的手动网络接近性实现移植到 Netcode 中。对于 Netcode 的可见性系统,不需要 OnRebuildObservers。
public override bool OnCheckObserver(NetworkConnection conn)
{
return IsvisibleToPlayer(getComponent<NetworkIdentity>(), coon);
}
public bool IsVisibleToPlayer(NetworkIdentity identity, NetworkConnection conn){
// 任何接近性函数。
return true;
}
public void Start(){
NetworkObject.CheckObjectVisibility = ((clientId) => {
return IsVisibleToPlayer(NetworkObject, NetworkManager.Singleton.ConnectedClients[clientId]);
});
}
public bool IsVisibleToPlayer(NetworkObject networkObject, NetworkClient client){
// 任何接近性函数。
return true;
}
请参阅 Object Visibility 以了解更多关于 Netcode 网络可见性检查的信息。
在 Netcode 中,场景管理不像 UNet 那样通过 NetworkManager 完成。NetworkSceneManager 提供了相同的功能,用于切换场景。
public void ChangeScene()
{
MyNetworkManager.ServerChangeScene("MyNewScene");
}
public void ChangeScene()
{
NetworkSceneManager.LoadScene("MyNewScene", LoadSceneMode.Single);
}
目前,Netcode 没有提供使用属性标记函数只在服务器或客户端上运行的替代方案。你可以手动在函数中返回以实现这个功能。
[Client]
public void MyClientOnlyFunction()
{
Debug.Log("我是客户端!");
}
public void MyClientOnlyFunction()
{
if (!IsClient) { return; }
Debug.Log("我是客户端!");
}
Netcode 没有为 SyncEvent 提供等效的功能。要将 SyncEvent 从 UNet 移植到 Netcode,可以发送一个 RPC 来在另一端触发事件。
public class DamageClass : NetworkBehaviour
{
public delegate void TakeDamageDelegate(int amount);
[SyncEvent]
public event TakeDamageDelegate EventTakeDamage;
[Command]
public void CmdTakeDamage(int val)
{
EventTakeDamage(val);
}
}
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);
}
}
Netcode 并没有提供网络发现功能。贡献库提供了一个用于网络发现的示例实现。
在迁移并更新到 Netcode 包之后,我们建议你参考以下内容:
最后更新于2023年2月1日
使用本指南学习如何创建你的第一个NGO项目。它将引导你创建一个简单的Hello World项目,实现Netcode for GameObjects(NGO)的基本功能。
参考命令行测试助手(Testing the command line helper),了解如何使用命令行助手测试你的程序。
在开始之前,请确保具备以下先决条件:
在继续之前,请使用Unity编辑器版本2021.3或更高版本创建一个新项目。
提示
如果你还没有Assets / Scripts/
文件夹,请现在创建一个:
- 在Projects选项卡中右键单击Assets文件夹,然后选择Create > Folder。
- 将新文件夹命名为Scripts。
这里是你将保存所有脚本的地方。
本部分指导你添加网络游戏的基本组件:
本部分指导你创建一个NetworkManager组件。
首先,创建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将不会生成玩家对象。请参考玩家对象。
本节指导你创建一个为每个连接的玩家生成的对象。
提示
你可以从场景中删除玩家游戏对象,因为你在 NetworkManager 组件的 Player prefab属性中分配了这个网络预制体。该库不支持将玩家对象定义为在场景中放置的 NetworkObject。
注意
添加平面可以提供一个可视化的参考点来显示玩家预制体的位置,但这并不是必需的。
本节指导你将你的场景添加到构建中。
启用 NetworkManager 的场景管理设置允许服务器控制哪些场景加载给客户端。然而,你必须将当前场景添加到构建中以进入Play模式。
注意
默认情况下,NetworkManager 的场景管理选项已启用。
Scenes/SampleScene 会列在构建的场景中。你可以关闭构建设置窗口。
本节指导你向项目中添加基本的RPCs。
提示
如果你还没有Assets/Scripts/
文件夹,现在创建一个:
- 在Projects选项卡中右键单击 Assets 文件夹,然后选择Create > Folder(创建 > 文件夹)。
- 将新文件夹命名为 Scripts。
这里将是你保存所有脚本的地方。
创建一个名为 RpcTest.cs
的脚本:
RpcTest
。将 RpcTest.cs
脚本添加到玩家预制体中:
编辑 RpcTest.cs
脚本:
RpcTest
。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);
}
}
本节将指导你测试前一节中添加的RPCs。
提示
你可以使用多人游戏模式(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
...
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
...
只有拥有 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
...
注意
这里的NetworkObjectId
是2
,因为主机也有一个具有RpcTest
脚本的 NetworkObject 生成,但它不会发送启动链的初始RPC,因为它是服务器。
本节将展示如何使用两个脚本:HelloWorldPlayer.cs
和HelloWorldManager.cs
扩展Hello World项目的功能。
HelloWorldManager.cs
脚本Scripts
文件夹中创建一个名为HelloWorldManager.cs
的新脚本。HelloWorldManager
,并将脚本附加为其组件。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();
}
}
}
}
}
在之前的 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);
}
如前面的代码片段中所示,
HelloWorldManager.cs
脚本还通过其单例使用 NetworkManager 的实例来获取属性,例如IsClient
、IsServer
和IsLocalClient
。IsClient
和IsServer
属性指示了已建立的连接状态。
HelloWorldManager.cs
脚本引入了一个名为 SubmitNewPosition()
的新方法,HelloWorldPlayer
脚本使用该方法创建一个简单的 RPC 调用。
:::
HelloWorldPlayer.cs
脚本提示:
如果你还没有Assets/Scripts/
文件夹,请现在创建一个:
- 在Projects 选项卡中,右键单击 Assets 文件夹,然后选择 Create > Folder。
- 将新文件夹命名为 Scripts。
这是你将保存所有脚本的位置。
Scripts
文件夹中创建一个名为 HelloWorldPlayer.cs
的新脚本。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;
}
}
}
HelloWorldPlayer.cs
脚本为 Hello World 项目的玩家角色添加了一些基本的移动功能。服务器玩家和客户端玩家都可以开始移动。然而,移动是通过服务器的 position NetworkVariable 进行的,这意味着服务器玩家可以立即移动,但客户端玩家必须向服务器请求移动,等待服务器更新 position NetworkVariable,然后在本地复制变化。
HelloWorldPlayer
类继承自 Unity.Netcode
的 NetworkBehaviour
,而不是 MonoBehaviour
。这允许你自定义网络代码,并在玩家生成时对发生的事件进行覆盖。
public class HelloWorldPlayer : NetworkBehaviour
对于多人游戏,每个对象都在至少两台机器上运行:玩家一和玩家二。因此,你需要确保两台机器具有相同的行为,并且对对象的信息是正确的。其中一个要考虑的情况是理解玩家如何移动。只有一个玩家能控制玩家对象的移动。以下代码通过验证正在运行代码的机器是否是玩家的所有者来强制执行这一点。
public override void OnNetworkSpawn()
{
if (IsOwner)
{
Move();
}
}
任何实现了 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;
}
HelloWorldPlayer.cs
脚本添加到 Player 预制体中:本部分将指导你将 HelloWorldPlayer.cs
脚本添加到 Player 预制体中。
选择 Player 预制体:
将 HelloWorldPlayer.cs
脚本添加到 Player 预制体作为一个组件:
HelloWorldPlayer
脚本HelloWorldPlayer
类继承自 NetworkBehaviour
,而不是 MonoBehaviour
。
public class HelloWorldPlayer : NetworkBehaviour
HelloWorldPlayer
类定义了一个 NetworkVariable
来表示玩家的网络位置。
public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>();
HelloWorldPlayer
类覆盖了 OnNetworkSpawn
方法。
public override void OnNetworkSpawn()
{
if (IsOwner)
{
Move();
}
}
该脚本在玩家的客户端和服务器实例上调用 Move()
方法。Move()
方法执行以下操作:
public void Move()
{
if (NetworkManager.Singleton.IsServer)
{
var randomPosition = GetRandomPositionOnPlane();
transform.position = randomPosition;
Position.Value = randomPosition;
}
else
{
SubmitPositionRequestServerRpc();
}
}
本部分将为你详细介绍如何添加一个简单的 RPC 示例代码。
如果玩家是在 OnNetworkSpawn()
中由服务器拥有的玩家,你可以立即移动该玩家,如下面的示例代码所示。
if (NetworkManager.Singleton.IsServer)
{
var randomPosition = GetRandomPositionOnPlane();
transform.position = randomPosition;
Position.Value = randomPosition;
}
如果玩家是客户端,脚本会调用一个 ServerRpc
。客户端可以调用 ServerRpc
在服务器上执行操作。
else
{
SubmitPositionRequestServerRpc();
}
ServerRpc
在服务器上的玩家实例中设置位置 NetworkVariable,只是在平面上随机选择了一个点。
[ServerRpc]
void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
{
Position.Value = GetRandomPositionOnPlane();
}
玩家的服务器实例通过 ServerRpc
修改了Position
NetworkVariable
。如果玩家是客户端,它必须在 Update
循环中本地应用这个位置。
void Update()
{
transform.position = Position.Value;
}
因为 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();
}
}
上面的代码块中的方法会添加一个上下文按钮,该按钮的行为取决于客户端是服务器还是客户端。当你按下这个方法创建的按钮时,它会找到你的本地玩家并调用 Move()
方法。
现在,你可以创建一个演示上述概念的构建。
创建两个构建实例:一个用于主机,另一个用于客户端(加入主机的游戏)。
这两个构建实例都可以通过 GUI 按钮移动玩家。服务器会立即移动玩家并在客户端上复制移动。
客户端可以请求一个新的位置,指示服务器更改该实例的位置 NetworkVariable
。服务器更新位置 NetworkVariable
后,客户端会在其 Update()
方法中应用该 NetworkVariable
位置。
本节指导你如何添加一个移动玩家的 NetworkTransform
组件。
将 NetworkTransform 组件添加到 Player 预制体中:
创建一个名为 NetworkTransformTest.cs
的脚本。
NetworkTransformTest
。将 NetworkTransformTest
脚本添加到 Player 预制体中:
编辑 NetworkTransformTest.cs
脚本:
NetworkTransformTest
。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));
}
}
}
测试 NetworkTransform
本节将指导你测试你在之前添加的 NetworkTransform。
在客户端和服务器生成后,玩家胶囊在客户端和服务器上以圆形移动。
要检查是否一切按预期工作,请尝试在 Unity 编辑器中启动主机。主机同时扮演服务器和客户端的角色。
你可以通过 Unity 编辑器或命令行助手来测试你的 Hello World 项目。如果选择后者,请参考创建命令行助手。否则,请按照以下指示通过 Unity 编辑器进行测试。直到第一个客户端连接之前,只有平面出现在服务器上。然后,NGO 为每个连接的客户端生成一个新的 Player 预制体;但在游戏视图中它们会重叠。
最后更新于 2023 年 9 月 15 日
NetworkManager
是一个必需的 Netcode for GameObjects(Netcode)组件,它包含了你项目中所有与网络代码相关的设置。将其视为你的网络代码启用项目的“中央网络代码中心”。
NetworkManager.ConnectionApprovalCallback
时,启用连接批准。NetworkObject.NetworkObjectIds
。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: 使用此系统创建和发送自定义消息。
为了执行任何涉及发送消息的网络代码相关操作,首先你必须启动一个服务器并监听至少一个已连接的客户端(当作为主机运行时,服务器可以向自己发送RPCs)。为了实现这一目标,你首先要将NetworkManager
作为服务器、主机或客户端启动。你可以调用三个NetworkManager
的方法来实现:
NetworkManager.Singleton.StartServer(); // 将NetworkManager作为仅服务器启动(即无本地客户端)。
NetworkManager.Singleton.StartHost(); // 将NetworkManager作为既是服务器又是客户端启动(即具有本地客户端)。
NetworkManager.Singleton.StartClient(); // 将NetworkManager作为仅客户端启动。
危险
不要在NetworkBehaviour的Awake方法中启动NetworkManager,因为这可能会导致不良结果,取决于项目的设置!
注意
当作为服务器启动或加入已经启动的会话时,NetworkManager
可以生成一个属于客户端的"玩家对象(Player Object)"。
有关玩家预制件的更多信息,请参阅:
当启动客户端时,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类型
);
如果你在同一代码块中配置服务器和客户端,并且希望将服务器配置为监听分配给它的所有IP地址,那么你也可以向SetConnectionData方法传递一个“监听地址”的值为“0.0.0.0”,像这样:
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(
"127.0.0.1", // IP地址是一个字符串
(ushort)12345, // 端口号是一个unsigned short类型
"0.0.0.0" // 服务器监听地址是一个字符串。
);
注意
使用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的更多信息
断开连接相当简单,但是一旦NetworkManager
停止,你将无法使用/访问任何子系统(即NetworkSceneManager
),因为它们将不再可用。对于客户端、主机或服务器模式,你只需要调用NetworkManager.Shutdown
方法,它将在关闭时断开连接。
信息
当没有网络会话处于活动状态且NetworkManager
已关闭时,你将需要使用UnityEngine.SceneManagement
加载任何与网络会话无关的场景。
public void Disconnect()
{
NetworkManager.Singleton.Shutdown();
// 在这个阶段,我们必须使用UnityEngine的SceneManager切换回到主菜单
UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
}
有时候,由于各种原因,你可能需要在不关闭服务器的情况下断开一个客户端的连接。为了做到这一点,你可以调用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);
}
客户端和服务器都可以订阅NetworkManager.OnClientDisconnectCallback
事件,以便在客户端断开连接时收到通知。客户端断开连接的通知是“相对”的,取决于接收通知的是哪一方。
NetworkManager.DisconnectClient
。
以下是你可以向任何类型的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);
}
}
在如今的多人游戏中,对象transform(变换)的同步是最常见的网络编码任务之一。这个概念看似简单:
乍一看,上述高级概述的任务似乎相对简单,但当你开始逐项实现时,几乎任何资深网络编码软件工程师都会认同:这一过程很快就会变得复杂起来。
例如,上述列举的任务并未考虑到以下问题:
幸运的是,NGO 提供了“NetworkTransform”组件实现,它能够处理transform同步中一些较复杂的问题,并且可以通过编辑器内的inspector视图访问和轻松配置其属性。
在给GameObject添加NetworkTransform组件时,你应该确保该GameObject已经附加了NetworkObject组件,或者该GameObject的transform父对象被分配给了一个已经附加了NetworkObject组件的GameObject,就像下图所示:
你也可以像下图所示那样,创建一个带有NetworkObject组件的父级GameObject,并在其下挂载一个带有NetworkTransform组件的子级GameObject:
很好,接下来你可以尝试看看官方文档了:
官方文档链接
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。