在 Unity Networking - UNET 中同步复杂的游戏对象

Posted

技术标签:

【中文标题】在 Unity Networking - UNET 中同步复杂的游戏对象【英文标题】:Synchronizing complex GameObjects in Unity Networking - UNET 【发布时间】:2018-02-05 02:01:25 【问题描述】:

我正在开发玩家可以构建复杂物体的第一人称游戏。结构示例:

Train
- Wagon
  - Table
  - Chair
  - Chest (stores items)
  - Workshop (manufactures items, has build queue)

玩家可以创建火车,添加货车,将对象放入货车,修改放置的 >对象。整列火车可以移动,物体在变换层次中。

玩家可以与放置的对象进行交互(例如将物品放入箱子,修改车间构建队列),因此我需要一种跨网络识别它们的方法。这表明所有对象都应该有NetworkIdentity。一些对象也有需要同步的状态(存储项目、构建队列)。

建议的同步方法是什么?哪些对象应该有NetworkIdentity


NetworkIdentity 添加到所有这些会阻止我在编辑器中创建火车预制件(预制件只能在根目录上拥有NetworkIdentity),但我可能会接受。当客户端上生成旅行车或对象时,我还必须“手动”设置父级。


另一种解决方案可能是将NetworkIdentity 仅添加到Train,然后通过train 中的某个ID 来识别对象。我无法想象如何通过这种方法使用SyncVar,因为一切都必须在Train上。

【问题讨论】:

【参考方案1】:

解决方案

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

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

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

当父对象生成时,我们还需要调用NetworkIdentity.RebuildObservers。这是通过自定义连接类实现的,当对象在客户端生成时通知MultiplayerGame(连接发送 Spawn 消息)。

完整的脚本如下。

NetworkChild

子对象上的网络组件的基类,例如马车,马车里的物体。

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

NetworkNotifyConnection

当 Spawn and 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 上的组件,在服务器启动时设置自定义网络连接类。

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);
            
        
    

【讨论】:

以上是关于在 Unity Networking - UNET 中同步复杂的游戏对象的主要内容,如果未能解决你的问题,请参考以下文章

Unity5.1 新的网络引擎UNET(十五) Networking 引用--上

Unity5网络模块UNet介绍

[Unet]Unity Unet Spwan回调函数

Unity 5 - UNET - 将拖动的游戏对象同步为客户端(HL2 重力枪风格)

Unity5.1 新的网络引擎UNET UNET 官方推荐Demo案例

Unity模拟弹幕——Unet