在Unity中,你通常用Instantiate来 “spawn”(也就是创建)新的游戏对象。然而,在Mirror中,"spawn "这个词的意思更明确。在Mirror的服务器授权模型中,在服务器上 "spawn "一个游戏对象意味着这个游戏对象是在连接到服务器的客户端上创建的,并由生成系统管理。
一旦游戏对象使用这个系统生成,只要服务器上的游戏对象发生变化,就会向客户端发送状态更新。当Mirror在服务器上销毁游戏对象时,它也会在客户端上销毁它。服务器将生成的游戏对象与所有其他联网的游戏对象一起管理,因此,如果另一个client后来加入游戏,服务器可以在该client上生成游戏对象。这些生成的游戏对象有一个独特的网络实例ID,称为 “netId”,在服务器和客户端的每个游戏对象都是一样的。这个唯一的网络实例ID被用来将网络上的信息传递给游戏对象,并识别游戏对象。
当服务器生成一个具有网络身份组件的游戏对象时,在客户端生成的游戏对象具有相同的 “状态”。这意味着它与服务器上的游戏对象是相同的;它有相同的Transform、运动状态和(如果使用网络变换和SyncVars)同步的变量。因此,客户端游戏对象在Mirror创建时总是最新的。这就避免了诸如游戏对象在错误的初始位置生成,然后在状态更新到来时重新出现在正确的位置等问题。
一个游戏对象Prefab在试图向Network Manager注册之前,必须有一个Network Identity组件。
要在编辑器中向Network Manager注册Prefab,选择Network Manager游戏对象,在Inspector中,导航到Network Manager组件。点击再生信息旁边的三角形来打开设置,然后在已注册的可再生预制件下,点击加号(+)按钮。将Prefabs拖入空栏,将其分配到列表中。
对于更高级的用户,你可能会发现你想注册Prefabs和生成游戏对象而不使用Network Manager组件。
要在不使用Network Manager的情况下生成游戏对象,你可以通过脚本自己处理Prefab的注册。使用NetworkClient.RegisterPrefab方法向Network Manager注册Prefabs。
using UnityEngine;
using Mirror;
public class MyNetworkManager : MonoBehaviour
{
public GameObject treePrefab;
// Register prefab and connect to the server
public void ClientConnect()
{
NetworkClient.RegisterPrefab(treePrefab);
NetworkClient.RegisterHandler<ConnectMessage>(OnClientConnect);
NetworkClient.Connect("localhost");
}
void OnClientConnect(ConnectMessage msg)
{
Debug.Log("Connected to server: " + conn);
}
在这个例子中,你创建了一个空的游戏对象作为NetworkManager,然后创建并将MyNetworkManager脚本(如上)附加到该游戏对象上。创建一个prefab,该prefab上有一个 "Network Identity "组件,并将其拖到检查器中MyNetworkManager组件的treePrefab插槽上。这可以确保当服务器生成树形游戏对象时,它也会在客户端创建相同类型的游戏对象。
注册prefab可以确保在创建资产时没有停顿或加载时间。
为了使该脚本工作,你还需要为服务器添加代码。将此添加到MyNetworkManager脚本中。
public void ServerListen()
{
NetworkServer.RegisterHandler<ConnectMessage>(OnServerConnect);
NetworkServer.RegisterHandler<ReadyMessage>(OnClientReady);
// start listening, and allow up to 4 connections
NetworkServer.Listen(4);
}
// When client is ready spawn a few trees
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);
}
服务器不需要注册任何东西,因为它知道被生成的是什么游戏对象(asset ID在生成消息中被发送)。客户端需要能够查找游戏对象,所以它必须在客户端注册。
在编写你自己的network manager时,重要的是在调用服务器上的spawn命令之前让客户端准备好接收状态更新,否则它们将不会被发送。如果你使用Mirror的内置network manager组件,这将自动发生。
对于更高级的使用,如对象池或动态创建的资产,你可以使用NetworkClient.RegisterSpawnHandler方法,它允许回调函数被注册为客户端的spawn。请参阅关于Custom Spawn Functions的文档,了解这方面的一个例子。
如果游戏对象有一个网络状态,比如同步变量,那么这个状态就会与spawn message 同步。在下面的例子中,这个脚本被附在树形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脚本附加到先前创建的tree Prefab脚本上,以查看其实际效果。
生成游戏对象的内部操作的实际流程是:
HLAPI中的玩家游戏对象与非玩家游戏对象的工作方式略有不同。用Network Manager生成玩家游戏对象的流程是:
注意 OnStartLocalPlayer 是在 OnStartClient 之后调用的,因为它只发生在玩家游戏对象生成后所有权消息从服务器到达时,所以在 OnStartClient 中没有设置 isLocalPlayer。
因为OnStartLocalPlayer只为客户端的本地玩家游戏对象调用,它是一个很好的地方,可以执行只为本地玩家做的初始化。这可能包括启用输入处理,以及启用玩家游戏对象的相机跟踪。
要生成游戏对象并将这些游戏对象的权限分配给特定客户端,请使用 NetworkServer.Spawn,它以要成为权限的客户端的 NetworkConnection 作为参数。
对于这些游戏对象,在有权限的客户端上属性hasAuthority为true,在有权限的客户端上调用OnStartAuthority。该客户端可以为该游戏对象发出命令。在其他客户端(和Host)上,hasAuthority 为 false
使用客户端权限生成的对象必须在其 NetworkIdentity 中设置 LocalPlayerAuthority。
例如,上面的tree spawn的例子可以被修改,让树拥有客户端的权限,就像这样(注意,我们现在需要为拥有客户端的连接传入一个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 中,因为此时尚未设置权限,因此调用会失败。