Synchronizing complex GameObjects in Unity Networking - UNET

3.1k views Asked by At

I'm working on first person game where player can build complex objects. Structure example:

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

Player is able to create trains, add wagons, place objects into wagons, modify placed objects. Whole train can move, objects are in transform hierarchy.

Player can interact with placed objects (e.g. put items into chest, modify workshop build queue), so I need a way to identify them across network. This indicates that all objects should have NetworkIdentity. Some objects have also their state which needs to be synced (stored items, build queue).

What's suggested synchronization approach? Which objects should have NetworkIdentity?


Adding NetworkIdentity to all of them prevents me from creating Train prefabs in Editor (prefabs can have NetworkIdentity only on root), but I could probably live with that. I have to also "manually" set parent when wagon or object is spawned on client.


Another solution might be to add NetworkIdentity only to Train and then identify objects by some ID within the train. I cannot imagine how to use SyncVar with this approach, since everything would have to be on the Train.

1

There are 1 answers

0
Ondrej Petrzilka On BEST ANSWER

Solution

  1. Add NetworkIdentity to all objects in hierarchy
  2. Ignore warning Prefab 'xxx' has several NetworkIdentity components attached to itself or its children, this is not supported.
  3. Handle hierarchy on the network manually by scripts

We need to ensure that client receives child object only when it has parent. We also need to ensure that client receives child object soon when it receives parent.

This is achieved by OnRebuildObservers and OnCheckObserver. These methods check whether client has parent object, when it does it adds player connection to list of observers, which causes player to receive the object.

We also need to call NetworkIdentity.RebuildObservers when parent object is spawned. This is achieved by custom connection class, which notifies MultiplayerGame when object is spawned on client (connection sends Spawn message).

Full scripts are below.

NetworkChild

Base class for network component on objects which are children, e.g. wagon, object in wagon.

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

Custom connection class which notifies MultiplayerGame when Spawn and Destroy message is sent to client.

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

MultiplayerGame

Component on NetworkManager, which sets custom network connection class when server is started.

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