Here’s something I can’t seem to figure out, and after reading as much documentation as I can get my hands on, I’m more confused than ever.
I’m trying something that should be simple: a player object wants to spawn an object that is under the control of the player that spawned it, e.g. a guided missile. The problem is with receiving the freshly spawned object from the server.
To spawn the object, the remote player executes a command on the server; the command first instantiates the prefab on the server then issues
NetworkServer.SpawnWithClientAuthority(theInstantiated, connectionToClient);
to create instances on all clients, and then pass authority to the client whose player created the object. This works fine. The problem I’m having is this: how does the player receive the object that was created? I could execute an Rpc from within the command and pass the netID of the created object to the remote client. The problem is: NetworkServer.Spawn and Rpc are asynchronous operations run over the network; it is theoretically possible that the Rpc executes before the remote spawn completes, making the passed netID invalid. This would introduce a race condition into my net code, something that I want to avoid.
So, what is the correct way to pass the freshly created GameObject to the client that requested a spawn?
[EDIT:
To be clear: I could also pass the server-spawned GameObject with the RPC because Rpc do support passing game objects that have network ID. But the same issue applies: what if there is no corresponding object on the remote client?]
It sounds like you’re using the High Level API, so here’s a solution I’m using for the HLAPI.
No Rpc is needed. The trick is to override a couple of functions, and use a syncvar to relay a netId. First I set some things up with a PlayerLogin script attached to the prefab used as the Player Prefab in NetworkManager. The PlayerLogin script should contain something like the following code:
public class PlayerLogin : NetworkBehaviour
{
[SerializeField] private GameObject missilePrefab
public static uint localPlayerId;
public override void OnStartLocalPlayer()
{
localPlayerId = this.GetComponent<NetworkIdentity>().netId.Value;
}
[Command] void CmdSpawnMissile( uint netId )
{
GameObject missile = Instantiate( missilePrefab, spawnLocationVector, Quaternion.identity );
missile.GetComponent<MissileSetup>().clientOwnerId = netId;
NetworkServer.SpawnWithClientAuthority( missile, connectionToClient );
}
}
The missile prefab should have something like the following code attached:
public class MissileSetup : NetworkBehaviour
{
[SerializeField] Behaviour[] componentsToDisable;
[SyncVar] public uint clientOwnerId = 0;
public override void OnStartClient()
{
if ( PlayerLogin.localPlayerId != clientOwnerId ) {
// This code is for disabling scripts on the instances of the object for clients that don't own it
for ( int i = 0; i < componentsToDisable.Length; i++ ) {
componentsToDisable[i].enabled = false;
}
} else {
// Put code here that applies to the instance of the object that belongs to the client that spawned it
}
}
}
Attach to the missile prefab a script to control it’s movement using the player’s input, and drag a reference to that script to the componentsToDisable list in the inspector. That way the players that do not own the missile will have the control script disabled. Spawn missiles by calling the CmdSpawnMissile function, and control will automatically be applied to the missile’s owner only. Of course you will also need to write a CmdDespawnMissile function, referencing the missile to be destroyed by it’s netId. Something like this:
[Command] void CmdDespawnMissile( NetworkInstanceId missileNetId )
{
GameObject missile = NetworkServer.FindLocalObject( missileNetId );
if ( missile != null ) {
NetworkServer.Destroy( missile );
}
}
Ah! Brilliant – thanks, Omniglitch!
So the idea is not to to try and race the signal, but to look at the issue only from the Client’s perspective: use the client’s OnClientStart() at the precise moment when the object is initialized, with the owning object’s netID sent as a sync var. I checked the documentation (_https://docs.unity3d.com/Manual/UNetSpawning.html?ga=2.263011212.1492835724.1532324148-776353532.1532324148 ), and indeed, the spawn sequence for a distributed object first deserializes the sync vars before calling OnStartClient, so there is no chance for a race condition.
Damn smart!
Looking at this I’m wondering about two things:
-
Why use OnClientstart() and not OnStartAuthority()? That is only executed on the client that has authority, making the code even simpler, and it is executed after OnStartClient. Looking at your code it seems you do that for the opportunity to disable components that aren’t used on objects that don’t have authority, right?
-
To eliminate the SyncVar, I plan to use SIP (see asset store) and use a notification during OnStartAuthority to notify the local player that the object exists.
Thank you so much!
-ch
I hadn’t considered using OnStartAuthority(), and it looks like it would simplify things. I haven’t tested it yet, but it seems you could have your control script on the missile (or whatever) disabled, and enable it in OnStartAuthority(). The syncvar could still be useful during gameplay if other clients need to know who owns an object.