在 Unity Networking 中同步复杂的游戏对象 - UNET
Synchronizing complex GameObjects in Unity Networking - UNET
我正在开发玩家可以构建复杂对象的第一人称游戏。结构示例:
Train
- Wagon
- Table
- Chair
- Chest (stores items)
- Workshop (manufactures items, has build queue)
玩家可以创造火车,添加货车,将物体放入货车,修改放置的对象。整列火车可以移动,对象处于变换层次结构中。
玩家可以与放置的物体互动(例如将物品放入箱子,修改车间建造队列),所以我需要一种跨网络识别它们的方法。这表明所有对象都应该有 NetworkIdentity
。一些对象也有需要同步的状态(存储的项目、构建队列)。
建议的同步方法是什么?哪些对象应该有 NetworkIdentity
?
向所有这些添加 NetworkIdentity
会阻止我在编辑器中创建 Train 预制件(预制件只能在 root 上具有 NetworkIdentity
),但我可能会接受它。当货车或物体在客户端上生成时,我还必须 "manually" 设置父对象。
另一种解决方案可能是仅将 NetworkIdentity
添加到 Train,然后通过 train 中的某个 ID 识别对象。我无法想象如何通过这种方法使用 SyncVar
,因为一切都必须在 Train.
上
解决方案
- 将
NetworkIdentity
添加到层次结构中的所有对象
- 忽略警告
Prefab 'xxx' has several NetworkIdentity components attached to itself or its children, this is not supported.
- 通过脚本手动处理网络上的层次结构
我们需要确保客户端只有在有父对象时才会接收到子对象。我们还需要确保客户端在收到父对象时尽快收到子对象。
这是通过OnRebuildObservers
和OnCheckObserver
实现的。这些方法检查客户端是否有父对象,如果有,它会将玩家连接添加到观察者列表,这会导致玩家接收对象。
我们还需要在生成父对象时调用 NetworkIdentity.RebuildObservers
。这是通过自定义连接 class 实现的,当对象在客户端生成时通知 MultiplayerGame
(连接发送生成消息)。
完整脚本如下。
网络子
Base class 用于子对象上的网络组件,例如货车,货车中的物体。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// Base component for network child objects.
/// </summary>
public abstract class NetworkChild : NetworkBehaviour
{
private NetworkIdentity m_networkParent;
[SyncVar(hook = "OnNetParentChanged")]
private NetworkInstanceId m_networkParentId;
public NetworkIdentity NetworkParent
{
get { return m_networkParent; }
}
#region Server methods
public override void OnStartServer()
{
UpdateParent();
base.OnStartServer();
}
[ServerCallback]
public void RefreshParent()
{
UpdateParent();
GetComponent<NetworkIdentity>().RebuildObservers(false);
}
void UpdateParent()
{
NetworkIdentity parent = transform.parent != null ? transform.parent.GetComponentInParent<NetworkIdentity>() : null;
m_networkParent = parent;
m_networkParentId = parent != null ? parent.netId : NetworkInstanceId.Invalid;
}
public override bool OnCheckObserver(NetworkConnection conn)
{
// Parent id might not be set yet (but parent is)
m_networkParentId = m_networkParent != null ? m_networkParent.netId : NetworkInstanceId.Invalid;
if (m_networkParent != null && m_networkParent.observers != null)
{
// Visible only when parent is visible
return m_networkParent.observers.Contains(conn);
}
return false;
}
public override bool OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
// Parent id might not be set yet (but parent is)
m_networkParentId = m_networkParent != null ? m_networkParent.netId : NetworkInstanceId.Invalid;
if (m_networkParent != null && m_networkParent.observers != null)
{
// Who sees parent will see child too
foreach (var parentObserver in m_networkParent.observers)
{
observers.Add(parentObserver);
}
}
return true;
}
#endregion
#region Client Methods
public override void OnStartClient()
{
base.OnStartClient();
FindParent();
}
void OnNetParentChanged(NetworkInstanceId newNetParentId)
{
if (m_networkParentId != newNetParentId)
{
m_networkParentId = newNetParentId;
FindParent();
OnParentChanged();
}
}
/// <summary>
/// Called on client when server sends new parent
/// </summary>
protected virtual void OnParentChanged()
{
}
private void FindParent()
{
if (NetworkServer.localClientActive)
{
// Both server and client, NetworkParent already set
return;
}
if (!ClientScene.objects.TryGetValue(m_networkParentId, out m_networkParent))
{
Debug.AssertFormat(false, "NetworkChild, parent object {0} not found", m_networkParentId);
}
}
#endregion
}
网络通知连接
自定义连接 class 当 Spawn 和 Destroy 消息发送到客户端时通知 MultiplayerGame
。
using System;
using UnityEngine;
using UnityEngine.Networking;
public class NetworkNotifyConnection : NetworkConnection
{
public MultiplayerGame Game;
public override void Initialize(string networkAddress, int networkHostId, int networkConnectionId, HostTopology hostTopology)
{
base.Initialize(networkAddress, networkHostId, networkConnectionId, hostTopology);
Game = NetworkManager.singleton.GetComponent<MultiplayerGame>();
}
public override bool SendByChannel(short msgType, MessageBase msg, int channelId)
{
Prefilter(msgType, msg, channelId);
if (base.SendByChannel(msgType, msg, channelId))
{
Postfilter(msgType, msg, channelId);
return true;
}
return false;
}
private void Prefilter(short msgType, MessageBase msg, int channelId)
{
}
private void Postfilter(short msgType, MessageBase msg, int channelId)
{
if (msgType == MsgType.ObjectSpawn || msgType == MsgType.ObjectSpawnScene)
{
// NetworkExtensions.GetObjectSpawnNetId uses reflection to extract private 'netId' field
Game.OnObjectSpawn(NetworkExtensions.GetObjectSpawnNetId(msg), this);
}
else if (msgType == MsgType.ObjectDestroy)
{
// NetworkExtensions.GetObjectDestroyNetId uses reflection to extract private 'netId' field
Game.OnObjectDestroy(NetworkExtensions.GetObjectDestroyNetId(msg), this);
}
}
}
多人游戏
NetworkManager
上的组件,它在服务器启动时设置自定义网络连接 class。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// Simple component which starts multiplayer game right on start.
/// </summary>
public class MultiplayerGame : MonoBehaviour
{
HashSet<NetworkIdentity> m_dirtyObj = new HashSet<NetworkIdentity>();
private void Start()
{
var net = NetworkManager.singleton;
var host = net.StartHost();
if (host != null)
{
NetworkServer.SetNetworkConnectionClass<NetworkNotifyConnection>();
}
}
/// <summary>
/// Reliable callback called on server when client receives new object.
/// </summary>
public void OnObjectSpawn(NetworkInstanceId objectId, NetworkConnection conn)
{
var obj = NetworkServer.FindLocalObject(objectId);
RefreshChildren(obj.transform);
}
/// <summary>
/// Reliable callback called on server when client loses object.
/// </summary>
public void OnObjectDestroy(NetworkInstanceId objectId, NetworkConnection conn)
{
}
void RefreshChildren(Transform obj)
{
foreach (var child in obj.GetChildren())
{
NetworkIdentity netId;
if (child.TryGetComponent(out netId))
{
m_dirtyObj.Add(netId);
}
else
{
RefreshChildren(child);
}
}
}
private void Update()
{
NetworkIdentity netId;
while (m_dirtyObj.RemoveFirst(out netId))
{
if (netId != null)
{
netId.RebuildObservers(false);
}
}
}
}
我正在开发玩家可以构建复杂对象的第一人称游戏。结构示例:
Train
- Wagon
- Table
- Chair
- Chest (stores items)
- Workshop (manufactures items, has build queue)
玩家可以创造火车,添加货车,将物体放入货车,修改放置的对象。整列火车可以移动,对象处于变换层次结构中。
玩家可以与放置的物体互动(例如将物品放入箱子,修改车间建造队列),所以我需要一种跨网络识别它们的方法。这表明所有对象都应该有 NetworkIdentity
。一些对象也有需要同步的状态(存储的项目、构建队列)。
建议的同步方法是什么?哪些对象应该有 NetworkIdentity
?
向所有这些添加 NetworkIdentity
会阻止我在编辑器中创建 Train 预制件(预制件只能在 root 上具有 NetworkIdentity
),但我可能会接受它。当货车或物体在客户端上生成时,我还必须 "manually" 设置父对象。
另一种解决方案可能是仅将 NetworkIdentity
添加到 Train,然后通过 train 中的某个 ID 识别对象。我无法想象如何通过这种方法使用 SyncVar
,因为一切都必须在 Train.
解决方案
- 将
NetworkIdentity
添加到层次结构中的所有对象 - 忽略警告
Prefab 'xxx' has several NetworkIdentity components attached to itself or its children, this is not supported.
- 通过脚本手动处理网络上的层次结构
我们需要确保客户端只有在有父对象时才会接收到子对象。我们还需要确保客户端在收到父对象时尽快收到子对象。
这是通过OnRebuildObservers
和OnCheckObserver
实现的。这些方法检查客户端是否有父对象,如果有,它会将玩家连接添加到观察者列表,这会导致玩家接收对象。
我们还需要在生成父对象时调用 NetworkIdentity.RebuildObservers
。这是通过自定义连接 class 实现的,当对象在客户端生成时通知 MultiplayerGame
(连接发送生成消息)。
完整脚本如下。
网络子
Base class 用于子对象上的网络组件,例如货车,货车中的物体。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// Base component for network child objects.
/// </summary>
public abstract class NetworkChild : NetworkBehaviour
{
private NetworkIdentity m_networkParent;
[SyncVar(hook = "OnNetParentChanged")]
private NetworkInstanceId m_networkParentId;
public NetworkIdentity NetworkParent
{
get { return m_networkParent; }
}
#region Server methods
public override void OnStartServer()
{
UpdateParent();
base.OnStartServer();
}
[ServerCallback]
public void RefreshParent()
{
UpdateParent();
GetComponent<NetworkIdentity>().RebuildObservers(false);
}
void UpdateParent()
{
NetworkIdentity parent = transform.parent != null ? transform.parent.GetComponentInParent<NetworkIdentity>() : null;
m_networkParent = parent;
m_networkParentId = parent != null ? parent.netId : NetworkInstanceId.Invalid;
}
public override bool OnCheckObserver(NetworkConnection conn)
{
// Parent id might not be set yet (but parent is)
m_networkParentId = m_networkParent != null ? m_networkParent.netId : NetworkInstanceId.Invalid;
if (m_networkParent != null && m_networkParent.observers != null)
{
// Visible only when parent is visible
return m_networkParent.observers.Contains(conn);
}
return false;
}
public override bool OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
{
// Parent id might not be set yet (but parent is)
m_networkParentId = m_networkParent != null ? m_networkParent.netId : NetworkInstanceId.Invalid;
if (m_networkParent != null && m_networkParent.observers != null)
{
// Who sees parent will see child too
foreach (var parentObserver in m_networkParent.observers)
{
observers.Add(parentObserver);
}
}
return true;
}
#endregion
#region Client Methods
public override void OnStartClient()
{
base.OnStartClient();
FindParent();
}
void OnNetParentChanged(NetworkInstanceId newNetParentId)
{
if (m_networkParentId != newNetParentId)
{
m_networkParentId = newNetParentId;
FindParent();
OnParentChanged();
}
}
/// <summary>
/// Called on client when server sends new parent
/// </summary>
protected virtual void OnParentChanged()
{
}
private void FindParent()
{
if (NetworkServer.localClientActive)
{
// Both server and client, NetworkParent already set
return;
}
if (!ClientScene.objects.TryGetValue(m_networkParentId, out m_networkParent))
{
Debug.AssertFormat(false, "NetworkChild, parent object {0} not found", m_networkParentId);
}
}
#endregion
}
网络通知连接
自定义连接 class 当 Spawn 和 Destroy 消息发送到客户端时通知 MultiplayerGame
。
using System;
using UnityEngine;
using UnityEngine.Networking;
public class NetworkNotifyConnection : NetworkConnection
{
public MultiplayerGame Game;
public override void Initialize(string networkAddress, int networkHostId, int networkConnectionId, HostTopology hostTopology)
{
base.Initialize(networkAddress, networkHostId, networkConnectionId, hostTopology);
Game = NetworkManager.singleton.GetComponent<MultiplayerGame>();
}
public override bool SendByChannel(short msgType, MessageBase msg, int channelId)
{
Prefilter(msgType, msg, channelId);
if (base.SendByChannel(msgType, msg, channelId))
{
Postfilter(msgType, msg, channelId);
return true;
}
return false;
}
private void Prefilter(short msgType, MessageBase msg, int channelId)
{
}
private void Postfilter(short msgType, MessageBase msg, int channelId)
{
if (msgType == MsgType.ObjectSpawn || msgType == MsgType.ObjectSpawnScene)
{
// NetworkExtensions.GetObjectSpawnNetId uses reflection to extract private 'netId' field
Game.OnObjectSpawn(NetworkExtensions.GetObjectSpawnNetId(msg), this);
}
else if (msgType == MsgType.ObjectDestroy)
{
// NetworkExtensions.GetObjectDestroyNetId uses reflection to extract private 'netId' field
Game.OnObjectDestroy(NetworkExtensions.GetObjectDestroyNetId(msg), this);
}
}
}
多人游戏
NetworkManager
上的组件,它在服务器启动时设置自定义网络连接 class。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// Simple component which starts multiplayer game right on start.
/// </summary>
public class MultiplayerGame : MonoBehaviour
{
HashSet<NetworkIdentity> m_dirtyObj = new HashSet<NetworkIdentity>();
private void Start()
{
var net = NetworkManager.singleton;
var host = net.StartHost();
if (host != null)
{
NetworkServer.SetNetworkConnectionClass<NetworkNotifyConnection>();
}
}
/// <summary>
/// Reliable callback called on server when client receives new object.
/// </summary>
public void OnObjectSpawn(NetworkInstanceId objectId, NetworkConnection conn)
{
var obj = NetworkServer.FindLocalObject(objectId);
RefreshChildren(obj.transform);
}
/// <summary>
/// Reliable callback called on server when client loses object.
/// </summary>
public void OnObjectDestroy(NetworkInstanceId objectId, NetworkConnection conn)
{
}
void RefreshChildren(Transform obj)
{
foreach (var child in obj.GetChildren())
{
NetworkIdentity netId;
if (child.TryGetComponent(out netId))
{
m_dirtyObj.Add(netId);
}
else
{
RefreshChildren(child);
}
}
}
private void Update()
{
NetworkIdentity netId;
while (m_dirtyObj.RemoveFirst(out netId))
{
if (netId != null)
{
netId.RebuildObservers(false);
}
}
}
}