在 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.

解决方案

  1. NetworkIdentity添加到层次结构中的所有对象
  2. 忽略警告Prefab 'xxx' has several NetworkIdentity components attached to itself or its children, this is not supported.
  3. 通过脚本手动处理网络上的层次结构

我们需要确保客户端只有在有父对象时才会接收到子对象。我们还需要确保客户端在收到父对象时尽快收到子对象。

这是通过OnRebuildObserversOnCheckObserver实现的。这些方法检查客户端是否有父对象,如果有,它会将玩家连接添加到观察者列表,这会导致玩家接收对象。

我们还需要在生成父对象时调用 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);
            }
        }
    }
}