Skip to content

fix: instantiate, spawn, and parent when spawning [Backport] #3403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Fixed issue where during a `NetworkObject`'s spawn if you instantiated, spawned, and parented another network prefab under the currently spawning `NetworkObject` the parenting message would not properly defer until the parent `NetworkObject` was spawned. (#3403)
- Fixed issue where in-scene placed `NetworkObjects` could fail to synchronize its transform properly (especially without a `NetworkTransform`) if their parenting changes from the default when the scene is loaded and if the same scene remains loaded between network sessions while the parenting is completely different from the original hierarchy. (#3388)
- Fixed an issue in `UnityTransport` where the transport would accept sends on invalid connections, leading to a useless memory allocation and confusing error message. (#3383)
- Fixed issue where `NetworkAnimator` would log an error if there was no destination transition information. (#3384)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int
networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context);
return false;
}

// If the target parent does not exist, then defer this message until it does.
if (LatestParent.HasValue && !networkManager.SpawnManager.SpawnedObjects.ContainsKey(LatestParent.Value))
{
networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, LatestParent.Value, reader, ref context);
return false;
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ public abstract class IntegrationTestWithApproximation : NetcodeIntegrationTest
{
private const float k_AproximateDeltaVariance = 0.01f;

/// <summary>
/// Returns a <see cref="Vector3"/> as a formatted string.
/// </summary>
/// <param name="vector3">reference of <see cref="Vector3"/> to return as a formatted string.</param>
/// <returns><see cref="string"/></returns>
protected string GetVector3Values(ref Vector3 vector3)
{
return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})";
}

/// <summary>
/// Returns a <see cref="Vector3"/> as a formatted string.
/// </summary>
/// <param name="vector3"><see cref="Vector3"/> to return as a formatted string.</param>
/// <returns><see cref="string"/></returns>
protected string GetVector3Values(Vector3 vector3)
{
return GetVector3Values(ref vector3);
}

protected virtual float GetDeltaVarianceThreshold()
{
return k_AproximateDeltaVariance;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Collections;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;

namespace Unity.Netcode.RuntimeTests
{
[TestFixture(NetworkSpawnTypes.OnNetworkSpawn)]
[TestFixture(NetworkSpawnTypes.OnNetworkPostSpawn)]
internal class ParentingDuringSpawnTests : IntegrationTestWithApproximation
{
protected override int NumberOfClients => 2;

public enum NetworkSpawnTypes
{
OnNetworkSpawn,
OnNetworkPostSpawn,
}

private NetworkSpawnTypes m_NetworkSpawnType;

private GameObject m_ParentPrefab;
private GameObject m_ChildPrefab;
private NetworkObject m_AuthorityInstance;
private List<NetworkManager> m_NetworkManagers = new List<NetworkManager>();
private StringBuilder m_Errors = new StringBuilder();

public class ParentDuringSpawnBehaviour : NetworkBehaviour
{
public GameObject ChildToSpawn;

public NetworkSpawnTypes NetworkSpawnType;

public Transform ChildSpawnPoint;

private void SpawnThenParent()
{
var child = NetworkObject.InstantiateAndSpawn(ChildToSpawn, NetworkManager, position: ChildSpawnPoint.position, rotation: ChildSpawnPoint.rotation);
if (!child.TrySetParent(NetworkObject))
{
var errorMessage = $"[{ChildToSpawn}] Failed to parent child {child.name} under parent {gameObject.name}!";
Debug.LogError(errorMessage);
}
}

public override void OnNetworkSpawn()
{
if (IsServer && NetworkSpawnType == NetworkSpawnTypes.OnNetworkSpawn)
{
SpawnThenParent();
}

base.OnNetworkSpawn();
}

protected override void OnNetworkPostSpawn()
{
if (IsServer && NetworkSpawnType == NetworkSpawnTypes.OnNetworkPostSpawn)
{
SpawnThenParent();
}
base.OnNetworkPostSpawn();
}
}

public ParentingDuringSpawnTests(NetworkSpawnTypes networkSpawnType) : base()
{
m_NetworkSpawnType = networkSpawnType;
}

protected override void OnServerAndClientsCreated()
{
m_ParentPrefab = CreateNetworkObjectPrefab("Parent");
m_ChildPrefab = CreateNetworkObjectPrefab("Child");
var parentComponet = m_ParentPrefab.AddComponent<ParentDuringSpawnBehaviour>();
parentComponet.ChildToSpawn = m_ChildPrefab;
var spawnPoint = new GameObject();
parentComponet.ChildSpawnPoint = spawnPoint.transform;
parentComponet.ChildSpawnPoint.position = GetRandomVector3(-5.0f, 5.0f);
var rotation = parentComponet.ChildSpawnPoint.rotation;
rotation.eulerAngles = GetRandomVector3(-180.0f, 180.0f);
parentComponet.ChildSpawnPoint.rotation = rotation;
base.OnServerAndClientsCreated();
}

private bool NonAuthorityInstancesSpawnedParent()
{
foreach (var networkManager in m_NetworkManagers)
{
if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId))
{
return false;
}
}
return true;
}

private bool NonAuthorityInstancesParentedChild()
{
m_Errors.Clear();
if (m_AuthorityInstance.transform.childCount == 0)
{
return false;
}
var authorityChildObject = m_AuthorityInstance.transform.GetChild(0).GetComponent<NetworkObject>();

foreach (var networkManager in m_NetworkManagers)
{
if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(authorityChildObject.NetworkObjectId))
{
m_Errors.AppendLine($"{networkManager.name} has not spawned the child {authorityChildObject.name}!");
return false;
}
var childObject = networkManager.SpawnManager.SpawnedObjects[authorityChildObject.NetworkObjectId];

if (childObject.transform.parent == null)
{
m_Errors.AppendLine($"{childObject.name} does not have a parent!");
return false;
}

if (!Approximately(authorityChildObject.transform.position, childObject.transform.position))
{
m_Errors.AppendLine($"{childObject.name} position {GetVector3Values(childObject.transform.position)} does " +
$"not match the authority's position {GetVector3Values(authorityChildObject.transform.position)}!");
return false;
}

if (!Approximately(authorityChildObject.transform.rotation, childObject.transform.rotation))
{
m_Errors.AppendLine($"{childObject.name} rotation {GetVector3Values(childObject.transform.rotation.eulerAngles)} does " +
$"not match the authority's position {GetVector3Values(authorityChildObject.transform.rotation.eulerAngles)}!");
return false;
}
}
return true;
}

[UnityTest]
public IEnumerator ParentDuringSpawn()
{
m_NetworkManagers.Clear();
var authorityNetworkManager = m_ServerNetworkManager;

m_NetworkManagers.AddRange(m_ClientNetworkManagers);
m_NetworkManagers.Add(m_ServerNetworkManager);

m_AuthorityInstance = SpawnObject(m_ParentPrefab, authorityNetworkManager).GetComponent<NetworkObject>();

yield return WaitForConditionOrTimeOut(NonAuthorityInstancesSpawnedParent);
AssertOnTimeout($"Not all clients spawned the parent {nameof(NetworkObject)}!");

yield return WaitForConditionOrTimeOut(NonAuthorityInstancesParentedChild);
AssertOnTimeout($"Non-Authority instance had a mismatched value: \n {m_Errors}");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,6 @@ protected override float GetDeltaVarianceThreshold()

private StringBuilder m_ValidationErrors;

private string GetVector3Values(ref Vector3 vector3)
{
return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})";
}

/// <summary>
/// Validates all transform instance values match the authority's
Expand Down