using System.Collections; using System.Collections.Generic; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests { public class NetworkObjectOnSpawnTests : NetcodeIntegrationTest { private GameObject m_TestNetworkObjectPrefab; private GameObject m_TestNetworkObjectInstance; protected override int NumberOfClients => 2; public enum ObserverTestTypes { WithObservers, WithoutObservers } private GameObject m_ObserverPrefab; private NetworkObject m_ObserverTestNetworkObject; private ObserverTestTypes m_ObserverTestType; private const string k_ObserverTestObjName = "ObsObj"; private const string k_WithObserversError = "Not all clients spawned the"; private const string k_WithoutObserversError = "A client spawned the"; protected override void OnServerAndClientsCreated() { m_ObserverPrefab = CreateNetworkObjectPrefab(k_ObserverTestObjName); base.OnServerAndClientsCreated(); } private bool CheckClientsSideObserverTestObj() { foreach (var client in m_ClientNetworkManagers) { if (!s_GlobalNetworkObjects.ContainsKey(client.LocalClientId)) { // When no observers there shouldn't be any client spawned NetworkObjects // (players are held in a different list) return !(m_ObserverTestType == ObserverTestTypes.WithObservers); } var clientObjects = s_GlobalNetworkObjects[client.LocalClientId]; // Make sure they did spawn the object if (m_ObserverTestType == ObserverTestTypes.WithObservers) { if (!clientObjects.ContainsKey(m_ObserverTestNetworkObject.NetworkObjectId)) { return false; } if (!clientObjects[m_ObserverTestNetworkObject.NetworkObjectId].IsSpawned) { return false; } } } return true; } /// /// Assures the late joining client has all /// NetworkPrefabs required to connect. /// protected override void OnNewClientCreated(NetworkManager networkManager) { foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs) { if (!networkManager.NetworkConfig.Prefabs.Contains(networkPrefab.Prefab)) { networkManager.NetworkConfig.Prefabs.Add(networkPrefab); } } base.OnNewClientCreated(networkManager); } /// /// This test validates property /// /// whether to spawn with or without observers [UnityTest] public IEnumerator ObserverSpawnTests([Values] ObserverTestTypes observerTestTypes) { m_ObserverTestType = observerTestTypes; var prefabNetworkObject = m_ObserverPrefab.GetComponent(); prefabNetworkObject.SpawnWithObservers = observerTestTypes == ObserverTestTypes.WithObservers; var instance = SpawnObject(m_ObserverPrefab, m_ServerNetworkManager); m_ObserverTestNetworkObject = instance.GetComponent(); var withoutObservers = m_ObserverTestType == ObserverTestTypes.WithoutObservers; if (withoutObservers) { // Just give a little time to make sure nothing spawned yield return s_DefaultWaitForTick; } yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj); AssertOnTimeout($"{(withoutObservers ? k_WithoutObserversError : k_WithObserversError)} {k_ObserverTestObjName} object!"); // If we spawned without observers if (withoutObservers) { // Make each client an observer foreach (var client in m_ClientNetworkManagers) { m_ObserverTestNetworkObject.NetworkShow(client.LocalClientId); } // Validate the clients spawned the NetworkObject m_ObserverTestType = ObserverTestTypes.WithObservers; yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj); AssertOnTimeout($"{k_WithObserversError} {k_ObserverTestObjName} object!"); // Validate that a late joining client does not see the NetworkObject when it spawns yield return CreateAndStartNewClient(); m_ObserverTestType = ObserverTestTypes.WithoutObservers; // Just give a little time to make sure nothing spawned yield return s_DefaultWaitForTick; yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj); AssertOnTimeout($"{(withoutObservers ? k_WithoutObserversError : k_WithObserversError)} {k_ObserverTestObjName} object!"); // Now validate that we can make the NetworkObject visible to the newly joined client m_ObserverTestNetworkObject.NetworkShow(m_ClientNetworkManagers[NumberOfClients].LocalClientId); // Validate the NetworkObject is visible to all connected clients (including the recently joined client) m_ObserverTestType = ObserverTestTypes.WithObservers; yield return WaitForConditionOrTimeOut(CheckClientsSideObserverTestObj); AssertOnTimeout($"{k_WithObserversError} {k_ObserverTestObjName} object!"); } } /// /// Tests that instantiating a and destroying without spawning it /// does not run or . /// [UnityTest] public IEnumerator InstantiateDestroySpawnNotCalled() { m_TestNetworkObjectPrefab = new GameObject("InstantiateDestroySpawnNotCalled_Object"); var networkObject = m_TestNetworkObjectPrefab.AddComponent(); var fail = m_TestNetworkObjectPrefab.AddComponent(); // instantiate m_TestNetworkObjectInstance = Object.Instantiate(m_TestNetworkObjectPrefab); yield return null; Object.Destroy(m_TestNetworkObjectInstance); } private class FailWhenSpawned : NetworkBehaviour { public override void OnNetworkSpawn() { Assert.Fail("Spawn should not be called on not spawned object"); } public override void OnNetworkDespawn() { Assert.Fail("Depawn should not be called on not spawned object"); } } protected override void OnCreatePlayerPrefab() { m_PlayerPrefab.AddComponent(); } protected override IEnumerator OnTearDown() { if (m_ObserverPrefab != null) { Object.Destroy(m_ObserverPrefab); } if (m_TestNetworkObjectPrefab != null) { Object.Destroy(m_TestNetworkObjectPrefab); } if (m_TestNetworkObjectInstance != null) { Object.Destroy(m_TestNetworkObjectInstance); } yield return base.OnTearDown(); } private List m_ClientTrackOnSpawnInstances = new List(); /// /// Test that callbacks are run for playerobject spawn, despawn, regular spawn, destroy on server. /// /// [UnityTest] public IEnumerator TestOnNetworkSpawnCallbacks() { // [Host-Side] Get the Host owned instance var serverInstance = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ServerNetworkManager.LocalClientId].GetComponent(); foreach (var client in m_ClientNetworkManagers) { var clientRpcTests = m_PlayerNetworkObjects[client.LocalClientId][m_ServerNetworkManager.LocalClientId].gameObject.GetComponent(); Assert.IsNotNull(clientRpcTests); m_ClientTrackOnSpawnInstances.Add(clientRpcTests); } // -------------- step 1 check player spawn despawn // check spawned on server Assert.AreEqual(1, serverInstance.OnNetworkSpawnCalledCount); // safety check server despawned Assert.AreEqual(0, serverInstance.OnNetworkDespawnCalledCount); // Conditional check for clients spawning or despawning var checkSpawnCondition = false; var expectedSpawnCount = 1; var expectedDespawnCount = 0; bool HasConditionBeenMet() { var clientsCompleted = 0; // check spawned on client foreach (var clientInstance in m_ClientTrackOnSpawnInstances) { if (checkSpawnCondition) { if (clientInstance.OnNetworkSpawnCalledCount == expectedSpawnCount) { clientsCompleted++; } } else { if (clientInstance.OnNetworkDespawnCalledCount == expectedDespawnCount) { clientsCompleted++; } } } return clientsCompleted >= NumberOfClients; } // safety check that all clients have not been despawned yet Assert.True(HasConditionBeenMet(), "Failed condition that all clients not despawned yet!"); // now verify that all clients have been spawned checkSpawnCondition = true; yield return WaitForConditionOrTimeOut(HasConditionBeenMet); Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side spawns!"); // despawn on server. However, since we'll be using this object later in the test, don't delete it serverInstance.GetComponent().Despawn(false); // check despawned on server Assert.AreEqual(1, serverInstance.OnNetworkDespawnCalledCount); // we now expect the clients to each have despawned once expectedDespawnCount = 1; yield return s_DefaultWaitForTick; // verify that all client-side instances are despawned checkSpawnCondition = false; yield return WaitForConditionOrTimeOut(HasConditionBeenMet); Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns!"); //----------- step 2 check spawn and destroy again serverInstance.GetComponent().Spawn(); // wait a tick yield return s_DefaultWaitForTick; // check spawned again on server this is 2 because we are reusing the object which was already spawned once. Assert.AreEqual(2, serverInstance.OnNetworkSpawnCalledCount); checkSpawnCondition = true; yield return WaitForConditionOrTimeOut(HasConditionBeenMet); Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side spawns! (2nd pass)"); // destroy the server object Object.Destroy(serverInstance.gameObject); yield return s_DefaultWaitForTick; // check whether despawned was called again on server instance Assert.AreEqual(2, serverInstance.OnNetworkDespawnCalledCount); checkSpawnCondition = false; yield return WaitForConditionOrTimeOut(HasConditionBeenMet); Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns! (2nd pass)"); } [Test] public void DynamicallySpawnedNoSceneOriginException() { var gameObject = new GameObject(); var networkObject = gameObject.AddComponent(); networkObject.IsSpawned = true; networkObject.SceneOriginHandle = 0; networkObject.IsSceneObject = false; // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject // when the scene of origin handle is zero var sceneOriginHandle = networkObject.GetSceneOriginHandle(); // This validates that GetSceneOriginHandle will return the GameObject's scene handle that should be the currently active scene var activeSceneHandle = UnityEngine.SceneManagement.SceneManager.GetActiveScene().handle; Assert.IsTrue(sceneOriginHandle == activeSceneHandle, $"{nameof(NetworkObject)} should have returned the active scene handle of {activeSceneHandle} but returned {sceneOriginHandle}"); } private class TrackOnSpawnFunctions : NetworkBehaviour { public int OnNetworkSpawnCalledCount { get; private set; } public int OnNetworkDespawnCalledCount { get; private set; } public override void OnNetworkSpawn() { OnNetworkSpawnCalledCount++; } public override void OnNetworkDespawn() { OnNetworkDespawnCalledCount++; } } } }