I’m trying to figure out what would be the best and most efficient way of syncing object references, and make it easy to work with. Suppose we have a game with a Player class, which represents the player/client object, and a Character class, which represents a character that a Player can take control of. We’d start off with something like this:
public class Player : NetworkBehaviour
{
/*
Function to take control of a Character, give it client authority, and set its OwningPlayer....
More Player logic.....
Etc....
*/
}
public class Character : NetworkBehaviour
{
[SyncVar]
public NetworkInstanceId OwningPlayerId;
}
But the problem with that is that everytime you need to access the owning ‘Player’ object of a Character, you have to get the GameObject through ClientScene.FindLocalObject(netId), and then get the Player component of that GameObject. Which means it’s very inefficient, especially for references that need to be accessed on Update(). What I’d like to do is to cache that Player object whenever OwningPlayerId is changed. So I’m doing something like this:
public class Player : NetworkBehaviour
{
/*
Function to take control of a Character, give it client authority, and set its OwningPlayer....
More Player logic.....
Etc....
*/
}
public class Character : NetworkBehaviour
{
[SyncVar(hook = "OnOwningPlayerChanged")]
private NetworkInstanceId OwningPlayerId;
public Player CachedOwningPlayer { get; private set; }
void OnOwningPlayerChanged(NetworkInstanceId newId)
{
UpdateCachedPlayerReference(newId);
OwningPlayerId = newId;
}
void UpdateCachedPlayerReference(NetworkInstanceId newId)
{
bool foundValidPlayer = false;
GameObject playerObject = ClientScene.FindLocalObject(newId);
if (playerObject)
{
Player playerComponent = playerObject.GetComponent<Player>();
if (playerComponent)
{
CachedOwningPlayer = playerComponent;
foundValidPlayer = true;
}
}
if (!foundValidPlayer)
{
CachedOwningPlayer = null;
Debug.LogWarning("OwningPlayerId was changed but couldn't find a valid Player");
}
}
public override void OnStartClient()
{
base.OnStartClient();
// This is necessary for games where players can join mid-game, because SyncVar hooks don't fire during a new client's initial synchronization
UpdateCachedPlayerReference(OwningPlayerId);
}
}
…which works well, but it’s unfortunate that I have to make this whole setup for every synced object reference in my game. Has anyone come up with a better, more transparent/automatic solution?
unfortunately, if I understand correctly, that wouldn’t solve the case where the SyncVar reference has simply changed to another already-spawned object during the game
I ended up simplifying it all down to this (not tested):
[SyncVar]
private NetworkInstanceId OwningPlayer;
public CachedNetBehaviour<Player> CachedOwningPlayer = new CachedNetBehaviour<Player>();
public override void OnStartClient()
{
base.OnStartClient();
CachedOwningPlayer.Setup(() => this.OwningPlayer);
// Example: Getting the Player reference
Player myTestPlayer = CachedOwningPlayer.Get();
}
You just have to call the Setup() function of the cached object and give it the synced netId getter, and it’ll always give you the latest value when calling Get(). Here’s the code for CachedNetBehaviour:
using System;
using UnityEngine;
using UnityEngine.Networking;
public struct CachedNetBehaviour<T> where T : NetworkBehaviour
{
private T BehaviourReference;
private NetworkInstanceId LastKnownNetId;
private Func<NetworkInstanceId> NetIdGetter;
private bool CheckInChildren;
private int ComponentIndex;
public void Setup(Func<NetworkInstanceId> netIdGetter, bool checkInChildren = false, int componentIndex = 0)
{
BehaviourReference = null;
LastKnownNetId = new NetworkInstanceId();
NetIdGetter = netIdGetter;
CheckInChildren = checkInChildren;
ComponentIndex = componentIndex;
UpdateReference(NetIdGetter.Invoke());
}
public T Get()
{
if (NetIdGetter != null)
{
// Detect netId change and automatically update reference
NetworkInstanceId actualNetIdValue = NetIdGetter.Invoke();
if (actualNetIdValue != LastKnownNetId)
{
UpdateReference(actualNetIdValue);
}
return BehaviourReference;
}
return null;
}
public void UpdateReference(NetworkInstanceId newNetId)
{
bool foundValidBehaviour = false;
GameObject networkedGameObject = ClientScene.FindLocalObject(newNetId);
if (networkedGameObject)
{
T behaviourComponent = null;
if (CheckInChildren)
{
if (ComponentIndex == 0)
{
behaviourComponent = networkedGameObject.GetComponentInChildren<T>();
}
else
{
T[] foundBehaviours = networkedGameObject.GetComponentsInChildren<T>();
if (foundBehaviours.Length > ComponentIndex)
{
behaviourComponent = foundBehaviours[ComponentIndex];
}
}
}
else
{
if (ComponentIndex == 0)
{
behaviourComponent = networkedGameObject.GetComponent<T>();
}
else
{
T[] foundComponents = networkedGameObject.GetComponents<T>();
if (foundComponents.Length > ComponentIndex)
{
behaviourComponent = foundComponents[ComponentIndex];
}
}
}
if (behaviourComponent != null)
{
BehaviourReference = behaviourComponent;
foundValidBehaviour = true;
}
}
if (!foundValidBehaviour)
{
BehaviourReference = null;
Debug.LogWarning("Tried updating the NetworkBehaviour reference of " + this + " but no valid NetworkBehaviour was found");
}
LastKnownNetId = newNetId;
}
}
The only thing I’m not completely happy about is the fact that it uses a Func delegate whenever Get() is called to check if the SyncVar has changed. Since it’s a delegate, it’s not as performant as a direct call. I might just end up manually calling the “UpdateReference()” function with a SyncVar hook instead and getting rid of the Func checker thing in the Get() function
I just use a global player manager where each player is stored in an array with a unique ID (int). The id is assigned, updated and synced only on player connection and disconnection. Then, I can easily access each player with this id from the array.