Tuesday, April 04, 2017

 

uNet High Level API clients sharing objects

The goal of Unity3D's uNet HL(HighLevel)API is to sync an experience across multiple clients. This differs from uNets' low level networking in that the HLAPI delegates network tasks through a spawned prefab that inherits from NetworkBehaviour. Syncing is done through this prefab created for each player as soon as they join a server. Keeping a shared variable or gameobject sync'd between clients, using the HLAPI, also needs to be accomplished through the prefab spawned in by the HLAPI. The clients prefab sends a message to the server that it wants to update a shared resource. The server then makes an RPC call for all clients to update their copy of the shared resource. To illustrate the flow, lets update a shared variable (instead of a gameobject).

When the following NetworkBehaviour is used as the prefab being spawned by the HLAPI, a SharedIntExample variable will be synced across the server and all clients. Whenever a player updates their copy a message is sent to all other clients to get their copies of the variable to match, via the server.
public class NetworkSession : NetworkBehaviour
{
    public static int SharedIntExample;

    void Update()
    {
        // only do something if the local player is doing it. CmdUpdateShare() calling RpcShareUpdate() will make sure everyone else sees it
        if (isLocalPlayer && Input.GetMouseButtonDown(0))
        {
            CmdUpdateShare(SharedIntExample + 1);  // define how the value should be updated, in this case increment by 1
        }
    }

    [Command]
    void CmdUpdateShare(int newValue)
    {
        RpcShareUpdate(newValue);  // use a Client RPC function to update the value on all clients
    }

    [ClientRpc]
    void RpcShareUpdate(int newValue)
    {
        SharedIntExample = newValue;
    }
}
This works great. You can check it by binding a UnityEngine.UI.Text field to the NetworkSession.SharedIntExample (the only reason this int is static is for ease of testing its value). It's great, unless a client joins after the variable has already been updated. Luckily, requesting a refresh to all clients is easy. Add these two functions to the NetworkSession class above.
    private void Start()
    {
        CmdNewClient();
    }

    [Command]
    void CmdNewClient()
    {
        RpcShareUpdate(SharedIntExample);
    }
Not all clients needed a refresh. Only the newly joining client needed that update. Specifying an update to a single client isn't too difficult if you make use of the newer [TargetRpc] attribute. https://docs.unity3d.com/ScriptReference/Networking.TargetRpcAttribute.html Here we update CmdNewClient() to use a new [TargetRpc] decorated function.
    [Command]
    void CmdNewClient()
    {
        TargetShareUpdate(this.connectionToClient, SharedIntExample);
    }

    [TargetRpc]
    void TargetShareUpdate(NetworkConnection target, int newValue)
    {
        SharedIntExample = newValue;
    }
Now each client has the latest value of SharedIntExample, even if it joins after the value has been updated.

Before I figured out how to do this correctly, as above, and by using AssignClientAuthority (from stumbling on http://stackoverflow.com/questions/33469930/how-do-i-sync-non-player-gameobject-properties-in-unet-unity5/35504268). I tried to do it an unnecessarily complex way, which is described below, in strike-through, under "Wrong way of doing it".

Wrong way of doing it


Decide what needs to modify the integer; just the server, just the client, or both the server and the client. If the number is modified on the client: - [Command] should be used to modify the master instance of that number stored on the server.
public class NetworkSession : NetworkBehaviour
{
    [Command]
    public void CmdSetSlide(int toIndex)
    {
        // update servers version of SlideServer.Instance.SlideIndex
        MapStatus.Instance.SlideIndex = toIndex;
    }
}
If the number is modified on the server: - [SyncVar] should be used to update the copy of that number on each client to match what is stored on the server.
public class FindIdentity : MonoBehaviour
{
    void Start()
    {
        networkConn = GameObject.FindObjectsOfType()
            .Where(s => s.isLocalPlayer)
            .FirstOrDefault();
    }

    void Update()
    {
        if (networkConn != null // check if this is a client. networkConn should only be null when this code is running on the server.
            && networkConn.CurrentCity != MapStatus.Instance.CurrentCity)
        {
            onCitySyncVarChanged();
        }
        else
        {
            keyInCity();

            // catch city changes Server side
            if (MapStatus.Instance.CurrentCity != oldCity)
            {
                oldCity = MapStatus.Instance.CurrentCity;

                // update map on the server
                mapOfPois.CurrentMap = MapStatus.Instance.CurrentCity;

                // update city syncVar of all sessions
                foreach (var netSession in GameObject.FindObjectsOfType())
                {
                    Debug.Log("netSession.name)" + netSession.name);
                    netSession.CurrentCity = MapStatus.Instance.CurrentCity;
                }
            }
        }
    }
}
- clients that join late need to have their copy updated to match the server
    ///  Make sure a newly connected client gets the city selection made before they joined
    /// This override only runs on the client when the client has authority, which is after OnStartClient 
    public override void OnStartLocalPlayer()
    {
        base.OnStartLocalPlayer();

        Debug.Log("OnStartLocalPlayer " + this.name + " with " + CurrentCity + ". Asking server to update CurrentCity.");
        CmdReconnect();
    }

    [Command]
    public void CmdReconnect()
    {
        Debug.Log("CmdReconnect " + this.name + " with " + CurrentCity + " updated to " + MapStatus.Instance.CurrentCity);

        // uses the servers copy of CurrentCity to update the SyncVar
        CurrentCity = MapStatus.Instance.CurrentCity;
    }
Since the prefab is spawned when a client joins you can't have it easily reference objects already in the scene by dragging gameobjects into fields in the inspector. You can create another gameobject to do this interfacing. It will need to search for the NetworkBehaviour, this is done above in the FindIdentity's Start(). That was the wrong way of doing it. The cleaner alternative of is above the "Wrong way of doing it" title.

Comments: Post a Comment

Subscribe to Post Comments [Atom]





<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]