diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 8817dfbe9c..1089c4012f 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -13,6 +13,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### 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)
+- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077)
- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075)
### Changed
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index f89bc1d345..5cca18a66a 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -1613,7 +1613,12 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla
}
else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
{
- NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this);
+ // If spawning with observers or if not spawning with observers but the observer count is greater than 1 (i.e. owner/authority creating),
+ // then we want to send a spawn notification.
+ if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1)
+ {
+ NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this);
+ }
}
else
{
@@ -3053,10 +3058,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
}
}
- // Add all known players to the observers list if they don't already exist
- foreach (var player in networkManager.SpawnManager.PlayerObjects)
+ // Only add all other players as observers if we are spawning with observers,
+ // otherwise user controls via NetworkShow.
+ if (networkObject.SpawnWithObservers)
{
- networkObject.Observers.Add(player.OwnerClientId);
+ // Add all known players to the observers list if they don't already exist
+ foreach (var player in networkManager.SpawnManager.PlayerObjects)
+ {
+ networkObject.Observers.Add(player.OwnerClientId);
+ }
}
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index ce359c180a..06b5b6c364 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -72,11 +72,22 @@ private void AddPlayerObject(NetworkObject playerObject)
return;
}
}
+
foreach (var player in m_PlayerObjects)
{
- player.Observers.Add(playerObject.OwnerClientId);
- playerObject.Observers.Add(player.OwnerClientId);
+ // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer.
+ if (player.SpawnWithObservers)
+ {
+ player.Observers.Add(playerObject.OwnerClientId);
+ }
+
+ // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object.
+ if (playerObject.SpawnWithObservers)
+ {
+ playerObject.Observers.Add(player.OwnerClientId);
+ }
}
+
m_PlayerObjects.Add(playerObject);
if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId))
{
diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
index 159d923d69..a84a7ccb7c 100644
--- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
+++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs
@@ -545,9 +545,14 @@ protected IEnumerator CreateAndStartNewClient()
private bool AllPlayerObjectClonesSpawned(NetworkManager joinedClient)
{
m_InternalErrorLog.Clear();
+ // If we are not checking for spawned players then exit early with a success
+ if (!ShouldCheckForSpawnedPlayers())
+ {
+ return true;
+ }
+
// Continue to populate the PlayerObjects list until all player object (local and clone) are found
ClientNetworkManagerPostStart(joinedClient);
-
var playerObjectRelative = m_ServerNetworkManager.SpawnManager.PlayerObjects.Where((c) => c.OwnerClientId == joinedClient.LocalClientId).FirstOrDefault();
if (playerObjectRelative == null)
{
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/PlayerObjectTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/PlayerObjectTests.cs
index 8fba758de9..dc75f41337 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/PlayerObjectTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/PlayerObjectTests.cs
@@ -11,7 +11,7 @@ namespace Unity.Netcode.RuntimeTests
[TestFixture(HostOrServer.Server)]
internal class PlayerObjectTests : NetcodeIntegrationTest
{
- protected override int NumberOfClients => 1;
+ protected override int NumberOfClients => 2;
protected GameObject m_NewPlayerToSpawn;
@@ -52,4 +52,72 @@ public IEnumerator SpawnAndReplaceExistingPlayerObject()
Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side player object to change!");
}
}
+
+ ///
+ /// Validate that when auto-player spawning but SpawnWithObservers is disabled,
+ /// the player instantiated is only spawned on the authority side.
+ ///
+ [TestFixture(HostOrServer.DAHost)]
+ [TestFixture(HostOrServer.Host)]
+ [TestFixture(HostOrServer.Server)]
+ internal class PlayerSpawnNoObserversTest : NetcodeIntegrationTest
+ {
+ protected override int NumberOfClients => 2;
+
+ public PlayerSpawnNoObserversTest(HostOrServer hostOrServer) : base(hostOrServer) { }
+
+ protected override bool ShouldCheckForSpawnedPlayers()
+ {
+ return false;
+ }
+
+ protected override void OnCreatePlayerPrefab()
+ {
+ var playerNetworkObject = m_PlayerPrefab.GetComponent();
+ playerNetworkObject.SpawnWithObservers = false;
+ base.OnCreatePlayerPrefab();
+ }
+
+ [UnityTest]
+ public IEnumerator SpawnWithNoObservers()
+ {
+ yield return s_DefaultWaitForTick;
+
+ if (!m_DistributedAuthority)
+ {
+ // Make sure clients did not spawn their player object on any of the clients including the owner.
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ foreach (var playerObject in m_ServerNetworkManager.SpawnManager.PlayerObjects)
+ {
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObject.NetworkObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{playerObject.NetworkObjectId}!");
+ }
+ }
+ }
+ else
+ {
+ // For distributed authority, we want to make sure the player object is only spawned on the authority side and all non-authority instances did not spawn it.
+ var playerObjectId = m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId;
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{m_ServerNetworkManager.LocalClientId}!");
+ }
+
+ foreach (var clientPlayer in m_ClientNetworkManagers)
+ {
+ playerObjectId = clientPlayer.LocalClient.PlayerObject.NetworkObjectId;
+ Assert.IsFalse(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{m_ServerNetworkManager.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!");
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ if (clientPlayer == client)
+ {
+ continue;
+ }
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!");
+ }
+ }
+
+ }
+ }
+ }
}