Skip to content

fix: NetworkManager instantiate & destroy notifications and player spawn prefab offset #3088

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

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

### Added

- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088)
- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088)

### Fixed

- 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,8 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne
// Server-side spawning (only if there is a prefab hash or player prefab provided)
if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null))
{
var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault())
: NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault());
var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position ?? null, response.Rotation ?? null)
: NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null);

// Spawn the player NetworkObject locally
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(
Expand Down Expand Up @@ -896,15 +896,15 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne
/// <summary>
/// Client-Side Spawning in distributed authority mode uses this to spawn the player.
/// </summary>
internal void CreateAndSpawnPlayer(ulong ownerId, Vector3 position = default, Quaternion rotation = default)
internal void CreateAndSpawnPlayer(ulong ownerId)
{
if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide)
{
var playerPrefab = NetworkManager.FetchLocalPlayerPrefabToSpawn();
if (playerPrefab != null)
{
var globalObjectIdHash = playerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash;
var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, position, rotation);
var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation);
networkObject.IsSceneObject = false;
networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene);
}
Expand Down
16 changes: 16 additions & 0 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ namespace Unity.Netcode
[AddComponentMenu("Netcode/Network Manager", -100)]
public class NetworkManager : MonoBehaviour, INetworkUpdateSystem
{
/// <summary>
/// Subscribe to this static event to get notifications when a <see cref="NetworkManager"/> instance has been instantiated.
/// </summary>
public static event Action<NetworkManager> OnInstantiated;

/// <summary>
/// Subscribe to this static event to get notifications when a <see cref="NetworkManager"/> instance is being destroyed.
/// </summary>
public static event Action<NetworkManager> OnDestroying;


#if UNITY_EDITOR
// Inspector view expand/collapse settings for this derived child class
[HideInInspector]
Expand Down Expand Up @@ -1030,6 +1041,8 @@ private void Awake()
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += ModeChanged;
#endif
// Notify we have instantiated a new instance of NetworkManager.
OnInstantiated?.Invoke(this);
}

private void OnEnable()
Expand Down Expand Up @@ -1632,6 +1645,9 @@ private void OnDestroy()

UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded;

// Notify we are destroying NetworkManager
OnDestroying?.Invoke(this);

if (Singleton == this)
{
Singleton = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,14 +706,14 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ
/// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the
/// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned.
/// </summary>
internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3 position = default, Quaternion rotation = default, bool isScenePlaced = false)
internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false)
{
NetworkObject networkObject = null;
// If the prefab hash has a registered INetworkPrefabInstanceHandler derived class
if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash))
{
// Let the handler spawn the NetworkObject
networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position, rotation);
networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default);
networkObject.NetworkManagerOwner = NetworkManager;
}
else
Expand Down Expand Up @@ -764,7 +764,9 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow
else
{
// Create prefab instance while applying any pre-assigned position and rotation values
networkObject = UnityEngine.Object.Instantiate(networkPrefabReference, position, rotation).GetComponent<NetworkObject>();
networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent<NetworkObject>();
networkObject.transform.position = position ?? networkObject.transform.position;
networkObject.transform.rotation = rotation ?? networkObject.transform.rotation;
networkObject.NetworkManagerOwner = NetworkManager;
networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,60 @@ internal class NetworkManagerEventsTests
private NetworkManager m_ClientManager;
private NetworkManager m_ServerManager;

private NetworkManager m_NetworkManagerInstantiated;
private bool m_Instantiated;
private bool m_Destroyed;

/// <summary>
/// Validates the <see cref="NetworkManager.OnInstantiated"/> and <see cref="NetworkManager.OnDestroying"/> event notifications
/// </summary>
[UnityTest]
public IEnumerator InstantiatedAndDestroyingNotifications()
{
NetworkManager.OnInstantiated += NetworkManager_OnInstantiated;
NetworkManager.OnDestroying += NetworkManager_OnDestroying;
var waitPeriod = new WaitForSeconds(0.01f);
var prefab = new GameObject("InstantiateDestroy");
var networkManagerPrefab = prefab.AddComponent<NetworkManager>();

Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} prefab did not get instantiated event notification!");
Assert.IsTrue(m_NetworkManagerInstantiated == networkManagerPrefab, $"{nameof(NetworkManager)} prefab parameter did not match!");

m_Instantiated = false;
m_NetworkManagerInstantiated = null;

for (int i = 0; i < 3; i++)
{
var instance = Object.Instantiate(prefab);
var networkManager = instance.GetComponent<NetworkManager>();
Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} instance-{i} did not get instantiated event notification!");
Assert.IsTrue(m_NetworkManagerInstantiated == networkManager, $"{nameof(NetworkManager)} instance-{i} parameter did not match!");
Object.DestroyImmediate(instance);
Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} instance-{i} did not get destroying event notification!");
m_Instantiated = false;
m_NetworkManagerInstantiated = null;
m_Destroyed = false;
}
m_NetworkManagerInstantiated = networkManagerPrefab;
Object.Destroy(prefab);
yield return null;
Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} prefab did not get destroying event notification!");
NetworkManager.OnInstantiated -= NetworkManager_OnInstantiated;
NetworkManager.OnDestroying -= NetworkManager_OnDestroying;
}

private void NetworkManager_OnInstantiated(NetworkManager networkManager)
{
m_Instantiated = true;
m_NetworkManagerInstantiated = networkManager;
}

private void NetworkManager_OnDestroying(NetworkManager networkManager)
{
m_Destroyed = true;
Assert.True(m_NetworkManagerInstantiated == networkManager, $"Destroying {nameof(NetworkManager)} and current instance is not a match for the one passed into the event!");
}

[UnityTest]
public IEnumerator OnServerStoppedCalledWhenServerStops()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,68 @@ public IEnumerator SpawnWithNoObservers()
}
}
}

/// <summary>
/// This test validates the player position and rotation is correct
/// relative to the prefab's initial settings if no changes are applied.
/// </summary>
[TestFixture(HostOrServer.DAHost)]
[TestFixture(HostOrServer.Host)]
[TestFixture(HostOrServer.Server)]
internal class PlayerSpawnPositionTests : IntegrationTestWithApproximation
{
protected override int NumberOfClients => 2;

public PlayerSpawnPositionTests(HostOrServer hostOrServer) : base(hostOrServer) { }

private Vector3 m_PlayerPosition;
private Quaternion m_PlayerRotation;

protected override void OnCreatePlayerPrefab()
{
var playerNetworkObject = m_PlayerPrefab.GetComponent<NetworkObject>();
m_PlayerPosition = GetRandomVector3(-10.0f, 10.0f);
m_PlayerRotation = Quaternion.Euler(GetRandomVector3(-180.0f, 180.0f));
playerNetworkObject.transform.position = m_PlayerPosition;
playerNetworkObject.transform.rotation = m_PlayerRotation;
base.OnCreatePlayerPrefab();
}

private void PlayerTransformMatches(NetworkObject player)
{
var position = player.transform.position;
var rotation = player.transform.rotation;
Assert.True(Approximately(m_PlayerPosition, position), $"Client-{player.OwnerClientId} position {position} does not match the prefab position {m_PlayerPosition}!");
Assert.True(Approximately(m_PlayerRotation, rotation), $"Client-{player.OwnerClientId} rotation {rotation.eulerAngles} does not match the prefab rotation {m_PlayerRotation.eulerAngles}!");
}

[UnityTest]
public IEnumerator PlayerSpawnPosition()
{
if (m_ServerNetworkManager.IsHost)
{
PlayerTransformMatches(m_ServerNetworkManager.LocalClient.PlayerObject);

foreach (var client in m_ClientNetworkManagers)
{
yield return WaitForConditionOrTimeOut(() => client.SpawnManager.SpawnedObjects.ContainsKey(m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId));
AssertOnTimeout($"Client-{client.LocalClientId} does not contain a player prefab instance for client-{m_ServerNetworkManager.LocalClientId}!");
PlayerTransformMatches(client.SpawnManager.SpawnedObjects[m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId]);
}
}

foreach (var client in m_ClientNetworkManagers)
{
yield return WaitForConditionOrTimeOut(() => m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId));
AssertOnTimeout($"Client-{m_ServerNetworkManager.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!");
PlayerTransformMatches(m_ServerNetworkManager.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]);
foreach (var subClient in m_ClientNetworkManagers)
{
yield return WaitForConditionOrTimeOut(() => subClient.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId));
AssertOnTimeout($"Client-{subClient.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!");
PlayerTransformMatches(subClient.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]);
}
}
}
}
}
Loading