Skip to content

Commit 3625247

Browse files
fix: client-server owner authoritative nested NetworkTransform invalid synchronization (#3099)
* fix This fixes the issue when using a client-server network topology spawning a player with nested NetworkTransform components would result in loss of the child NetworkTransform component(s) settings/flags which would end up being serialized to the owner client (thus causing invalid synchronization results). * update Adding the changelog entry. * fix The issue is not just for local players but for all prefabs that have nested NetworkTransforms, are owner authoritative, are not owned by the server, and the session instance is using a client-server network topology. * test Adding a test to validate this fix. * update adding PR number to changelog entry
1 parent 5f7441c commit 3625247

File tree

3 files changed

+308
-1
lines changed

3 files changed

+308
-1
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1818

1919
### Fixed
2020

21+
- Fixed issue with nested `NetworkTransform` components clearing their initial prefab settings when in owner authoritative mode on the server side while using a client-server network topology which resulted in improper synchronization of the nested `NetworkTransform` components. (#3099)
2122
- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096)
2223
- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092)
2324
- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078)

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,6 @@ protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
14871487
HalfVectorRotation = new HalfVector4(),
14881488
HalfVectorScale = new HalfVector3(),
14891489
NetworkDeltaPosition = new NetworkDeltaPosition(),
1490-
14911490
};
14921491

14931492
if (serializer.IsWriter)
@@ -3050,12 +3049,44 @@ protected internal override void InternalOnNetworkSessionSynchronized()
30503049
base.InternalOnNetworkSessionSynchronized();
30513050
}
30523051

3052+
private void ApplyPlayerTransformState()
3053+
{
3054+
SynchronizeState.InLocalSpace = InLocalSpace;
3055+
SynchronizeState.UseInterpolation = Interpolate;
3056+
SynchronizeState.QuaternionSync = UseQuaternionSynchronization;
3057+
SynchronizeState.UseHalfFloatPrecision = UseHalfFloatPrecision;
3058+
SynchronizeState.QuaternionCompression = UseQuaternionCompression;
3059+
SynchronizeState.UsePositionSlerp = SlerpPosition;
3060+
}
3061+
30533062
/// <summary>
30543063
/// For dynamically spawned NetworkObjects, when the non-authority instance's client is already connected and
30553064
/// the SynchronizeState is still pending synchronization then we want to finalize the synchornization at this time.
30563065
/// </summary>
30573066
protected internal override void InternalOnNetworkPostSpawn()
30583067
{
3068+
// This is a special case for client-server where a server is spawning an owner authoritative NetworkObject but has yet to serialize anything.
3069+
// When the server detects that:
3070+
// - We are not in a distributed authority session (DAHost check).
3071+
// - This is the first/root NetworkTransform.
3072+
// - We are in owner authoritative mode.
3073+
// - The NetworkObject is not owned by the server.
3074+
// - The SynchronizeState.IsSynchronizing is set to false.
3075+
// Then we want to:
3076+
// - Force the "IsSynchronizing" flag so the NetworkTransform has its state updated properly and runs through the initialization again.
3077+
// - Make sure the SynchronizingState is updated to the instantiated prefab's default flags/settings.
3078+
if (NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode && m_IsFirstNetworkTransform && !OnIsServerAuthoritative() && !IsOwner && !SynchronizeState.IsSynchronizing)
3079+
{
3080+
// Assure the first/root NetworkTransform has the synchronizing flag set so the server runs through the final post initialization steps
3081+
SynchronizeState.IsSynchronizing = true;
3082+
// Assure the SynchronizeState matches the initial prefab's values for each associated NetworkTransfrom (this includes root + all children)
3083+
foreach (var child in NetworkObject.NetworkTransforms)
3084+
{
3085+
child.ApplyPlayerTransformState();
3086+
}
3087+
// Now fall through to the final synchronization portion of the spawning for NetworkTransform
3088+
}
3089+
30593090
if (!CanCommitToTransform && NetworkManager.IsConnectedClient && SynchronizeState.IsSynchronizing)
30603091
{
30613092
NonAuthorityFinalizeSynchronization();

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Text;
56
using NUnit.Framework;
67
using Unity.Netcode.Components;
78
using Unity.Netcode.TestHelpers.Runtime;
@@ -539,5 +540,279 @@ protected override bool OnIsServerAuthoritative()
539540
}
540541
}
541542
}
543+
544+
[TestFixture(HostOrServer.DAHost, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using distributed authority
545+
[TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Server)] // Validate we have not impacted NetworkTransform server authoritative mode
546+
[TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using client-server
547+
internal class NestedNetworkTransformTests : IntegrationTestWithApproximation
548+
{
549+
private const int k_NestedChildren = 5;
550+
protected override int NumberOfClients => 2;
551+
552+
private GameObject m_SpawnObject;
553+
554+
private NetworkTransform.AuthorityModes m_AuthorityMode;
555+
556+
private StringBuilder m_ErrorLog = new StringBuilder();
557+
558+
private List<NetworkManager> m_NetworkManagers = new List<NetworkManager>();
559+
private List<GameObject> m_SpawnedObjects = new List<GameObject>();
560+
561+
public NestedNetworkTransformTests(HostOrServer hostOrServer, NetworkTransform.AuthorityModes authorityMode) : base(hostOrServer)
562+
{
563+
m_AuthorityMode = authorityMode;
564+
}
565+
566+
/// <summary>
567+
/// Creates a player prefab with several nested NetworkTransforms
568+
/// </summary>
569+
protected override void OnCreatePlayerPrefab()
570+
{
571+
var networkTransform = m_PlayerPrefab.AddComponent<NetworkTransform>();
572+
networkTransform.AuthorityMode = m_AuthorityMode;
573+
var parent = m_PlayerPrefab;
574+
// Add several nested NetworkTransforms
575+
for (int i = 0; i < k_NestedChildren; i++)
576+
{
577+
var nestedChild = new GameObject();
578+
nestedChild.transform.parent = parent.transform;
579+
var nestedNetworkTransform = nestedChild.AddComponent<NetworkTransform>();
580+
nestedNetworkTransform.AuthorityMode = m_AuthorityMode;
581+
nestedNetworkTransform.InLocalSpace = true;
582+
parent = nestedChild;
583+
}
584+
base.OnCreatePlayerPrefab();
585+
}
586+
587+
private void RandomizeObjectTransformPositions(GameObject gameObject)
588+
{
589+
var networkObject = gameObject.GetComponent<NetworkObject>();
590+
Assert.True(networkObject.ChildNetworkBehaviours.Count > 0);
591+
592+
foreach (var networkTransform in networkObject.NetworkTransforms)
593+
{
594+
networkTransform.gameObject.transform.position = GetRandomVector3(-15.0f, 15.0f);
595+
}
596+
}
597+
598+
/// <summary>
599+
/// Randomizes each player's position when validating distributed authority
600+
/// </summary>
601+
/// <returns></returns>
602+
private GameObject FetchLocalPlayerPrefabToSpawn()
603+
{
604+
RandomizeObjectTransformPositions(m_PlayerPrefab);
605+
return m_PlayerPrefab;
606+
}
607+
608+
/// <summary>
609+
/// Randomizes the player position when validating client-server
610+
/// </summary>
611+
/// <param name="connectionApprovalRequest"></param>
612+
/// <param name="connectionApprovalResponse"></param>
613+
private void ConnectionApprovalHandler(NetworkManager.ConnectionApprovalRequest connectionApprovalRequest, NetworkManager.ConnectionApprovalResponse connectionApprovalResponse)
614+
{
615+
connectionApprovalResponse.Approved = true;
616+
connectionApprovalResponse.CreatePlayerObject = true;
617+
RandomizeObjectTransformPositions(m_PlayerPrefab);
618+
connectionApprovalResponse.Position = GetRandomVector3(-15.0f, 15.0f);
619+
}
620+
621+
protected override void OnServerAndClientsCreated()
622+
{
623+
// Create a prefab to spawn with each NetworkManager as the owner
624+
m_SpawnObject = CreateNetworkObjectPrefab("SpawnObj");
625+
var networkTransform = m_SpawnObject.AddComponent<NetworkTransform>();
626+
networkTransform.AuthorityMode = m_AuthorityMode;
627+
var parent = m_SpawnObject;
628+
// Add several nested NetworkTransforms
629+
for (int i = 0; i < k_NestedChildren; i++)
630+
{
631+
var nestedChild = new GameObject();
632+
nestedChild.transform.parent = parent.transform;
633+
var nestedNetworkTransform = nestedChild.AddComponent<NetworkTransform>();
634+
nestedNetworkTransform.AuthorityMode = m_AuthorityMode;
635+
nestedNetworkTransform.InLocalSpace = true;
636+
parent = nestedChild;
637+
}
638+
639+
if (m_DistributedAuthority)
640+
{
641+
if (!UseCMBService())
642+
{
643+
m_ServerNetworkManager.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn;
644+
}
645+
646+
foreach (var client in m_ClientNetworkManagers)
647+
{
648+
client.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn;
649+
}
650+
}
651+
else
652+
{
653+
m_ServerNetworkManager.NetworkConfig.ConnectionApproval = true;
654+
m_ServerNetworkManager.ConnectionApprovalCallback += ConnectionApprovalHandler;
655+
foreach (var client in m_ClientNetworkManagers)
656+
{
657+
client.NetworkConfig.ConnectionApproval = true;
658+
}
659+
}
660+
661+
base.OnServerAndClientsCreated();
662+
}
663+
664+
/// <summary>
665+
/// Validates the transform positions of two NetworkObject instances
666+
/// </summary>
667+
/// <param name="current">the local instance (source of truth)</param>
668+
/// <param name="testing">the remote instance</param>
669+
/// <returns></returns>
670+
private bool ValidateTransforms(NetworkObject current, NetworkObject testing)
671+
{
672+
if (current.ChildNetworkBehaviours.Count == 0 || testing.ChildNetworkBehaviours.Count == 0)
673+
{
674+
return false;
675+
}
676+
677+
for (int i = 0; i < current.NetworkTransforms.Count - 1; i++)
678+
{
679+
var transformA = current.NetworkTransforms[i].transform;
680+
var transformB = testing.NetworkTransforms[i].transform;
681+
if (!Approximately(transformA.position, transformB.position))
682+
{
683+
m_ErrorLog.AppendLine($"TransformA Position {transformA.position} != TransformB Position {transformB.position}");
684+
return false;
685+
}
686+
if (!Approximately(transformA.localPosition, transformB.localPosition))
687+
{
688+
m_ErrorLog.AppendLine($"TransformA Local Position {transformA.position} != TransformB Local Position {transformB.position}");
689+
return false;
690+
}
691+
if (transformA.parent != null)
692+
{
693+
if (current.NetworkTransforms[i].InLocalSpace != testing.NetworkTransforms[i].InLocalSpace)
694+
{
695+
m_ErrorLog.AppendLine($"NetworkTransform-{current.OwnerClientId}-{current.NetworkTransforms[i].NetworkBehaviourId} InLocalSpace ({current.NetworkTransforms[i].InLocalSpace}) is different from the remote instance version on Client-{testing.NetworkManager.LocalClientId}!");
696+
return false;
697+
}
698+
}
699+
}
700+
return true;
701+
}
702+
703+
/// <summary>
704+
/// Validates all player instances spawned with the correct positions including all nested NetworkTransforms
705+
/// When running in server authority mode we are validating this fix did not impact that.
706+
/// </summary>
707+
private bool AllClientInstancesSynchronized()
708+
{
709+
m_ErrorLog.Clear();
710+
711+
foreach (var current in m_NetworkManagers)
712+
{
713+
var currentPlayer = current.LocalClient.PlayerObject;
714+
var currentNetworkObjectId = currentPlayer.NetworkObjectId;
715+
foreach (var testing in m_NetworkManagers)
716+
{
717+
if (currentPlayer == testing.LocalClient.PlayerObject)
718+
{
719+
continue;
720+
}
721+
722+
if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId))
723+
{
724+
m_ErrorLog.AppendLine($"Failed to find Client-{currentPlayer.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!");
725+
return false;
726+
}
727+
728+
var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId];
729+
if (!ValidateTransforms(currentPlayer, remoteInstance))
730+
{
731+
m_ErrorLog.AppendLine($"Failed to validate Client-{currentPlayer.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!");
732+
return false;
733+
}
734+
}
735+
}
736+
return true;
737+
}
738+
739+
/// <summary>
740+
/// Validates that dynamically spawning works the same.
741+
/// When running in server authority mode we are validating this fix did not impact that.
742+
/// </summary>
743+
/// <returns></returns>
744+
private bool AllSpawnedObjectsSynchronized()
745+
{
746+
m_ErrorLog.Clear();
747+
748+
foreach (var current in m_SpawnedObjects)
749+
{
750+
var currentNetworkObject = current.GetComponent<NetworkObject>();
751+
var currentNetworkObjectId = currentNetworkObject.NetworkObjectId;
752+
foreach (var testing in m_NetworkManagers)
753+
{
754+
if (currentNetworkObject.OwnerClientId == testing.LocalClientId)
755+
{
756+
continue;
757+
}
758+
759+
if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId))
760+
{
761+
m_ErrorLog.AppendLine($"Failed to find Client-{currentNetworkObject.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!");
762+
return false;
763+
}
764+
765+
var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId];
766+
if (!ValidateTransforms(currentNetworkObject, remoteInstance))
767+
{
768+
m_ErrorLog.AppendLine($"Failed to validate Client-{currentNetworkObject.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!");
769+
return false;
770+
}
771+
}
772+
}
773+
return true;
774+
}
775+
776+
/// <summary>
777+
/// Validates that spawning player and dynamically spawned prefab instances with nested NetworkTransforms
778+
/// synchronizes properly in both client-server and distributed authority when using owner authoritative mode.
779+
/// </summary>
780+
[UnityTest]
781+
public IEnumerator NestedNetworkTransformSpawnPositionTest()
782+
{
783+
if (!m_DistributedAuthority || (m_DistributedAuthority && !UseCMBService()))
784+
{
785+
m_NetworkManagers.Add(m_ServerNetworkManager);
786+
}
787+
m_NetworkManagers.AddRange(m_ClientNetworkManagers);
788+
789+
yield return WaitForConditionOrTimeOut(AllClientInstancesSynchronized);
790+
AssertOnTimeout($"Failed to synchronize all client instances!\n{m_ErrorLog}");
791+
792+
foreach (var networkManager in m_NetworkManagers)
793+
{
794+
// Randomize the position
795+
RandomizeObjectTransformPositions(m_SpawnObject);
796+
797+
// Create an instance owned by the specified networkmanager
798+
m_SpawnedObjects.Add(SpawnObject(m_SpawnObject, networkManager));
799+
}
800+
// Randomize the position once more just to assure we are instantiating remote instances
801+
// with a completely different position
802+
RandomizeObjectTransformPositions(m_SpawnObject);
803+
yield return WaitForConditionOrTimeOut(AllSpawnedObjectsSynchronized);
804+
AssertOnTimeout($"Failed to synchronize all spawned NetworkObject instances!\n{m_ErrorLog}");
805+
m_SpawnedObjects.Clear();
806+
m_NetworkManagers.Clear();
807+
}
808+
809+
protected override IEnumerator OnTearDown()
810+
{
811+
// In case there was a failure, go ahead and clear these lists out for any pending TextFixture passes
812+
m_SpawnedObjects.Clear();
813+
m_NetworkManagers.Clear();
814+
return base.OnTearDown();
815+
}
816+
}
542817
}
543818
#endif

0 commit comments

Comments
 (0)