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\$

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.