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