英文原文:
https://mirror-networking.gitbook.io/docs/guides/gameobjects/spawning-gameobjects
在 Unity 中,您通常使用 Instantiate“生成”(即创建)新游戏对象。然而,在 Mirror 中,“spawn”这个词意味着更具体的东西。在 Mirror 的服务器权威模型中,在服务器上“生成”游戏对象意味着游戏对象是在连接到服务器的客户端上创建的,并由生成系统管理。
一旦使用此系统生成游戏对象,只要服务器上的游戏对象发生更改,状态更新就会发送到客户端。当 Mirror 在服务器上销毁游戏对象时,它也会在客户端上销毁它。服务器与所有其他联网游戏对象一起管理生成的游戏对象,因此如果稍后另一个客户端加入游戏,服务器可以在该客户端上生成游戏对象。这些生成的游戏对象具有唯一的网络实例 ID,称为“netId”,每个游戏对象在服务器和客户端上都是相同的。唯一的网络实例 ID 用于通过网络将消息集路由到游戏对象,并识别游戏对象。
当服务器生成带有网络身份组件的游戏对象时,在客户端生成的游戏对象具有相同的“状态”。这意味着它与服务器上的游戏对象相同;它具有相同的transform、运动状态和(如果使用网络变换和 SyncVars)同步变量。因此,当 Mirror 创建客户端游戏对象时,它们始终是最新的。这避免了诸如游戏对象在错误的初始位置生成,然后在状态更新到达时重新出现在其正确位置等问题。
游戏对象 Prefab 在尝试向网络管理器注册之前必须具有网络身份组件。
要在 Editor 中使用 Network Manager 注册 Prefab,请选择 Network Manager 游戏对象,然后在 Inspector 中导航到 Network Manager 组件。单击 Spawn Info 旁边的三角形以打开设置,然后在 Registered Spawnable Prefabs 下,单击加号 (+) 按钮。将 Prefab 拖放到空白字段中以将它们分配到列表中。
在没有网络管理器的情况下生成
对于更高级的用户,您可能会发现您希望在不使用网络管理器组件的情况下注册预制件并生成游戏对象。
要在不使用网络管理器的情况下生成游戏对象,您可以通过脚本自己处理 Prefab 注册。使用 NetworkClient.RegisterPrefab 方法将 Prefab 注册到网络管理器。
using UnityEngine;
using Mirror;
public class MyNetworkManager : MonoBehaviour
{
public GameObject treePrefab;
// 注册预制件并连接到服务器
public void ClientConnect()
{
NetworkClient.RegisterPrefab(treePrefab);
NetworkClient.RegisterHandler<ConnectMessage>(OnClientConnect);
NetworkClient.Connect("localhost");
}
void OnClientConnect(ConnectMessage msg)
{
Debug.Log("Connected to server: " + conn);
}
}
在此示例中,您创建一个空游戏对象作为网络管理器,然后创建 MyNetworkManager 脚本(如上)并将其附加到该游戏对象。创建一个附加了 Network Identity 组件的预制件,并将其拖到 Inspector 中 MyNetworkManager 组件上的 treePrefab 插槽上。这确保了当服务器生成树游戏对象时,它也会在客户端上创建相同类型的游戏对象。
注册预制件可确保创建资产没有停滞或加载时间。
要使脚本正常工作,您还需要为服务器添加代码。将此添加到 MyNetworkManager 脚本:
public void ServerListen()
{
NetworkServer.RegisterHandler<ConnectMessage>(OnServerConnect);
NetworkServer.RegisterHandler<ReadyMessage>(OnClientReady);
// 开始收听,并允许最多 4 个连接
NetworkServer.Listen(4);
}
// 当客户端准备好生成几棵树
void OnClientReady(NetworkConnection conn, ReadyMessage msg)
{
Debug.Log("Client is ready to start: " + conn);
NetworkServer.SetClientReady(conn);
SpawnTrees();
}
void SpawnTrees()
{
int x = 0;
for (int i = 0; i < 5; ++i)
{
GameObject treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
NetworkServer.Spawn(treeGo);
}
}
void OnServerConnect(NetworkConnection conn, ConnectMessage msg)
{
Debug.Log("New client connected: " + conn);
}
服务器不需要注册任何东西,因为它知道正在生成什么游戏对象(并且资产 ID 在生成消息中发送)。客户端需要能够查找游戏对象,因此必须在客户端上注册。
在编写自己的网络管理器时,重要的是让客户端在调用服务器上的 spawn 命令之前准备好接收状态更新,否则它们将不会被发送。如果您使用 Mirror 的内置网络管理器组件,这会自动发生。
对于更高级的用途,例如对象池或动态创建的资产,您可以使用 NetworkClient.RegisterSpawnHandler 方法,该方法允许注册回调函数以进行客户端生成。有关此示例,请参阅有关自定义生成函数的文档。
如果游戏对象具有类似同步变量的网络状态,则该状态与生成消息同步。在以下示例中,此脚本附加到树 Prefab:
using UnityEngine;
using Mirror;
class Tree : NetworkBehaviour
{
[SyncVar]
public int numLeaves;
public override void OnStartClient()
{
Debug.Log("Tree spawned with leaf count " + numLeaves);
}
}
附加此脚本后,您可以更改 numLeaves 变量并修改 SpawnTrees 函数以查看它准确地反映在客户端上:
void SpawnTrees()
{
int x = 0;
for (int i = 0; i < 5; ++i)
{
GameObject treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
Tree tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.Spawn(treeGo);
}
}
将 Tree 脚本附加到之前创建的 treePrefab 脚本以查看此操作。
限制条件
- NetworkIdentity 必须位于可生成 Prefab 的根游戏对象上。没有这个,网络管理器无法注册预制件。
- NetworkBehaviour 脚本必须与 NetworkIdentity 在同一个游戏对象上,而不是在子游戏对象上
游戏对象创建流程
生成游戏对象的内部操作的实际流程是:
- 带有网络身份组件的预制件被注册为可生成的。
- 游戏对象从服务器上的 Prefab 实例化。
- 游戏代码在实例上设置初始值(请注意,此处应用的 3D 物理力不会立即生效)。
- NetworkServer.Spawn 与实例一起调用。
- 通过调用 [Network Behaviour] 组件上的 OnSerialize 来收集服务器上实例上 SyncVars 的状态。
- ObjectSpawn 类型的网络消息被发送到包含 SyncVar 数据的已连接客户端。
- 在服务器上的实例上调用 OnStartServer,并将 isServer 设置为 true
- 客户端收到 ObjectSpawn 消息并从注册的 Prefab 创建一个新实例。
- 通过在网络行为组件上调用 OnDeserialize,将 SyncVar 数据应用到客户端上的新实例。
- 在每个客户端上的实例上调用 OnStartClient,并将 isClient 设置为 true
- 随着游戏的进行,对 SyncVar 值的更改会自动同步到客户端。这种情况一直持续到游戏结束。
- 在服务器上的实例上调用 NetworkServer.Destroy。
- OnStopServer 在服务器上的实例上调用
- ObjectDestroy 类型的网络消息被发送到客户端。
- OnStopClient 在客户端上的实例上调用
- 实例被销毁(如果它是场景对象,则被禁用)
玩家游戏对象
HLAPI 中的玩家游戏对象与非玩家游戏对象的工作方式略有不同。使用网络管理器生成玩家游戏对象的流程是:
- 具有 NetworkIdentity 的 Prefab 注册为 PlayerPrefab
- 客户端连接到服务器
- 客户端调用AddPlayer,MsgType.AddPlayer类型的网络消息被发送到服务器
- 服务器接收消息并调用 NetworkManager.OnServerAddPlayer
- 游戏对象从服务器上的 Player Prefab 实例化
- 使用服务器上的新player实例调用 NetworkManager.AddPlayerForConnection
- player实例已生成 - 您不必为player实例调用 NetworkServer.Spawn。生成消息会像正常生成一样发送给所有客户端。
- Owner 类型的网络消息被发送到添加玩家的客户端(仅限该客户端!)
- 原客户端收到网络消息
- OnStartLocalPlayer 在原客户端的player实例上调用,isLocalPlayer 设置为 true
注意 OnStartLocalPlayer 是在 OnStartClient 之后调用的,因为它只发生在玩家游戏对象生成后所有权消息从服务器到达时,所以在 OnStartClient 中没有设置 isLocalPlayer。
因为 OnStartLocalPlayer 只为客户端的本地玩家游戏对象调用,所以它是执行只应为本地玩家执行的初始化的好地方。这可能包括启用输入处理,并启用玩家游戏对象的相机跟踪。
要生成游戏对象并将这些游戏对象的权限分配给特定客户端,请使用 NetworkServer.Spawn,它以要成为权限的客户端的 NetworkConnection 作为参数。
对于这些游戏对象,在有权限的客户端上属性hasAuthority为true,在有权限的客户端上调用OnStartAuthority。该客户端可以为该游戏对象发出命令。在其他客户端(和主机)上,hasAuthority 为假。
使用客户端权限生成的对象必须在其 NetworkIdentity 中设置 LocalPlayerAuthority。
例如,上面的树生成示例可以修改为允许树具有这样的客户端权限(请注意,我们现在需要为拥有客户端的连接传入一个 NetworkConnection 游戏对象):
void SpawnTrees(NetworkConnection conn)
{
int x = 0;
for (int i = 0; i < 5; ++i)
{
GameObject treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
Tree tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.Spawn(treeGo, conn);
}
}
现在可以修改 Tree 脚本以向服务器发送命令:
public override void OnStartAuthority()
{
CmdMessageFromTree("Tree with " + numLeaves + " reporting in");
}
[Command]
void CmdMessageFromTree(string msg)
{
Debug.Log("Client sent a tree message: " + msg);
}
请注意,您不能只将 CmdMessageFromTree 调用添加到 OnStartClient 中,因为此时尚未设置权限,因此调用会失败。