diff --git a/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs index 423ff4a65c..f7529de12c 100644 --- a/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs +++ b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs @@ -1,3 +1,4 @@ +using Unity.Netcode.Logging; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -19,8 +20,22 @@ internal class SetInScenePlaced : IProcessSceneWithReport public int callbackOrder => 0; public void OnProcessScene(Scene scene, BuildReport report) { + var log = new ContextualLogger(); + log.AddInfo(scene.name, scene.handle); foreach (var networkObject in FindObjects.FromSceneByType(scene, true)) { + if (networkObject.SceneOrigin.IsValid() && networkObject.SceneOrigin.handle != scene.handle) + { + log.Warning(new Context(LogLevel.Developer, $"{nameof(NetworkObject)}'s SceneOrigin doesn't match current scene being processed! Skipping processing.").AddInfo("SceneOrigin", networkObject.SceneOriginHandle).AddNetworkObject(networkObject)); + continue; + } + + if (networkObject.HasBeenSpawned) + { + log.Error(new Context(LogLevel.Normal, $"Processing {nameof(NetworkObject)} that has already been spawned! This should not be possible. Skipping processing.").AddNetworkObject(networkObject)); + continue; + } + networkObject.InScenePlaced = true; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs index c6f30d2835..b1c071e705 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs @@ -166,6 +166,22 @@ public bool Validate(int index = -1) return false; } + if (networkObject.InScenePlaced) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"{NetworkPrefabHandler.PrefabDebugHelper(this)} InScenePlaced {nameof(NetworkObject)} is being registered as a Prefab. This can cause issues!"); + } + } + + if (networkObject.IsSpawned) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"{NetworkPrefabHandler.PrefabDebugHelper(this)} Currently spawned {nameof(NetworkObject)} is being registered as a Prefab. This can cause issues!"); + } + } + return true; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 6a9397e48e..985dd5c9af 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using Unity.Netcode.Components; +using Unity.Netcode.Logging; using Unity.Netcode.Runtime; #if UNITY_EDITOR using UnityEditor; @@ -106,6 +107,9 @@ public uint PrefabIdHash /// public NetworkObject CurrentParent { get; private set; } + private int m_SpawnCount; + internal bool HasBeenSpawned => m_SpawnCount > 0; + #if UNITY_EDITOR private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; @@ -1448,12 +1452,16 @@ internal Scene SceneOrigin set { + if (SceneOriginHandle.IsEmpty() && value.IsValid() && value.isLoaded) + { + SceneOriginHandle = value.handle; + } + // The scene origin should only be set once. // Once set, it should never change. - if (SceneOriginHandle.IsEmpty() && value.IsValid() && value.isLoaded) + if (!m_SceneOrigin.IsValid()) { m_SceneOrigin = value; - SceneOriginHandle = value.handle; } } } @@ -1782,7 +1790,7 @@ private void OnDestroy() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) + private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (NetworkManagerOwner == null) { @@ -1853,7 +1861,28 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } } - if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), InScenePlaced, playerObject, ownerClientId, destroyWithScene)) + + // Calculate the legacy IsSceneObject value as the public field is obsolete with warning + // We can't break the public behavior of the field. +#pragma warning disable CS0618 // Type or member is obsolete + var legacyIsSceneObject = IsSceneObject.HasValue && IsSceneObject.Value; +#pragma warning restore CS0618 // Type or member is obsolete + + // If SpawnInternal is being called on an object that is marked as InScenePlaced, + // The scene object was never automatically spawned when the scene was loaded. + // Count this object as a dynamically spawned object. + // TODO-[MTT-15388]: Actually support disabled/not spawned InScenePlaced NetworkObjects + if (InScenePlaced && m_SpawnCount == 0) + { + if (NetworkManagerOwner.NetworkConfig.EnableSceneManagement && NetworkManagerOwner.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"[{name}][SceneOrigin={SceneOriginHandle}] Dynamically spawning InScenePlaced network object. This can cause issues!", this); + } + + InScenePlaced = false; + } + + if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), legacyIsSceneObject, playerObject, ownerClientId, destroyWithScene)) { if (NetworkManagerOwner.LogLevel <= LogLevel.Normal) { @@ -2053,6 +2082,7 @@ internal void SetupOnSpawn(ulong networkId, bool isPlayerObject, ulong ownerClie // When spawned, previous owner is always the first assigned owner PreviousOwnerId = ownerClientId; m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + m_SpawnCount++; IsSpawned = true; // If this is the player, and the client is the owner, then lock ownership by default @@ -3466,11 +3496,7 @@ internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject seri if (serializedObject.NetworkObjectId == default) { - if (networkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"[{nameof(GlobalObjectIdHash)}={serializedObject.Hash}] Received spawn request with invalid {nameof(NetworkObjectId)} {serializedObject.NetworkObjectId}. This should not happen!"); - } - + NetworkLog.LogErrorServer(new Context(LogLevel.Error, $"Received spawn request with invalid {nameof(NetworkObjectId)}. This should not happen!").AddInfo(nameof(NetworkObjectId), serializedObject.NetworkObjectId).AddInfo(nameof(GlobalObjectIdHash), serializedObject.Hash)); reader.Seek(endOfSynchronizationData); return null; } @@ -3627,17 +3653,17 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) NetworkSceneHandle = SceneOriginHandle; } } - else // Otherwise, the client did not find the client to server scene handle - if (NetworkManagerOwner.LogLevel <= LogLevel.Developer) - { - // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject - // into, but that scenario seemed very edge case and under most instances a user should be notified that this - // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to - // the server-side too. - NetworkLog.LogWarningServer($"[Client-{NetworkManagerOwner.LocalClientId}][{name}] Server - " + - $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" + - $"has no associated server side (network) scene handle!"); - } + // Otherwise, the client did not find the client to server scene handle + else if (NetworkManagerOwner.LogLevel <= LogLevel.Developer) + { + // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject + // into, but that scenario seemed very edge case and under most instances a user should be notified that this + // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to + // the server-side too. + NetworkLog.LogWarningServer($"[Client-{NetworkManagerOwner.LocalClientId}][{name}] Server - " + + $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" + + $"has no associated server side (network) scene handle!"); + } OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index df08bee61f..66a7df1f96 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -103,6 +103,8 @@ internal static void ConfigureIntegrationTestLogging(NetworkManager networkManag /// The message to log [HideInCallstack] public static void LogErrorServer(string message) => s_Log.ErrorServer(new Context(LogLevel.Error, message, true)); + [HideInCallstack] + internal static void LogErrorServer(Context context) => s_Log.ErrorServer(context); internal static LogType GetMessageLogType(UnityEngine.LogType engineLogType) { @@ -115,14 +117,14 @@ internal static LogType GetMessageLogType(UnityEngine.LogType engineLogType) }; } - private const string k_SenderId = "SenderId"; internal static Context BuildContextForServerMessage([NotNull] NetworkManager networkManager, LogLevel level, ulong senderId, string message) { - var ctx = new Context(level, message, true).AddInfo(k_SenderId, senderId); - if (TryGetNetworkObjectName(networkManager, message, out var name)) + var ctx = new Context(level, message, true).AddTag("Received log from client").AddInfo(k_SenderId, senderId); + var networkObject = TryGetNetworkObject(networkManager, message); + if (networkObject != null) { - ctx.AddTag(name); + ctx.AddNetworkObject(networkObject); } return ctx; } @@ -135,29 +137,41 @@ internal enum LogType : byte None } - private static readonly Regex k_GlobalObjectIdHash = new($@"\[{nameof(NetworkObject.GlobalObjectIdHash)}=(\d+)\]", RegexOptions.Compiled); + private static readonly Regex k_NetworkObjectId = new($@"\[{nameof(NetworkObject.NetworkObjectId)}:(\d+)\]", RegexOptions.Compiled); + private static readonly Regex k_GlobalObjectIdHash = new($@"\[{nameof(NetworkObject.GlobalObjectIdHash)}:(\d+)\]", RegexOptions.Compiled); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetNetworkObjectName([NotNull] NetworkManager networkManager, string message, out string name) + private static NetworkObject TryGetNetworkObject([NotNull] NetworkManager networkManager, string message) { - name = null; + if (k_NetworkObjectId.IsMatch(message)) + { + var stringId = k_NetworkObjectId.Match(message).Groups[1].Value; + if (ulong.TryParse(stringId, out var networkObjectId) && networkObjectId > 0 && networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var networkObject)) + { + return networkObject; + } + } + if (!k_GlobalObjectIdHash.IsMatch(message)) { - return false; + return null; } var stringHash = k_GlobalObjectIdHash.Match(message).Groups[1].Value; if (!ulong.TryParse(stringHash, out var globalObjectIdHash)) { - return false; + return null; } - if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(globalObjectIdHash, out var networkObject)) + NetworkObject matchingObject = null; + foreach (var networkObject in networkManager.SpawnManager.SpawnedObjectsList) { - return false; + if (networkObject.GlobalObjectIdHash == globalObjectIdHash) + { + matchingObject = networkObject; + } } - name = networkObject.name; - return true; + return matchingObject; } [HideInCallstack] diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 2f8ae30d03..d6a3eb028c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -957,6 +957,7 @@ internal NetworkObject InstantiateNetworkPrefab([NotNull] GameObject networkPref /// /// For most cases this is client-side only, except when the server is spawning a player. /// + [return: MaybeNull] internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject serializedObject, byte[] instantiationData = null) { NetworkObject networkObject = null; @@ -977,11 +978,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject s networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, serializedObject.NetworkSceneHandle); if (networkObject == null) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); - } - + NetworkLog.LogErrorServer(new Context(LogLevel.Error, $"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure!").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), globalObjectIdHash)); return null; } @@ -1122,7 +1119,14 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n return false; } - if (!sceneObject && NetworkManager.LogLevel <= LogLevel.Error) + if (playerObject && networkObject.InScenePlaced) + { + NetworkLog.LogError(new Context(LogLevel.Developer, "Player prefab is marked as belonging to a scene. This may cause issues.").AddNetworkObject(networkObject).AddInfo("SceneName", networkObject.SceneOrigin.name)); + networkObject.InScenePlaced = false; + } + NetworkLog.InternalAssert(sceneObject == networkObject.InScenePlaced, "Legacy sceneObject value should match calculated InScenePlaced value."); + + if (!networkObject.InScenePlaced && NetworkManager.LogLevel <= LogLevel.Error) { var networkObjectChildren = networkObject.GetComponentsInChildren(); if (networkObjectChildren.Length > 1) @@ -1137,7 +1141,7 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n networkObject.NetworkManagerOwner = NetworkManager; networkObject.InvokeBehaviourNetworkPreSpawn(); - if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) + if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && networkObject.InScenePlaced) { networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; @@ -1175,10 +1179,7 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize { if (SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"Trying to spawn a {nameof(NetworkObject)} with a {nameof(NetworkObject.NetworkObjectId)} of {serializedObject.NetworkObjectId} but an object with that id is already in the spawned list. This should not happen!"); - } + NetworkManager.Log.Warning(new Context(LogLevel.Error, $"Trying to spawn a {nameof(NetworkObject)} but an object with that {nameof(NetworkObject.NetworkObjectId)} is already in the spawned list. This should not happen!")); networkObject = null; return false; } @@ -1195,14 +1196,11 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize // Log the error that the NetworkObject failed to construct if (networkObject == null) { - if (NetworkManager.LogLevel <= LogLevel.Normal) - { - NetworkLog.LogError($"[{nameof(NetworkObject.GlobalObjectIdHash)}={serializedObject.Hash}] Failed to spawn {nameof(NetworkObject)}!"); - } - + NetworkManager.Log.ErrorServer(new Context(LogLevel.Normal, $"Failed to spawn {nameof(NetworkObject)}").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash)); return false; } + networkObject.InScenePlaced = serializedObject.IsSceneObject; networkObject.NetworkManagerOwner = NetworkManager; // This will get set again when the NetworkObject is spawned locally, but we set it here ahead of spawning @@ -1227,11 +1225,7 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize if (networkObject.IsSpawned) { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); - } - + NetworkManager.Log.ErrorServer(new Context(LogLevel.Normal, $"{nameof(NetworkObject)} is already spawned!").AddNetworkObject(networkObject)); // Mark the spawn as a success if the object is already spawned return true; } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs index 0e226ec115..f27f28d53f 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Text.RegularExpressions; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; @@ -193,7 +194,7 @@ private bool HaveLogsBeenReceived() return false; } - if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, $"[Netcode] [SenderId:{m_ClientNetworkManagers[0].LocalClientId}] [Invalid Destroy][{m_ClientPlayerName}][NetworkObjectId:{m_ClientNetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.")) + if (!NetcodeLogAssert.HasLogBeenReceived(LogType.Error, new Regex($"SenderId:{m_ClientNetworkManagers[0].LocalClientId}]"))) { return false; } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeLogAssert.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeLogAssert.cs index 72f9753ee1..e2c95d7450 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeLogAssert.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeLogAssert.cs @@ -222,6 +222,30 @@ public bool HasLogBeenReceived(LogType type, string message) return found; } + /// + /// Determines if a log message was logged or not. + /// + /// to check for. + /// containing the message to search for. + /// or + public bool HasLogBeenReceived(LogType type, Regex message) + { + var found = false; + lock (m_Lock) + { + foreach (var logEvent in AllLogs) + { + if (logEvent.LogType == type && message.IsMatch(logEvent.Message)) + { + found = true; + break; + } + } + } + return found; + } + + /// /// Clears out the log history that is searched. /// diff --git a/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset b/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset index 5d957b4c4e..d3f0f5a0e5 100644 --- a/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/testproject/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -13,19 +13,21 @@ MonoBehaviour: m_Name: Default Local Group m_EditorClassIdentifier: m_GroupName: Default Local Group - m_Data: - m_SerializedData: [] m_GUID: 93aa504d1b753cb41a8a779ae63f5795 m_SerializeEntries: + - m_GUID: 68d21678646384e6291bb2b568b5d95c + m_Address: Assets/Scenes/AddressableInSceneObject.unity + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 - m_GUID: ffa1ab8ed58b72343ad93116ded1700a m_Address: AddressableTestObject.prefab m_ReadOnly: 0 m_SerializedLabels: [] - m_MainAsset: {fileID: 0} - m_TargetAsset: {fileID: 0} + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: 75e5cd8b6bfca5d49a5818e70d71b64d, type: 2} m_SchemaSet: m_Schemas: - - {fileID: 11400000, guid: 6b8a226e7c996ad4b841b997672f866c, type: 2} - {fileID: 11400000, guid: 07e6ff907f5779a42b6e632340d10c58, type: 2} + - {fileID: 11400000, guid: 6b8a226e7c996ad4b841b997672f866c, type: 2} diff --git a/testproject/Assets/Prefabs/AddressableTestObject.prefab b/testproject/Assets/Prefabs/AddressableTestObject.prefab index 473b199037..2930d6b0f3 100644 --- a/testproject/Assets/Prefabs/AddressableTestObject.prefab +++ b/testproject/Assets/Prefabs/AddressableTestObject.prefab @@ -9,8 +9,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6175900562428407388} - - component: {fileID: 6175900562428407389} - component: {fileID: 6175900562428407386} + - component: {fileID: 6175900562428407389} m_Layer: 0 m_Name: AddressableTestObject m_TagString: Untagged @@ -25,14 +25,15 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 6175900562428407387} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &6175900562428407389 +--- !u!114 &6175900562428407386 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -41,12 +42,24 @@ MonoBehaviour: m_GameObject: {fileID: 6175900562428407387} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 6c8e5a8a448245769b69af7de9c47b21, type: 3} + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: - AnIntVal: 1234567 - AStringVal: 1234567 ---- !u!114 &6175900562428407386 + GlobalObjectIdHash: 4054942115 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + k__BackingField: 0 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!114 &6175900562428407389 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -55,10 +68,9 @@ MonoBehaviour: m_GameObject: {fileID: 6175900562428407387} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Script: {fileID: 11500000, guid: 6c8e5a8a448245769b69af7de9c47b21, type: 3} m_Name: m_EditorClassIdentifier: - GlobalObjectIdHash: 951099334 - AlwaysReplicateAsRoot: 0 - DontDestroyWithOwner: 0 - AutoObjectParentSync: 1 + ShowTopMostFoldoutHeaderGroup: 1 + AnIntVal: 1234567 + AStringVal: 1234567 diff --git a/testproject/Assets/Scenes/AddressableInSceneObject.unity b/testproject/Assets/Scenes/AddressableInSceneObject.unity new file mode 100644 index 0000000000..e9fea46774 --- /dev/null +++ b/testproject/Assets/Scenes/AddressableInSceneObject.unity @@ -0,0 +1,199 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &1879396995 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 6175900562428407386, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: GlobalObjectIdHash + value: 1346653293 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407387, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_Name + value: AddressableTestObject + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalPosition.x + value: 1.09747 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalPosition.z + value: 1.59747 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6175900562428407388, guid: ffa1ab8ed58b72343ad93116ded1700a, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: ffa1ab8ed58b72343ad93116ded1700a, type: 3} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1879396995} diff --git a/testproject/Assets/Scenes/AddressableInSceneObject.unity.meta b/testproject/Assets/Scenes/AddressableInSceneObject.unity.meta new file mode 100644 index 0000000000..dbdfd8aca5 --- /dev/null +++ b/testproject/Assets/Scenes/AddressableInSceneObject.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 68d21678646384e6291bb2b568b5d95c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: addressablebundle + assetBundleVariant: diff --git a/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs b/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs new file mode 100644 index 0000000000..575e38189a --- /dev/null +++ b/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; +using UnityEngine.SceneManagement; + +#if UNITY_6000_6_OR_NEWER && UNITY_EDITOR + [Scripting.LifecycleManagement.AutoStaticsCleanup] +#endif +public partial class MoveDynamicSpawnInAwake : MonoBehaviour +{ + public NetworkObject MovedObject { get; private set; } + private static readonly HashSet k_AlreadyMovedObjects = new(); + private void Awake() + { + var networkManager = NetworkManager.Singleton; + if (networkManager == null || !networkManager.IsListening) + { + return; + } + + foreach (var spawnedObject in networkManager.SpawnManager.SpawnedObjects.Values) + { + if (spawnedObject == null || !spawnedObject.IsSpawned || !spawnedObject.HasAuthority || spawnedObject.IsPlayerObject || spawnedObject.InScenePlaced) + { + continue; + } + + if (!k_AlreadyMovedObjects.Add(spawnedObject)) + { + continue; + } + + MovedObject = spawnedObject; + SceneManager.MoveGameObjectToScene(spawnedObject.gameObject, gameObject.scene); + return; + } + } +} diff --git a/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs.meta b/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs.meta new file mode 100644 index 0000000000..053fb00eb0 --- /dev/null +++ b/testproject/Assets/Scripts/MoveDynamicSpawnInAwake.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 566f22ce0b192499d8728df60af83423 \ No newline at end of file diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta index 46669dd13d..88bb22b242 100644 --- a/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity.meta @@ -1,7 +1,7 @@ fileFormatVersion: 2 -guid: abd4c8b51c445d54faa16c67ac973f1b +guid: bfb4addb64cf6455a88668eba2f759ba DefaultImporter: externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity new file mode 100644 index 0000000000..f5b385433f --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity @@ -0,0 +1,170 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &1786247463 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1786247465} + - component: {fileID: 1786247464} + m_Layer: 0 + m_Name: MoveDynamicSpawnInAwake + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1786247464 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1786247463} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 566f22ce0b192499d8728df60af83423, type: 3} + m_Name: + m_EditorClassIdentifier: TestProject::MoveDynamicSpawnInAwake +--- !u!4 &1786247465 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1786247463} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1.09747, y: 0, z: 1.59747} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1786247465} diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity.meta new file mode 100644 index 0000000000..46669dd13d --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: abd4c8b51c445d54faa16c67ac973f1b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/AddressablesTests.cs b/testproject/Assets/Tests/Runtime/AddressablesTests.cs index 4c9151c0f3..4f08597430 100644 --- a/testproject/Assets/Tests/Runtime/AddressablesTests.cs +++ b/testproject/Assets/Tests/Runtime/AddressablesTests.cs @@ -7,6 +7,8 @@ using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.SceneManagement; using UnityEngine.TestTools; using Assert = UnityEngine.Assertions.Assert; @@ -24,6 +26,7 @@ public class AddressablesTests : NetcodeIntegrationTest protected override bool m_TearDownIsACoroutine => false; private const string k_ValidObject = "AddressableTestObject.prefab"; + private const string k_ValidScene = "Assets/Scenes/AddressableInSceneObject.unity"; public AddressablesTests(HostOrServer hostOrServer) { @@ -56,6 +59,24 @@ private IEnumerator LoadAsset(AssetReferenceGameObject asset, NetcodeIntegration prefab.Result = handle.Result; } + private IEnumerator LoadSceneWithInSceneObject(AssetReference asset, NetcodeIntegrationTestHelpers.ResultWrapper prefab) + { + var handle = Addressables.LoadSceneAsync(asset, LoadSceneMode.Additive); + while (!handle.IsDone) + { + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + Assert.AreEqual(AsyncOperationStatus.Succeeded, handle.Status, "Addressables.LoadSceneAsync failed!"); + + foreach (var networkObject in FindObjects.FromSceneByType(handle.Result.Scene, false)) + { + prefab.Result = networkObject.gameObject; + break; + } + } + protected void StartWithAddressableAssetAdded() { StartServerAndClientsWithTimeTravel(); @@ -70,7 +91,7 @@ private void AddPrefab(GameObject prefab) } } - private void SpawnAndValidate(GameObject prefab, bool waitAndAddOnClient = false) + private void SpawnAndValidate(GameObject prefab, bool waitAndAddOnClient = false, bool wasLoadedFromScene = false) { // Have to spawn it ourselves. var serverObj = Object.Instantiate(prefab); @@ -80,7 +101,14 @@ private void SpawnAndValidate(GameObject prefab, bool waitAndAddOnClient = false // Prefabs loaded by addressables actually don't show up in this search. // Unlike other tests that make prefabs programmatically, those aren't added to the scene until they're instantiated - Assert.AreEqual(1, objs.Length); + var numExpected = 1; + if (wasLoadedFromScene) + { + // If prefab was loaded from the scene, there'll be an additional object found + numExpected++; + } + + Assert.AreEqual(numExpected, objs.Length); var startTime = MockTimeProvider.StaticRealTimeSinceStartup; @@ -91,7 +119,7 @@ private void SpawnAndValidate(GameObject prefab, bool waitAndAddOnClient = false // Since it's not added, after the CreateObjectMessage is received, it's not spawned yet // Verify that to be the case as a precondition. objs = FindObjects.ByType(); - Assert.AreEqual(1, objs.Length); + Assert.AreEqual(numExpected, objs.Length); WaitForConditionOrTimeOutWithTimeTravel(() => MockTimeProvider.StaticRealTimeSinceStartup - startTime >= m_ClientNetworkManagers[0].NetworkConfig.SpawnTimeout - 0.25); foreach (var client in m_ClientNetworkManagers) { @@ -100,12 +128,22 @@ private void SpawnAndValidate(GameObject prefab, bool waitAndAddOnClient = false } objs = FindObjects.ByType(); - Assert.AreEqual(NumberOfClients + 1, objs.Length); + Assert.AreEqual(NumberOfClients + numExpected, objs.Length); foreach (var obj in objs) { Assert.AreEqual(1234567, obj.AnIntVal); Assert.AreEqual("1234567", obj.AStringVal); Assert.AreEqual("12345671234567", obj.GetValue()); + + // TODO-[MTT-15388]: Object spawned from a scene should be InScenePlaced after this ticket + if (obj.IsSpawned) + { + Assert.IsFalse(obj.NetworkObject.InScenePlaced, "Object was dynamically spawned and should be marked as such!"); + } + else + { + Assert.IsTrue(obj.NetworkObject.InScenePlaced, "Object that was loaded from scene should have been marked as in-scene placed during loading!"); + } } } @@ -144,7 +182,7 @@ public IEnumerator WhenLoadingAValidObjectAfterStarting_SpawningItSucceedsOnServ } [UnityTest] - public IEnumerator WhenSpawningServerPrefabBeforeClientPrefabHasLoaded_SpawningItSucceedsOnServerAndClientAfterConfiguredDelay([Values(1, 2, 3)] int timeout) + public IEnumerator WhenSpawningServerPrefabBeforeClientPrefabHasLoaded_SpawningItSucceedsOnServerAndClientAfterDelay() { var asset = new AssetReferenceGameObject(k_ValidObject); @@ -152,7 +190,7 @@ public IEnumerator WhenSpawningServerPrefabBeforeClientPrefabHasLoaded_SpawningI m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; foreach (var client in m_ClientNetworkManagers) { - client.NetworkConfig.SpawnTimeout = timeout; + client.NetworkConfig.SpawnTimeout = 3; client.NetworkConfig.ForceSamePrefabs = false; } @@ -163,6 +201,32 @@ public IEnumerator WhenSpawningServerPrefabBeforeClientPrefabHasLoaded_SpawningI SpawnAndValidate(prefabResult.Result, true); } + + // TODO-[MTT-15388]: Reconsider whether this test should be valid + // Reported on Github issue https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues/4049 + [UnityTest] + public IEnumerator RegisteringPrefabFromLoadedAddressablesSceneWorks() + { + var asset = new AssetReference(k_ValidScene); + + CreateServerAndClients(); + foreach (var manager in m_NetworkManagers) + { + manager.NetworkConfig.ForceSamePrefabs = false; + } + + StartServerAndClientsWithTimeTravel(); + + var prefabResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); + yield return LoadSceneWithInSceneObject(asset, prefabResult); + + foreach (var manager in m_NetworkManagers) + { + manager.AddNetworkPrefab(prefabResult.Result); + } + + SpawnAndValidate(prefabResult.Result, wasLoadedFromScene: true); + } } } #endif diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs new file mode 100644 index 0000000000..807ff09eeb --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs @@ -0,0 +1,75 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.TestTools; + +namespace TestProject.RuntimeTests +{ + internal class InScenePlacedProcessorTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 0; + + private static readonly string k_DynamicObjectMoverScene = "MoveADynamicObjectInAwake"; + + private GameObject m_DynamicSpawnPrefab; + + protected override void OnServerAndClientsCreated() + { + m_DynamicSpawnPrefab = CreateNetworkObjectPrefab("DynamicSpawnObject"); + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator InScenePlacedProcessorSkipsMovedObject() + { + var authority = GetAuthorityNetworkManager(); + var spawnedObj = SpawnObject(m_DynamicSpawnPrefab, authority).GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(spawnedObj); + AssertOnTimeout("Timed out waiting for object to spawn!"); + + Assert.IsFalse(spawnedObj.InScenePlaced, "Dynamically spawned object should not be InScenePlaced!"); + + authority.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; + var status = authority.SceneManager.LoadScene(k_DynamicObjectMoverScene, LoadSceneMode.Additive); + Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start lading scene {k_DynamicObjectMoverScene} with status {status}!"); + yield return WaitForConditionOrTimeOut(() => m_SceneLoaded.IsValid()); + AssertOnTimeout("Timed out waiting for scene to load!"); + + var movers = FindObjects.ByType(); + foreach (var mover in movers) + { + var movedObject = mover.MovedObject; + Assert.IsFalse(movedObject == null); + Assert.IsFalse(movedObject.InScenePlaced, "Dynamically spawned object should not be re-processed as InScenePlaced!"); + Assert.AreEqual(mover.gameObject.scene, movedObject.gameObject.scene, "Object should have moved scenes!"); + } + + yield return CreateAndStartNewClient(); + AssertOnTimeout("Timed out waiting for late joining client!"); + + yield return WaitForSpawnedOnAllOrTimeOut(spawnedObj); + AssertOnTimeout("Timed out waiting for object to be spawned on late joining client!"); + } + + private Scene m_SceneLoaded; + private void SceneManager_OnSceneEvent(SceneEvent sceneEvent) + { + var authority = GetAuthorityNetworkManager(); + switch (sceneEvent.SceneEventType) + { + case SceneEventType.LoadComplete: + { + if (sceneEvent.ClientId == authority.LocalClientId) + { + m_SceneLoaded = sceneEvent.Scene; + } + return; + } + } + } + } +} diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs.meta b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs.meta new file mode 100644 index 0000000000..3d0a9439ec --- /dev/null +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObject/InScenePlacedProcessorTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9abce9656f174f768488bb052afa8d92 +timeCreated: 1783723861 \ No newline at end of file diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs index d5eb5f427c..9318e1d63e 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectSceneMigrationTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Text; using NUnit.Framework; using Unity.Netcode; using Unity.Netcode.TestHelpers.Runtime; @@ -13,27 +15,30 @@ namespace TestProject.RuntimeTests { /// /// NetworkObject Scene Migration Integration Tests - /// - /// /// - public class NetworkObjectSceneMigrationTests : NetcodeIntegrationTest + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] + internal class NetworkObjectSceneMigrationTests : NetcodeIntegrationTest { - private List m_TestScenes = new List() { "EmptyScene1", "EmptyScene2", "EmptyScene3" }; + private readonly List m_TestScenes = new() { "EmptyScene1", "EmptyScene2", "EmptyScene3" }; protected override int NumberOfClients => 2; + private GameObject m_TestPrefab; private GameObject m_TestPrefabAutoSynchActiveScene; private GameObject m_TestPrefabDestroyWithScene; private Scene m_OriginalActiveScene; - private bool m_ClientsLoadedScene; - private bool m_ClientsUnloadedScene; - private Scene m_SceneLoaded; private List m_ServerSpawnedPrefabInstances = new List(); private List m_ServerSpawnedDestroyWithSceneInstances = new List(); - private List m_ScenesLoaded = new List(); - private string m_CurrentSceneLoading; - private string m_CurrentSceneUnloading; + private readonly List m_ScenesLoaded = new List(); + + public NetworkObjectSceneMigrationTests(HostOrServer hostOrServer) : base(hostOrServer) { } + // TODO: [MTT-15430] Fix automatic scene object migration between clients + protected override bool UseCMBService() + { + return false; + } protected override IEnumerator OnSetup() { @@ -45,99 +50,81 @@ protected override void OnServerAndClientsCreated() { // Synchronize Scene Changes (default) Test Network Prefab m_TestPrefab = CreateNetworkObjectPrefab("TestObject"); + m_TestPrefab.AddComponent(); // Auto Synchronize Active Scene Changes Test Network Prefab - m_TestPrefabAutoSynchActiveScene = CreateNetworkObjectPrefab("ASASObject"); + m_TestPrefabAutoSynchActiveScene = CreateNetworkObjectPrefab("ActiveSceneSynchronizationObject"); m_TestPrefabAutoSynchActiveScene.GetComponent().ActiveSceneSynchronization = true; + m_TestPrefabAutoSynchActiveScene.AddComponent(); // Destroy With Scene Test Network Prefab - m_TestPrefabDestroyWithScene = CreateNetworkObjectPrefab("DWSObject"); + m_TestPrefabDestroyWithScene = CreateNetworkObjectPrefab("DestroyWithSceneObject"); m_TestPrefabDestroyWithScene.AddComponent(); + m_TestPrefabDestroyWithScene.AddComponent(); - DestroyWithSceneInstancesTestHelper.ShouldNeverSpawn = m_TestPrefabDestroyWithScene; + var neverSpawnObj = CreateNetworkObjectPrefab("ShouldNeverSpawn"); + var shouldNeverSpawn = neverSpawnObj.AddComponent(); + DestroyWithSceneInstancesTestHelper.ShouldNeverSpawn = shouldNeverSpawn; base.OnServerAndClientsCreated(); } - private bool DidClientsSpawnInstance(NetworkObject serverObject, bool checkDestroyWithScene = false) + private enum ExpectedLoadType { - foreach (var networkManager in m_ClientNetworkManagers) - { - if (!s_GlobalNetworkObjects.ContainsKey(networkManager.LocalClientId)) - { - return false; - } - var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId]; - if (!clientNetworkObjects.ContainsKey(serverObject.NetworkObjectId)) - { - return false; - } + Loaded, + Unloaded + } - if (checkDestroyWithScene) + private bool ValidateSceneOnAllClients(StringBuilder errorLog, string sceneName, ExpectedLoadType loadType) + { + var allValid = true; + foreach (var networkManager in m_NetworkManagers) + { + var sceneLoaded = false; + foreach (var scene in networkManager.SceneManager.ScenesLoaded.Values) { - if (serverObject.DestroyWithScene != clientNetworkObjects[serverObject.NetworkObjectId]) + if (scene.name == sceneName) { - return false; + sceneLoaded = true; + break; } } - } - return true; - } - - private bool VerifyAllClientsSpawnedInstances() - { - foreach (var serverObject in m_ServerSpawnedPrefabInstances) - { - if (!DidClientsSpawnInstance(serverObject)) + if (!sceneLoaded && loadType == ExpectedLoadType.Loaded) { - return false; + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] scene {sceneName} wasn't loaded on this client!"); + allValid = false; } - } - - foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances) - { - if (!DidClientsSpawnInstance(serverObject, true)) + else if (sceneLoaded && loadType == ExpectedLoadType.Unloaded) { - return false; + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] scene {sceneName} was still loaded on this client!"); + allValid = false; } } - return true; + return allValid; } - private bool AreClientInstancesInTheRightScene(NetworkObject serverObject) + private bool VerifyAllScenesMatch(StringBuilder errorLog, List authorityInstances) { - foreach (var networkManager in m_ClientNetworkManagers) + foreach (var authorityInstance in authorityInstances) { - var clientNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId]; - if (clientNetworkObjects == null) - { - continue; - } - // If a networkObject is null then it was destroyed - if (clientNetworkObjects[serverObject.NetworkObjectId].gameObject.scene.name != serverObject.gameObject.scene.name) + foreach (var networkManager in m_NetworkManagers) { - return false; - } - } - return true; - } + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(authorityInstance.NetworkObjectId, out var instance)) + { + errorLog.AppendLine($"[{authorityInstance.name}] Client-{networkManager.LocalClientId} doesn't have a local version of network object {authorityInstance.name} with id {authorityInstance.NetworkObjectId}"); + return false; + } - private bool VerifySpawnedObjectsMigrated() - { - foreach (var serverObject in m_ServerSpawnedPrefabInstances) - { - if (!AreClientInstancesInTheRightScene(serverObject)) - { - return false; - } - } + if (instance.gameObject.scene.name != authorityInstance.gameObject.scene.name) + { + errorLog.AppendLine($"[{instance.name}] NetworkObject-{authorityInstance.NetworkObjectId} is in the wrong scene! Expected: {authorityInstance.gameObject.scene.name}, Actual: {instance.gameObject.scene.name}"); + return false; + } - foreach (var serverObject in m_ServerSpawnedDestroyWithSceneInstances) - { - if (!AreClientInstancesInTheRightScene(serverObject)) - { - return false; + // The SceneOrigin should never change + var originalSceneTracker = instance.GetComponent(); + Assert.AreEqual(originalSceneTracker.SceneWhereAwakeHappened, (NetworkSceneHandle)instance.SceneOrigin.handle, "The SceneOrigin of an object should never change!"); } } @@ -154,28 +141,34 @@ private bool VerifySpawnedObjectsMigrated() [UnityTest] public IEnumerator MigrateIntoNewSceneTest() { + var authority = GetAuthorityNetworkManager(); + + var authoritySpawnedInstances = new List(); // Spawn 9 NetworkObject instances for (int i = 0; i < k_MaxObjectsToSpawn; i++) { - var serverInstance = Object.Instantiate(m_TestPrefab); - var serverNetworkObject = serverInstance.GetComponent(); - serverNetworkObject.Spawn(); - m_ServerSpawnedPrefabInstances.Add(serverNetworkObject); + var instance = SpawnObject(m_TestPrefab, authority); + var spawnedObject = instance.GetComponent(); + authoritySpawnedInstances.Add(spawnedObject); } - yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances); + + yield return WaitForSpawnedOnAllOrTimeOut(authoritySpawnedInstances); AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!"); // Now load three scenes to migrate the newly spawned NetworkObjects into - m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; - for (int i = 0; i < 3; i++) + authority.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; + foreach (var sceneToLoad in m_TestScenes) { - m_ClientsLoadedScene = false; - m_CurrentSceneLoading = m_TestScenes[i]; - var status = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive); - Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {status}"); - yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene); - AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!"); + var status = authority.SceneManager.LoadScene(sceneToLoad, LoadSceneMode.Additive); + Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start loading scene {sceneToLoad}! Return status: {status}"); + yield return WaitForConditionOrTimeOut(errorLog => ValidateSceneOnAllClients(errorLog, sceneToLoad, ExpectedLoadType.Loaded)); + AssertOnTimeout($"Timed out waiting for all clients to load scene {sceneToLoad}!"); } + authority.SceneManager.OnSceneEvent -= SceneManager_OnSceneEvent; + Assert.AreEqual(m_TestScenes.Count, m_ScenesLoaded.Count, "Not all the test scenes were loaded!"); + + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); + AssertOnTimeout("[After spawn] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); var objectCount = 0; // Migrate each networkObject into one of the three scenes. @@ -183,31 +176,32 @@ public IEnumerator MigrateIntoNewSceneTest() foreach (var scene in m_ScenesLoaded) { // Now migrate the NetworkObject - SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount].gameObject, scene); - SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 1].gameObject, scene); - SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[objectCount + 2].gameObject, scene); + SceneManager.MoveGameObjectToScene(authoritySpawnedInstances[objectCount].gameObject, scene); + SceneManager.MoveGameObjectToScene(authoritySpawnedInstances[objectCount + 1].gameObject, scene); + SceneManager.MoveGameObjectToScene(authoritySpawnedInstances[objectCount + 2].gameObject, scene); objectCount += 3; } - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Register for the server-side client synchronization so we can send an object scene migration event at the same time // the new client begins to synchronize - m_ServerNetworkManager.SceneManager.OnSynchronize += MigrateObjects_OnSynchronize; + m_ServerSpawnedPrefabInstances = authoritySpawnedInstances; + authority.SceneManager.OnSynchronize += MigrateObjects_OnSynchronize; // Verify that a late joining client synchronizes properly even while new scene migrations occur // during its synchronization yield return CreateAndStartNewClient(); - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"[Late Joined Client] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Verify that a late joining client synchronizes properly even if we migrate // during its synchronization and despawn some of the NetworkObjects migrated. - m_ServerNetworkManager.SceneManager.OnSynchronize += MigrateAndDespawnObjects_OnSynchronize; + authority.SceneManager.OnSynchronize += MigrateAndDespawnObjects_OnSynchronize; yield return CreateAndStartNewClient(); - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"[Late Joined Client] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); } @@ -219,19 +213,27 @@ public IEnumerator MigrateIntoNewSceneTest() /// private void MigrateAndDespawnObjects_OnSynchronize(ulong clientId) { - var objectCount = 0; + var authority = GetAuthorityNetworkManager(); // Migrate the NetworkObjects into different scenes than they originally were migrated into for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--) { var scene = m_ScenesLoaded[i % m_ScenesLoaded.Count]; - SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[i].gameObject, scene); + var obj = m_ServerSpawnedPrefabInstances[i]; + if (m_DistributedAuthority) + { + // When the new client joins, authority will be distributed. + // Ensure we have the authority instance. + obj = GetAuthorityInstance(obj); + } + SceneManager.MoveGameObjectToScene(obj.gameObject, scene); // De-spawn every-other object if (i % 2 == 0) { - m_ServerSpawnedPrefabInstances[objectCount + i].Despawn(); + obj.Despawn(); m_ServerSpawnedPrefabInstances.RemoveAt(i); } } + authority.SceneManager.OnSynchronize -= MigrateObjects_OnSynchronize; } /// @@ -252,7 +254,7 @@ private void MigrateObjects_OnSynchronize(ulong clientId) } // Unsubscribe to this event for this part of the test - m_ServerNetworkManager.SceneManager.OnSynchronize -= MigrateObjects_OnSynchronize; + GetAuthorityNetworkManager().SceneManager.OnSynchronize -= MigrateObjects_OnSynchronize; } /// @@ -263,36 +265,36 @@ private void MigrateObjects_OnSynchronize(ulong clientId) [UnityTest] public IEnumerator ActiveSceneSynchronizationTest() { + var authority = GetAuthorityNetworkManager(); // Disable resynchronization for this test to avoid issues with trying // to synchronize them. NetworkSceneManager.DisableReSynchronization = true; + var autoSyncActive = new List(); // Spawn 3 NetworkObject instances that auto synchronize to active scene changes for (int i = 0; i < 3; i++) { - var serverInstance = Object.Instantiate(m_TestPrefabAutoSynchActiveScene); - var serverNetworkObject = serverInstance.GetComponent(); // We are also testing that objects marked to synchronize with changes to // the active scene and marked to destroy with scene =are destroyed= if // the scene being unloaded is currently the active scene and the scene that // the NetworkObjects reside within. - serverNetworkObject.Spawn(true); - m_ServerSpawnedPrefabInstances.Add(serverNetworkObject); + var serverInstance = SpawnObject(m_TestPrefabAutoSynchActiveScene, authority, true); + var serverNetworkObject = serverInstance.GetComponent(); + autoSyncActive.Add(serverNetworkObject); } - // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes // and ==should not be== destroyed with the scene (these should be the only remaining // instances) + var autoSyncInactive = new List(); for (int i = 0; i < 3; i++) { - var serverInstance = Object.Instantiate(m_TestPrefab); - var serverNetworkObject = serverInstance.GetComponent(); // This set of NetworkObjects will be used to verify that NetworkObjets // spawned with DestroyWithScene set to false will migrate into the current // active scene if the scene they currently reside within is destroyed and // is not the currently active scene. - serverNetworkObject.Spawn(); - m_ServerSpawnedPrefabInstances.Add(serverNetworkObject); + var serverInstance = SpawnObject(m_TestPrefab, authority); + var serverNetworkObject = serverInstance.GetComponent(); + autoSyncInactive.Add(serverNetworkObject); } // Spawn 3 NetworkObject instances that do not auto synchronize to active scene changes @@ -307,104 +309,103 @@ public IEnumerator ActiveSceneSynchronizationTest() serverNetworkObject.Spawn(true); m_ServerSpawnedDestroyWithSceneInstances.Add(serverNetworkObject); } + var authoritySpawnedInstances = new List(); + authoritySpawnedInstances.AddRange(autoSyncActive); + authoritySpawnedInstances.AddRange(autoSyncInactive); + authoritySpawnedInstances.AddRange(m_ServerSpawnedDestroyWithSceneInstances); - yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances); + yield return WaitForSpawnedOnAllOrTimeOut(authoritySpawnedInstances); AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!"); // Now load three scenes - m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; - for (int i = 0; i < 3; i++) + authority.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; + foreach (var sceneName in m_TestScenes) { - m_ClientsLoadedScene = false; - m_CurrentSceneLoading = m_TestScenes[i]; - var loadStatus = m_ServerNetworkManager.SceneManager.LoadScene(m_TestScenes[i], LoadSceneMode.Additive); - Assert.True(loadStatus == SceneEventProgressStatus.Started, $"Failed to start loading scene {m_CurrentSceneLoading}! Return status: {loadStatus}"); - yield return WaitForConditionOrTimeOut(() => m_ClientsLoadedScene); - AssertOnTimeout($"Timed out waiting for all clients to load scene {m_CurrentSceneLoading}!"); + var loadStatus = authority.SceneManager.LoadScene(sceneName, LoadSceneMode.Additive); + Assert.True(loadStatus == SceneEventProgressStatus.Started, $"Failed to start loading scene {sceneName}! Return status: {loadStatus}"); + yield return WaitForConditionOrTimeOut(errorLog => ValidateSceneOnAllClients(errorLog, sceneName, ExpectedLoadType.Loaded)); + AssertOnTimeout($"Timed out waiting for all clients to load scene {sceneName}!"); } + authority.SceneManager.OnSceneEvent -= SceneManager_OnSceneEvent; + var sceneToMigrateTo = m_ScenesLoaded[2]; // Migrate the instances that don't synchronize with active scene changes into the 3rd loaded scene // (We are making sure these stay in the same scene they are migrated into) - for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++) + foreach (var spawnedObject in autoSyncInactive) { - SceneManager.MoveGameObjectToScene(m_ServerSpawnedPrefabInstances[i].gameObject, m_ScenesLoaded[2]); + SceneManager.MoveGameObjectToScene(spawnedObject.gameObject, sceneToMigrateTo); } // Migrate the instances that don't synchronize with active scene changes and are destroyed with the // scene unloading into the 3rd loaded scene // (We are making sure these get destroyed when the scene is unloaded) - for (int i = 0; i < m_ServerSpawnedDestroyWithSceneInstances.Count; i++) + foreach (var spawnedObject in m_ServerSpawnedDestroyWithSceneInstances) { - SceneManager.MoveGameObjectToScene(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject, m_ScenesLoaded[2]); + SceneManager.MoveGameObjectToScene(spawnedObject.gameObject, sceneToMigrateTo); } // Make sure they migrated to the proper scene - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Now change the active scene - SceneManager.SetActiveScene(m_ScenesLoaded[1]); + var newActiveScene = m_ScenesLoaded[1]; + SceneManager.SetActiveScene(newActiveScene); // We have to do this Object.DontDestroyOnLoad(m_TestPrefabAutoSynchActiveScene); // First, make sure server-side scenes and client side scenes match - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Verify that the auto-active-scene synchronization NetworkObjects migrated to the newly // assigned active scene - for (int i = 0; i < 3; i++) + foreach (var obj in autoSyncActive) { - Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1], - $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name}!"); + Assert.AreEqual(newActiveScene, obj.gameObject.scene, $"{obj.gameObject.name} did not migrate into scene {newActiveScene.name}!"); } // Verify that the other NetworkObjects that don't synchronize with active scene changes did // not migrate into the active scene. - for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++) + foreach (var obj in autoSyncInactive) { - Assert.False(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1], - $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!"); + Assert.AreNotEqual(newActiveScene, obj.gameObject.scene, $"{obj.gameObject.name} migrated into scene {newActiveScene.name}!"); } - for (int i = 0; i < 3; i++) + foreach (var obj in m_ServerSpawnedDestroyWithSceneInstances) { - Assert.False(m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.scene == m_ScenesLoaded[1], - $"{m_ServerSpawnedDestroyWithSceneInstances[i].gameObject.name} migrated into scene {m_ScenesLoaded[1].name}!"); + Assert.AreNotEqual(newActiveScene, obj.gameObject.scene, $"{obj.gameObject.name} migrated into scene {newActiveScene.name}!"); } // Verify that a late joining client synchronizes properly and destroys the appropriate NetworkObjects yield return CreateAndStartNewClient(); - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"[Late Joined Client #1] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Now, unload the scene containing the NetworkObjects that don't synchronize with active scene changes DestroyWithSceneInstancesTestHelper.NetworkObjectDestroyed += OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed; - m_ClientsUnloadedScene = false; - m_CurrentSceneUnloading = m_ScenesLoaded[2].name; - var status = m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[2]); - Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start unloading scene {m_ScenesLoaded[2].name} with status {status}!"); - yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene); + var status = authority.SceneManager.UnloadScene(sceneToMigrateTo); + Assert.True(status == SceneEventProgressStatus.Started, $"Failed to start unloading scene {sceneToMigrateTo.name} with status {status}!"); + yield return WaitForConditionOrTimeOut(log => ValidateSceneOnAllClients(log, sceneToMigrateTo.name, ExpectedLoadType.Unloaded)); // Clean up any destroyed NetworkObjects - for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--) + for (int i = authoritySpawnedInstances.Count - 1; i >= 0; i--) { - if (m_ServerSpawnedPrefabInstances[i] == null) + if (authoritySpawnedInstances[i] == null) { - m_ServerSpawnedPrefabInstances.RemoveAt(i); + authoritySpawnedInstances.RemoveAt(i); } } - AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!"); - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + AssertOnTimeout($"Timed out waiting for all clients to unload scene {sceneToMigrateTo.name}!"); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // Verify that the NetworkObjects that don't synchronize with active scene changes but marked to not // destroy with the scene are migrated into the current active scene - for (int i = 3; i < m_ServerSpawnedPrefabInstances.Count; i++) + foreach (var obj in autoSyncInactive) { - Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.scene == m_ScenesLoaded[1], - $"{m_ServerSpawnedPrefabInstances[i].gameObject.name} did not migrate into scene {m_ScenesLoaded[1].name} but are in scene {m_ServerSpawnedPrefabInstances[i].gameObject.scene.name}!"); + Assert.True(obj.gameObject.scene == newActiveScene, $"{obj.gameObject.name} did not migrate into scene {newActiveScene.name} but are in scene {obj.gameObject.scene.name}!"); } // Verify all NetworkObjects that should have been destroyed with the scene unloaded were destroyed @@ -414,18 +415,16 @@ public IEnumerator ActiveSceneSynchronizationTest() // Now unload the active scene to verify all remaining NetworkObjects are migrated into the SceneManager // assigned active scene - m_ClientsUnloadedScene = false; - m_CurrentSceneUnloading = m_ScenesLoaded[1].name; - m_ServerNetworkManager.SceneManager.UnloadScene(m_ScenesLoaded[1]); - yield return WaitForConditionOrTimeOut(() => m_ClientsUnloadedScene); - AssertOnTimeout($"Timed out waiting for all clients to unload scene {m_CurrentSceneUnloading}!"); + authority.SceneManager.UnloadScene(newActiveScene); + yield return WaitForConditionOrTimeOut(log => ValidateSceneOnAllClients(log, sceneToMigrateTo.name, ExpectedLoadType.Unloaded)); + AssertOnTimeout($"Timed out waiting for all clients to unload scene {newActiveScene.name}!"); // Clean up any destroyed NetworkObjects - for (int i = m_ServerSpawnedPrefabInstances.Count - 1; i >= 0; i--) + for (int i = authoritySpawnedInstances.Count - 1; i >= 0; i--) { - if (m_ServerSpawnedPrefabInstances[i] == null) + if (authoritySpawnedInstances[i] == null) { - m_ServerSpawnedPrefabInstances.RemoveAt(i); + authoritySpawnedInstances.RemoveAt(i); } } @@ -433,22 +432,17 @@ public IEnumerator ActiveSceneSynchronizationTest() yield return CreateAndStartNewClient(); // Verify the late joining client spawns all instances - yield return WaitForConditionOrTimeOut(VerifyAllClientsSpawnedInstances); - AssertOnTimeout($"Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!"); + yield return WaitForSpawnedOnAllOrTimeOut(authoritySpawnedInstances); + AssertOnTimeout($"[Late Joined Client #2] Timed out waiting for all clients to spawn {nameof(NetworkObject)}s!"); // Verify the instances are in the correct scenes - yield return WaitForConditionOrTimeOut(VerifySpawnedObjectsMigrated); + yield return WaitForConditionOrTimeOut(errorLog => VerifyAllScenesMatch(errorLog, authoritySpawnedInstances)); AssertOnTimeout($"[Late Joined Client #2] Timed out waiting for all clients to migrate all NetworkObjects into the appropriate scenes!"); // All but 3 instances should be destroyed - Assert.True(m_ServerSpawnedPrefabInstances.Count == 3, $"{nameof(m_ServerSpawnedPrefabInstances)} still has a count of {m_ServerSpawnedPrefabInstances.Count} " + - $"NetworkObject instances!"); - Assert.True(m_ServerSpawnedDestroyWithSceneInstances.Count == 0, $"{nameof(m_ServerSpawnedDestroyWithSceneInstances)} still has a count of " + - $"{m_ServerSpawnedDestroyWithSceneInstances.Count} NetworkObject instances!"); - for (int i = 0; i < 3; i++) - { - Assert.True(m_ServerSpawnedPrefabInstances[i].gameObject.name.Contains(m_TestPrefab.gameObject.name), $"Expected {m_ServerSpawnedPrefabInstances[i].gameObject.name} to contain {m_TestPrefab.gameObject.name}!"); - } + Assert.IsEmpty(autoSyncActive.Where(obj => obj != null), $"All the NetworkObjects with {nameof(NetworkObject.ActiveSceneSynchronization)}=true should have been destroyed when the active scene was unloaded!"); + Assert.IsEmpty(m_ServerSpawnedDestroyWithSceneInstances.Where(obj => obj != null), $"All the NetworkObjects with {nameof(NetworkObject.DestroyWithScene)} should have been destroyed when the active scene was unloaded!"); + Assert.AreEqual(3, autoSyncInactive.Count(obj => obj != null), $"All the NetworkObjects with {nameof(NetworkObject.ActiveSceneSynchronization)}=false should have survived the active scene change!"); } /// @@ -462,30 +456,16 @@ private void OnNonActiveSynchDestroyWithSceneNetworkObjectDestroyed(NetworkObjec private void SceneManager_OnSceneEvent(SceneEvent sceneEvent) { + var authority = GetAuthorityNetworkManager(); switch (sceneEvent.SceneEventType) { case SceneEventType.LoadComplete: { - if (sceneEvent.ClientId == m_ServerNetworkManager.LocalClientId) + if (sceneEvent.ClientId == authority.LocalClientId) { - m_SceneLoaded = sceneEvent.Scene; m_ScenesLoaded.Add(sceneEvent.Scene); } - break; - } - case SceneEventType.LoadEventCompleted: - { - Assert.IsTrue(sceneEvent.ClientsThatTimedOut.Count == 0, $"{sceneEvent.ClientsThatTimedOut.Count} clients timed out while trying to load scene {m_CurrentSceneLoading}!"); - m_ClientsLoadedScene = true; - break; - } - case SceneEventType.UnloadEventCompleted: - { - if (sceneEvent.SceneName == m_CurrentSceneUnloading) - { - m_ClientsUnloadedScene = true; - } - break; + return; } } } @@ -501,6 +481,43 @@ protected override IEnumerator OnTearDown() m_ScenesLoaded.Clear(); yield return base.OnTearDown(); } + + private NetworkObject GetAuthorityInstance(NetworkObject instance) + { + if (instance.IsOwner) + { + return instance; + } + + var owner = instance.OwnerClientId; + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager.LocalClientId == owner) + { + networkManager.SpawnManager.SpawnedObjects.TryGetValue(instance.NetworkObjectId, out var networkObject); + return networkObject; + } + } + + return null; + } + } + + internal class SceneOriginTracker : NetworkBehaviour + { + public NetworkSceneHandle SceneWhereAwakeHappened; + private void Awake() + { + SceneWhereAwakeHappened = gameObject.scene.handle; + } + } + + internal class ShouldNeverSpawn : NetworkBehaviour + { + public override void OnNetworkSpawn() + { + Assert.Fail("Should never spawn!"); + } } /// @@ -509,7 +526,7 @@ protected override IEnumerator OnTearDown() /// internal class DestroyWithSceneInstancesTestHelper : NetworkBehaviour { - public static GameObject ShouldNeverSpawn; + public static ShouldNeverSpawn ShouldNeverSpawn; public static Dictionary> ObjectRelativeInstances = new Dictionary>(); @@ -544,12 +561,9 @@ public override void OnNetworkDespawn() public override void OnDestroy() { - if (NetworkManager != null) + if (IsSpawned && HasAuthority) { - if (NetworkManager.LocalClientId == NetworkManager.ServerClientId) - { - NetworkObjectDestroyed?.Invoke(NetworkObject); - } + NetworkObjectDestroyed?.Invoke(NetworkObject); } base.OnDestroy(); } diff --git a/testproject/ProjectSettings/EditorBuildSettings.asset b/testproject/ProjectSettings/EditorBuildSettings.asset index b2632cd9f5..9a93a17af6 100644 --- a/testproject/ProjectSettings/EditorBuildSettings.asset +++ b/testproject/ProjectSettings/EditorBuildSettings.asset @@ -133,6 +133,9 @@ EditorBuildSettings: guid: 17b92153f7381d34fa48c4d5c0393d13 - enabled: 1 path: Assets/Tests/Manual/IntegrationTestScenes/EmptyScene3.unity + guid: bfb4addb64cf6455a88668eba2f759ba + - enabled: 1 + path: Assets/Tests/Manual/IntegrationTestScenes/MoveADynamicObjectInAwake.unity guid: abd4c8b51c445d54faa16c67ac973f1b - enabled: 1 path: Assets/Samples/SpawnObject/SimpleSpawn.unity