Search code examples
c#unity-game-enginemultiplayerunity3d-mirror

Unity Multiplayer (Mirror) - Problem in syncing game object's variables across all clients. [Trying to send command for object without authority]


I am building a 3d virtual Auction house in Unity for my semester's project where multiple users can join the host server and interact with the action items in the game and increase their bids. The updated bid values should be synced across all the users. I am able to sync player's movements across the network by adding my "character controller" to the "player prefab" of the network manager. Users are also able to interact with the other game object to increase the item's bid locally. I am facing problems in syncing the updated bids of each auction item across the network for every client.

I am adding each auction item to the "registered spawnable prefabs" list of the network manager.

Registered Spawnable Prefabs

This is the error I am getting


Trying to send command for object without authority. DataSync.CmdIncreaseBidUI
UnityEngine.Debug:LogWarning(Object)
Mirror.NetworkBehaviour:SendCommandInternal(Type, String, NetworkWriter, Int32, Boolean) (at Assets/Mirror/Runtime/NetworkBehaviour.cs:185)
DataSync:CmdIncreaseBidUI(Int32)
DataSync:Update() (at Assets/Scripts/DataSync.cs:38)

This is the script that I placed on my auction item. I am using the text mash pro game object to show the current bid of the auction item.

Game Scene

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class DataSync : NetworkBehaviour
{

    [SyncVar]
    public int num = 100;

    public TMPro.TextMeshPro textObj;

    void Start()
    {
    }

    [Command]
    public void CmdIncreaseBidUI(int num)
    {
        num += 100;
        RpcIncreaseBidUI(num);
    }

    [ClientRpc]
    public void RpcIncreaseBidUI(int num)
    {
        this.num = num;
        GetComponent<TMPro.TextMeshProUGUI>().text = "Current Bid $" + num.ToString();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            CmdIncreaseBidUI(num);
        }
    }
}


Solution

  • As said by default only the Host/Server has the Network Authority over spawned objects.

    enter image description here

    Unless you spawn them with a certain client having the authority over the object (using Networking.NetworkServer.SpawnWithClientAuthority) but this also makes no sense if multiple clients shall be able to interact with the object as in your case.


    And [SyncVar]

    variables will have their values sychronized from the server to clients

    .. and only in this direction!


    Now there is the option to gain the authority over an object via Networking.NetworkIdentity.AssignClientAuthority so yes, you could send a [Command] to the Host/Server and tell it to assign the authority to you so you can call a [Command] on that object.

    However, this has some huge flaws:

    • As this might change very often and quick you always have to make sure first that you currently have the authority and also the Host/Server has to make sure authority is assignable to you
    • You don't know when exactly the Host/Server is done assigning the authority so you would have to send a ClientRpc back to the client so he knows that now he can send a Command back to the host ... you see where this is going: Why not simply tell the Host/Server already with the very first [Command] what shall happen ;)

    Instead you have to (should/ I would ^^) put your logic into your player script (or at least a script attached to the player object you have authority over) and rather do something like

    using System.Linq;
    
    // This script goes on your player object/prefab
    public class DataSyncInteractor : NetworkBehaviour
    {
        // configure this interaction range via the Inspector in Unity units
        [SerializeField] private float interactionRange = 1;
    
        // Update is called once per frame
        void Update()
        {
            //Disable this compoennt if this is not your local player
            if (!isLocalPlayer)
            {
                enabled = false;
                return;
            }
    
            if (Input.GetKeyDown("space"))
            {
                if(FindClosestDataSyncItemInRange(out var dataSync))
                {
                    CmdIncreaseBid(dataSync.gameObject, gameObject);
                }
            }
        }
    
        private bool FindClosestDataSyncItemInRange(out DataSync closestDataSyncInRange)
        {
            // Find all existing DataSync items in the scene that are currently actuve and enabled
            var allActiveAndEnabledDataSyncs = FindObjectsOfType<DataSync>();
    
            // Use Linq Where to filter out only those that are in range
            var dataSyncsInRange = allActiveAndEnabledDataSyncs.Where(d => Vector3.Distance(d.transform.position, transform.position) <= interactionRange);
    
            // Use Linq OrderBy to order them by distance (ascending)
            var dataSyncsOrderedByDistance = dataSyncsInRange.OrderBy(d => Vector3.Distance(d.transform.position, transform.position));
    
            // Take the first item (the closest one) or null if the list is empty
            closestDataSyncInRange = dataSyncsOrderedByDistance.FirstOrDefault();
    
            // return true if an item was found (meaning closestDataSyncInRange != null)
            return closestDataSyncInRange;
        }
    
        // As you can see the CMD is now on the player object so here you HAVE the authority
        // You can pass in a reference to GameObject as long as this GameObject has a
        // NetworkIdentity component attached!
        [Command]
        private void CmdIncreaseBid(GameObject dataSyncObject, GameObject biddingPlayer)
        {
            // This is happening on the host
            // Just as a little extra from my side: I thought it would probably be interesting to store the 
            // last bidding player so when it comes to sell the item you know
            // who was the last one to bid on it ;)
            dataSyncObject.GetComponent<DataSync>().IncreaseBid(biddingPlayer);
        }
    }
    

    and then change your DataSync a little

    [RequireComponent(typeof(NetworkIdentity))]
    public class DataSync : NetworkBehaviour
    {
        // This is automatically synced from HOST to CLIENT
        // (and only in this direction)
        // whenever it does the hook method will be executed on all clients
        [SyncVar(hook = nameof(OnNumChanged))]
        public int num = 100;
    
        public TMPro.TextMeshPro textObj;
    
        public GameObject LastBiddingPlayer;
    
        void Start()
        {
            if(!textObj) textObj = GetComponent<TMPro.TextMeshProUGUI>();
        }
    
        // This method will only be called on the Host/Server
        [Server]
        public void IncreaseBid(GameObject biddingPlayer)
        {
            // increase the value -> will be synced to all clients
            num += 100;
    
            // store the last bidding player (probably enough to do this on the host)
            LastBiddingPlayer = biddingPlayer;
    
            // Since the hook is only called on the clients
            // update the display also on the host
            OnNumChanged(num);
        }
    
        private void OnNumChanged(int newNum)
        {
            textObj.text = "Current Bid $" + num.ToString();
        }
    }