产成对象
在Unity中,使用Instantiate(
)创建新的游戏对象有时被称为“spawn”。在网络HLAPI中,“spawn”一词用于表示更具体的内容。在网络HLAPI的服务器权威模型中,为了“产生”服务器上的对象,意味着应该在连接到服务器的客户端上创建对象,并且该对象将由Spawn系统管理。一旦进入Spawn系统,当服务器上的对象发生更改时,状态更新就会发送给客户端,并且当客户端在服务器上销毁时,客户端将被销毁。产生的对象也被添加到服务器正在管理的网络对象集中,这样如果其他客户端稍后加入游戏,对象也将在该客户端上产生。这些对象有一个称为“netId”的唯一网络实例标识,它在每个对象的服务器和客户端上都是相同的。这用于将消息路由到对象并识别对象。
当NetworkIdentity
对象在客户端上产生时,它们将使用服务器上对象的当前状态创建。这适用于对象的变换,移动状态和同步变量。因此,客户端对象在创建时始终处于最新状态。这样可以避免出现问题,例如对象在错误的初始位置产生,然后在状态更新数据包到达时弹出到正确的位置。
这听起来不错,但有一些即时问题会引发。在客户端上创建的对象如何?而且,如果对象在产生时间和另一个客户端连接之间发生变化,该怎么办?那么为新客户端生成哪个版本的对象?
产生客户端上的对象的实例化来自传递给服务器上的NetworkServer.Spawn
的对象的预制件中的客户端对象。NetworkIdentity
检查器预览面板显示NetworkIdentity
的assetID
,正是此值标识预制,以便客户端可以创建对象。为了高效工作,客户必须执行注册步骤; 他们必须调用ClientScene.RegisterPrefab
来告诉系统有关将从中创建客户端对象的资产。
编辑器中的NetworkManager
可以很方便地完成spawn
预制件的注册。NetworkManager
的“spawn信息”部分允许您注册预制而无需编写任何代码。当创建NetworkClient
时,这也可以通过代码完成。要在代码中完成它:
using UnityEngine;
using UnityEngine.Networking;
public class MyNetworkManager : MonoBehaviour
{
public GameObject alienPrefab;
NetworkClient myClient;
// Create a client and connect to the server port
public void SetupClient()
{
ClientScene.RegisterPrefab(alienPrefab);
myClient = new NetworkClient();
myClient.RegisterHandler(MsgType.Connect, OnConnected);
myClient.Connect("127.0.0.1", 4444);
}
}
在这个例子中,用户会将预制资产拖到MyNetworkManager
脚本的alienPrefab
插槽中。所以当在服务器上产生一个外来对象时,将在客户端上创建相同类型的对象。这种资产注册可确保资产与场景一起加载,从而在创建资产时不会出现摊档。对于更高级的用例,如对象池或动态创建的资产,有ClientScene.RegisterSpawnHandler
,它允许为客户端生成注册回调函数。
下面是一个用随机数叶子创建树的spawner的简单示例。
class Tree : NetworkBehaviour
{
[SyncVar]
public int numLeaves;
}
class MySpawner : NetworkBehaviour
{
public GameObject treePrefab;
public void Spawn()
{
GameObject tree = (GameObject)Instantiate(treePrefab, transform.position, transform.rotation);
tree.GetComponent<Tree>().numLeaves = Random.Range(10,200);
NetworkServer.Spawn(tree);
}
}
为了完成这个例子,该项目将为具有树脚本和NetworkIdentity
组件的树提供预制资源。然后在场景中的MySpawner实例上,treePrefab
插槽将由树预制资源填充。此外,树预制必须注册为一个可生成的对象 - 使用NetworkManager UI
或在代码中使用ClientScene.RegisterPrefab()
。
当此代码运行时,客户端上创建的树对象将具有来自服务器的numLeaves
的正确值。
约束
• NetworkIdentity必须位于可生成预制件的根游戏对象上
• NetworkBehaviour脚本必须与NetworkIdentity位于同一个游戏对象上,而不是子游戏对象
• 预制件不能在NetworkManager中注册,除非它们的根对象上有NetworkIdentity
对象创建流程
创建的实际操作流程是:
• NetworkIdentity组件的预制注册为spawn
• GameObject是从服务器上的预制实例化的
• 游戏代码在实例上设置初始值(请注意,此处应用的3D物理力不会立即生效)
• NetworkServer.Spawn()与实例一起被调用
• 通过调用NetworkBehaviour组件上的OnSerialize()来收集服务器上实例上SyncVars的状态
• 将类型为MsgType.ObjectSpawn的网络消息发送到包含SyncVar数据的连接客户端
• 在服务器上的实例上调用OnStartServer(),并将isServer设置为true
• 客户端收到ObjectSpawn消息并从注册的预制件创建一个新实例
• SyncVar数据通过调用NetworkBehaviour组件上的OnDeserialize()应用于客户端上的新实例
• 在每个客户端上的实例上调用OnStartClient(),并将isClient设置为true
• 随着游戏进行,对SyncVar值的更改会自动同步到客户端。这一直持续到游戏结束。
• NetworkServer.Destroy()在服务器上的实例上被调用
• 一个类型为MsgType ObjectDestroy的网络消息被发送给客户端
• OnNetworkDestroy()在客户端上的实例上调用,然后实例被销毁。
玩家对象
网络HLAPI中的玩家对象在某些方面是特殊的。使用NetworkManager生成玩家对象的流程是:
• NetworkIdentity预制注册为PlayerPrefab
• 客户端连接到服务器
• 客户端调用AddPlayer(),类型MsgType.AddPlayer的网络消息被发送到服务器
• 服务器接收消息并调用NetworkManager.OnServerAddPlayer()
• GameObject从服务器上的PlayerPrefab实例化
• 使用服务器上的新玩家实例调用NetworkManager.AddPlayerForConnection()
• 玩家实例生成 - 您不必为玩家实例调用NetworkServer.Spawn()
• 一个类型为MsgType.Owner的网络消息被发送到添加了玩家的客户端(仅限该客户端!)
• 原始客户端收到网络消息
• 在原始客户端的玩家实例上调用OnStartLocalPlayer(),并将isLocalPlayer设置为true
请注意,OnStartLocalPlayer()
在OnStartClient()
之后被调用,因为它只会在玩家对象产生后所有权消息从服务器到达时发生。所以isLocalPlayer
不会在OnStartClient()
中设置。
由于OnStartLocalPlayer
仅针对您的玩家进行调用,因此它是执行初始化的好地方,只应该为本地玩家完成。这可能包括启用输入处理,并启用播放器对象的摄像头跟踪。通常只有本地玩家才有活动的相机。
产生客户机权限的对象
可以生成对象并将对象的权限分配给特定的客户端。这是通过NetworkServer.SpawnWithClientAuthority
完成的,它将客户端的NetworkConnection
作为参数进行授权。
对于这些对象,拥有权限的客户端的属性hasAuthority
将为true
,并且将在具有权限的客户端上调用OnStartAuthority()
。该客户端将能够为该对象发出命令。在其他客户端(和主机上),hasAuthority
将是false
。
使用客户端权限生成的对象必须在其NetworkIdentity
中设置LocalPlayerAuthority
。
例如,为了让玩家产生并控制一个物体:
[Command]
void CmdSpawn()
{
var go = (GameObject)Instantiate(
otherPrefab,
transform.position + new Vector3(0,1,0),
Quaternion.identity);
NetworkServer.SpawnWithClientAuthority(go, connectionToClient);
}
🔚