1
\$\begingroup\$

We are currently writing an online game in Unity. The essential idea is that it is structured like a split-screen, couch co-op game, with the online being offered as an extension to allow multiple players (up to 4) to play remotely or using their own screens. In a network game, players can devote the screen to their own view, but can also see exactly what each other player sees either using screen switching or split screen. As such, it is important for each player to synchronize their own camera, and for each player to have a constant player index number so that other players can easily call up their camera (i.e. from a SyncList or similar).

However, I cannot figure out how to implement this. I am attempting to use the UNet RPC interface to have the client call into the server, assign a player number, then report that value back to the client. However, not only do RPCs not return values, they also appear to have a very limited style that only allows them to be called on objects owned by a particular player - ClientRpc and TargetRpc can only be called by clients, and Command does not give a reliable way to tell which client called it. I did manage to get around the first problem with a library that lets async calls return values using what are essentially JavaScript Promises, but when I try to make directed Rpc calls, the Unity runtime rejects the call from the client.

Simply put, I want a single client to ask the server a question, and get a response back from it. This seems like a fundamental thing to have in a network architecture, and is the basis of many RPC systems such as HTTP REST calls. Is UNet simply not designed for this kind of interaction whatsoever?

My code is below - this is a NetworkBehaviour script attached to a root object that is visible to all clients, but has no other duties. The design is that the client would call AssignPlayerNumber to get a Promise yielding their canonical player index, and the server would maintain the actual list of player numbers so that numbers don't get mixed up.

using RSG;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class PlayerNumberBroker : NetworkBehaviour {

    // NOTE: Right now, this uses the player GameObjects as the universal key,
    // which might not work if player objects are despawned

    public struct PlayerUniversalKey
    {
        public bool present;
        public GameObject playerObject;
    }

    public class SyncListPlayers : SyncListStruct<PlayerUniversalKey>
    {

    }

    private SyncListPlayers players;

    private void Start()
    {
        players = new SyncListPlayers();
    }

    private int assignPlayerNumberRequestSequenceNum = 0;
    private Dictionary<int, IPendingPromise<int>> assignPlayerNumberRequests = new Dictionary<int, IPendingPromise<int>>();

    public IPromise<int> AssignPlayerNumber(GameObject player)
    {
        if (isServer)
        {
            return Promise<int>.Resolved(AssignPlayerNumber_Inner(player));
        }
        else if (isClient)
        {
            Promise<int> promise = new Promise<int>();
            int requestNum = assignPlayerNumberRequestSequenceNum++;
            assignPlayerNumberRequests.Add(requestNum, promise);
            TargetAssignPlayerNumberRequest(this.connectionToServer, player, requestNum);
            return promise;
        }
        else
            throw new System.Exception("Caller to AssignPlayerNumber is neither client nor server");
    }

    private int AssignPlayerNumber_Inner(GameObject player)
    {
        PlayerUniversalKey keyToUse = new PlayerUniversalKey()
        {
            present = true,
            playerObject = player
        };
        int indexToUse;
        for (int i = 0; i < players.Count; i++)
        {
            if (!players[i].present)
            {
                indexToUse = i;
                goto indexFound;
            }
            else if (players[i].playerObject.Equals(player))
            {
                // Simply return the already assigned player number
                return i;
            }
        }
        indexToUse = players.Count;
        players.Add(keyToUse);

        indexFound:
        players[indexToUse] = keyToUse;
        players.Dirty(indexToUse);
        Debug.Log(player.ToString() + ":" + indexToUse.ToString());

        return indexToUse;
    }

    [TargetRpc]
    private void TargetAssignPlayerNumberResponse(NetworkConnection target, int playerNumber, int requestNumber)
    {
        IPendingPromise<int> promise = assignPlayerNumberRequests[requestNumber];
        assignPlayerNumberRequests.Remove(requestNumber);
        promise.Resolve(playerNumber);
    }

    // run on server only
    // TODO: Apparently this STILL doesn't work, because a TargetRpc can only be called by a server.
    // We will have to figure out a way to pass a message more directly (though, as can be seen here,
    // we don't have to do much to make that happen).
    [TargetRpc]
    private void TargetAssignPlayerNumberRequest(NetworkConnection caller, GameObject player, int requestNumber)
    {
        int indexToUse = AssignPlayerNumber_Inner(player);

        TargetAssignPlayerNumberResponse(caller, indexToUse, requestNumber);
    }

    // run on server only
    [TargetRpc]
    public void TargetFreeAssignmentRequest(NetworkConnection caller, GameObject player)
    {
        for (int i = 0; i < players.Count; i++)
        {
            if (players[i].playerObject.Equals(player))
            {
                players[i] = new PlayerUniversalKey()
                {
                    present = false,
                    playerObject = null
                };
            }
        }
    }

    public int GetPlayerNumber(GameObject player)
    {
        for (int i = 0; i < players.Count; i++)
        {
            if (players[i].present && players[i].playerObject.Equals(player))
            {
                return i;
            }
        }
        return -1;
    }
}
\$\endgroup\$
0
\$\begingroup\$

If you want to stick with your current request/response paradigm, you can try a pair of NetworkMessages. One type gets sent from the client, and the server responds with the other.

However, the method I'm using is this: add an integer property, annotated with SyncVar, on a MonoBehavior attached to the player object. Then, have the server assign that attribute before spawning the player object (in your NetworkManager). This way you don't need the Client to request it at all.

\$\endgroup\$
  • \$\begingroup\$ SyncVars should work in many cases, but if you are joining a game in progress you may need paired methods anyway for getting other networked data. There is also a chance the SyncVar may not be set immediately or when you need it. \$\endgroup\$ – Benjamin Danger Johnson Dec 4 '20 at 8:26
0
\$\begingroup\$

Forgive me if this answer is a little rough, I am copying it from a similar project I have where users need to know their own IDs so some of this may be a mix of real code and untested pseudo code. What I do is similar to your promise system, I have the server/host create players and assign their IDs and then have the networked clients query the server/host for their ID and the other players IDs. This is usually the only script I attach to the player prefab in the network settings. Let me know if anything is broken, I cleaned it up on the spot and removed all references to local players (my game allowed a mix of local players on gamepads and networked players).

public class ClientHandle : NetworkBehaviour
{
    public GameObject playerPrefab;
    public PlayerController localPlayer;
    private static int nextAvailablePlayerId = 0;

    private void Start()
    {
        if (isLocalPlayer)
        {
            CmdSpawnPlayerPrefab(); // I am a new player
        }
        
        if (!isServer && hasAuthority)
        {
            CmdReloadPlayersOnNewClient(); // This guy was here before it was cool
        }
    }

    #region Initialize New Players

    [Command]
    private void CmdSpawnPlayerPrefab()
    {
        GameObject spawnedPlayer = Instantiate(playerPrefab);
        NetworkServer.SpawnWithClientAuthority(spawnedPlayer, connectionToClient);

        RpcInitializePlayer(spawnedPlayer, nextAvailablePlayerId);

        if (!isClient)
        {
            PlayerInitialize(spawnedPlayer, nextAvailablePlayerId);
        }

        nextAvailablePlayerId++;
    }
   
    [ClientRpc]
    private void RpcPlayerInitialize(GameObject playerObject, int playerId)
    {
        PlayerInitialize(playerObject, playerId);
    }

    #endregion

    #region Initialize Older Players

    [Command]
    private void CmdReloadPlayersOnNewClient()
    {
        foreach (ClientHandle otherClientHandles in FindObjectsOfType<ClientHandle>())
        {
            if (otherClientHandle != this) // Ignore new players handle
            {
                PlayerController player = otherClientHandle.localPlayer;
                TargetPlayerInitialize(connectionToClient, player.gameObject, player.PlayerId);
            }
        }
    }

    [TargetRpc]
    private void TargetPlayerInitialize(NetworkConnection targetConnection, GameObject playerObject, int playerId)
    {
        PlayerInitialize(playerObject, playerId);
    }
            
    #endregion  

    private void PlayerInitialize(GameObject playerObject, int playerId)
    {
        localPlayer = playerObject.GetComponent<PlayerController>();
        localPlayer.PlayerId = playerId;
    }      
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.