Mirror: [Documentation & Suggestions] NetworkServer.Spawn() causing parent to be lost on clients, and clients modifying SyncVar
The documentation of NetworkServer.Spawn() is lacking what I view as a major use-case, as it doesn’t really explain that the gameObject’s parent is lost, which applies even if the parent has an identity (e.g. the Player prefab), and it even removes the effect of parent’s transform, causing the spawned position to differ between a server (with the parent) and a client. This should be documented using <summary> in the source code of the Spawn function itself, and I’d also suggest that some check could be made, displaying a warning by default, unless the user explicitly chooses that the parent on server is supposed to differ, e.g. via an overload (the check could be disabled at runtime via #if UNITY_EDITOR).
As a further suggestion, maybe there could be an overload that supports this explicitly under supported conditions (e.g. if both client and server share a reference to some object, or if the parent is a NetworkIdentity), as it’d make game architecture arguably simpler if developers didn’t have to introduce boilerplate to explicitly initialize and protect some parts of the code until the parent is set, or use RPC to spawn/initialize the object. In a worse scenario, developers may actually choose to abuse Update() for that, checking for a null field and assigning a parent every single frame for every single object. People shouldn’t be forced to follow bad practices this early on.
Here is an example of a workaround that one may require in a Player class in order to parent a car to it:
[SyncVar] private GameObject _car;
private NetworkIdentity _identity;
// Using extension method defined in a static class
public static void ExecuteWithoutParent(this GameObject on, Action<GameObject> action)
{
// Temporary unparenting, similar to adding parent's transform values and later subtracting them
var onTransform = on.transform;
var lastParent = onTransform.parent;
onTransform.parent = null;
action(on);
onTransform.parent = lastParent;
}
public void Start()
{
_identity = GetComponent<NetworkIdentity>();
// The car was already spawned by other player before connecting
if (!ReferenceEquals(_car, null))
{
_car.transform.parent = transform;
}
}
// Starting point of player choosing to spawn, or a car being re-spawned by the server
public void SelectedCar(int carIndex)
{
if (isServer)
{
var car = Instantiate(cars[carIndex], transform);
// Spawning without parent is necessary, otherwise stacked cars collide on client at the
// moment of re-spawn at position (0, 0, 0), causing rigidbody of client to fly away and
// keep its velocity in spite of being properly teleported once parented using the RPC.
car.ExecuteWithoutParent(o => NetworkServer.Spawn(o, _identity.connectionToClient));
_car = car;
RpcOnSpawnedCar(car);
}
else
{
CmdSelectedCar(carIndex);
}
}
[Command]
public void CmdSelectedCar(int carIndex)
{
var car = Instantiate(cars[carIndex], transform);
// Spawning without parent is necessary, otherwise the parent position is ignored
car.ExecuteWithoutParent(o => NetworkServer.Spawn(o, _identity.connectionToClient));
_car = car;
RpcOnSpawnedCar(car);
}
[ClientRpc]
public void RpcOnSpawnedCar(GameObject car)
{
// The _car will be synced in the following cycle, so we have to use param car instead
car.transform.parent = transform;
}
Note that to solve this simple issue, I’ve already had to learn how to use the [SyncVar], including the fact that only objects with NetworkIdentity are supported. That made me spend several days constantly debugging new issues before I finally understood few things, including the significance of the documented phrase such that the values are synchronized from the server to clients:
- Assigning on a client made everything seem okay until a third client joined.
- Assigning on a server before invoking RPC made a client see a null value, which I finally fixed by adding it as an RPC method parameter (and then I assigned it from inside the RPC method in order to get it recognized by other components ASAP, which is a new hazard causing the value to de-sync, so I had to modify all of the other class methods that I call from that RPC to always ask for the reference using a parameter instead of them accessing class field).
- Later I was trying to set the reference to null once I destroyed the object using a client, which was a fatal mistake, as the value was no longer synchronized from the server. I assumed that when I wrongly set a value on a client, it will get overridden by the server on the next frame, but it stayed changed to null.
It’s also mentioned that the variables are synced after OnStartClient(), but it doesn’t say how that relates to Unity’s Start(). Either way, if synced variables aren’t meant to ever be modified on the client, not only should it be documented explicitly, but a perfect solution could be if it were possible to display a warning. I’m not sure what are the C# limitations, but if it’s possible to implement a client-side setter hook, I think Mirror could at least mark the variable as dirty. Or is there any situation where client should intentionally de-sync the variable?
By the way, the other overload public static void Spawn(GameObject obj, GameObject player), which uses performance-heavy player.GetComponent<NetworkIdentity>(), even has a comment of // Deprecated 11/23/2019, so why doesn’t it use [Obsolete]? At a first glance this overload feels to be analogous to the Instantiate, which has the parent as a second parameter, so it’s easy to misunderstand how you fixed an issue, for example (this happened in my team) by wrongly deducing that “client’s object needs to be spawned with their Player as a parent to sync correctly”. I think the other Spawn should also document within its comment an explanation of the NetworkConnection parameter, so that one can easily see the quickest solution to obtain the NetworkConnection without googling around, even if the GameObject player overload gets removed.
To illustrate the severity of this issue, I’ve had a discussion on Mirror’s Discord where another user had been fighting a problem that an object, which was as a child of a canvas, has only spawned on the server, not on the clients. Not only is an UI object without a canvas object invisible, its position will also randomly move far away. It’s not even mentioned which position is used by default if it was parented. There are dozens of similar examples as to why this introduces a very steep learning curve.
I’ve also collected further feedback regarding the SpawnObject documentation on Discord, along with my ideas:
- 1.
There’s one specific typo:The Network Manager before trying to register it with the Network Manager. - 2. It doesn’t explicitly tell you how to spawn something with the Network Manager before jumping straight to “Spawning Without Network Manager”.
- 3. The article doesn’t elaborate why you would/wouldn’t use the Network Manager.
- 4. It doesn’t contain a “simplest use case” example. The Tank example was much clearer in its intentions.
- 5. In fact, just adding a very simple code example of a simple use case to the “spawning with Network Manager” section at the top of the page would go a long way.
- 6.
The difference betweenNetworkServer.Destroyand Unity’sDestroyisn’t really explained, asDestroyseems to work just fine. - 7. There are no guidelines regarding how to preconfigure variations of a spawnable prefab, as it seems to be assumed that if we have hundreds of possible modifications, we will add all of them to the
NetworkManagerindividually. - 8.
The use-case of nested spawnable objects isn’t covered. For example, if you have a car with a detachable part, and you want to despawn it as a child and spawn it as an individual object, not only will you get warnings aboutNetworkIdentitynot being allowed to be a child, but you won’t be able to spawn the object either. Most likely you’ll need to create duplicate prefabs and do your best in keeping them both the same, except one of the prefabs having theNetworkIdentity. - 9. It is said that
Once the game object is spawned using this system, state updates are sent to clients whenever the game object changes on the server, yet nothing is synced until[SyncVar]or components likeNetworkTransformdo it. It’s even said thatit has the same Transform, movement stateas if it were kept in sync (what’s a movement state?). (As a side note, Mirror doesn’t really specify which kinds of objects are really “meant” to be synchronized and which aren’t, e.g. there’s no native way to properly sync a rigidbody, but I’m aware of at least some experimental WIP pull requests.) - 10.
No comments are made regarding workarounds if one needs to add or remove network components at a runtime; or about extending the existing network components for similar purposes in general. - 11. The SpawnPlayerCustom documentation indirectly mentions a way to re-spawn a Player by
NetworkServer.ReplacePlayerForConnectionin order to use another random NetworkStartPosition, but the example seems too complex and doesn’t explain how to receive theNetworkConnectionusingplayer.GetComponent<NetworkIdentity>().connectionToClient. There’s still no way to prevent start position conflicts though, either by not using the same position for re-spawn purposes, or by detecting collisions using SphereCast, so it’s effectively useless! - 12. There’s no easy way to parametrize Player (related to SpawnPlayer docs) before it’s spawned by a call like
networkManager.StartHost(), e.g. to set a player name, unless you extend NetworkManager via SpawnPlayerCustom. This can motivate users to pass variables using static fields, or by encapsulating them in an object that usesDontDestroyOnLoad()before switching to the Online Scene (and it’s not even obvious how one could switch between multiple scenes if it weren’t feasible to useLoadSceneMode.Additivewhile keeping all irrelevant objects enabled and synchronized). - 13. It’s possible to spawn objects by placing them into Scene, as documented by SceneObjects, but there’s no way to re-spawn such objects, or to even access their prefab reference if you decide to keep the original objects disabled to serve as a template; and we only realize this by experimenting. Basically, this could be solved by letting us clone a SceneObject. As a workaround, all you can do is
GetComponentsInChildren<NetworkIdentity>()and compare their name excluding postfix like (1) with the prefab name before copying their position and rotation, which obviously isn’t feasible in large projects. Furthermore, if newly connected clients always keep the previously de-spawned objects as disabled, then I don’t see a reason why the server can’t also keep all originals disabled and let us clone them. Due to being unable to spawn prefab children, we can’t simply save them to a shared prefab that’s placed in the scene either. Is there any other workaround?
Of course, this is only supposed to be a constructive criticism, intended to also prevent similar issues in the future, as Mirror is still an amazing free library, yet we should address the UX questions of learnability along with major use-case patterns. It’s preferable to have this addressed by someone who can see a bigger picture, but feel free to choose which documentation issues you’d rather have us as a community fix by ourselves via PR.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 26 (9 by maintainers)
Yeah I still have no clue how to use the NetworkManager after reading the doc, the api ref, this thread, and everything else I could find searching. All I want to do is a spawn some objects and it just doesn’t work. Pls send help.
you have to spawn an object on the server then mirror will send a message to the client and it will also spawn there.