From d1bc02c2ce22b8ad0b2119024bbff42a6aaec0d8 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 19 Oct 2026 15:13:13 -0500 Subject: [PATCH 1/3] feat: count NGO and hybrid spawns per session in editor analytics NetworkSpawnManager counts local spawns (editor only) and the hybrid ones among them. NetcodeAnalytics reads both when the session stops and adds NgoSpawnCount and HybridSpawnCount to the NGO_NetworkManager event. --- .../Editor/Analytics/NetcodeAnalytics.cs | 10 +++ .../Analytics/NetworkManagerAnalytics.cs | 5 ++ .../Runtime/Spawning/NetworkSpawnManager.cs | 12 +++ .../NetworkObject/SpawnCountAnalyticsTests.cs | 80 +++++++++++++++++++ .../SpawnCountAnalyticsTests.cs.meta | 11 +++ 5 files changed, 118 insertions(+) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs index b1c4e69698..fb62293d96 100644 --- a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs @@ -18,6 +18,8 @@ internal struct NetworkSessionInfo public bool UsedCMBService; public string Transport; public NetworkConfig NetworkConfig; + public int NgoSpawnCount; + public int HybridSpawnCount; } /// @@ -124,6 +126,12 @@ internal override void SessionStopped(NetworkManager networkManager) return; } recentSession.UsedCMBService = networkManager.CMBServiceConnection; + var spawnManager = networkManager.SpawnManager; + if (spawnManager != null) + { + recentSession.HybridSpawnCount = spawnManager.HybridSpawnCount; + recentSession.NgoSpawnCount = spawnManager.SpawnCount - spawnManager.HybridSpawnCount; + } recentSession.SessionStopped = true; RecentSessions[lastIndex] = recentSession; } @@ -220,6 +228,8 @@ private NetworkManagerAnalytics GetNetworkManagerAnalytics(NetworkSessionInfo ne NetworkTransport = networkSession.Transport, EnableSceneManagement = networkSession.NetworkConfig.EnableSceneManagement, TickRate = (int)networkSession.NetworkConfig.TickRate, + NgoSpawnCount = networkSession.NgoSpawnCount, + HybridSpawnCount = networkSession.HybridSpawnCount, }; return networkManagerAnalytics; } diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs index cd1ca3b1de..7b45227f4f 100644 --- a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs @@ -17,6 +17,8 @@ internal struct NetworkManagerAnalytics : IAnalytic.IData, IEquatable public readonly HashSet SpawnedObjectsList = new HashSet(); +#if UNITY_EDITOR + // Spawns this peer made or received during the session, reported by the editor analytics. + internal int SpawnCount; + internal int HybridSpawnCount; +#endif + /// /// Use to get all NetworkObjects owned by a client /// Ownership to Objects Table Format: @@ -1313,11 +1319,17 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong if (networkObject.HasGhost) { networkObject.NetworkObjectBridge.UpdateGhostOwner(NetworkManager, ownerClientId); +#if UNITY_EDITOR + HybridSpawnCount++; +#endif } #endif SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); SpawnedObjectsList.Add(networkObject); +#if UNITY_EDITOR + SpawnCount++; +#endif networkObject.ApplyNetworkParenting(); NetworkObject.CheckOrphanChildren(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs new file mode 100644 index 0000000000..a83f9de2c2 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs @@ -0,0 +1,80 @@ +#if UNITY_EDITOR +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Validates the spawn counts the editor analytics reads from :
+ /// - Every peer counts each instance it spawns locally, whether it is the authority or received the spawn.
+ /// - Hybrid instances are also counted on their own.
+ ///
+ [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.UnifiedHost)] + internal class SpawnCountAnalyticsTests : NetcodeIntegrationTest + { + private const int k_DefaultInstances = 3; + private const int k_NgoInstances = 2; + + protected override int NumberOfClients => 2; + + // Hybrid during a unified test pass. + private GameObject m_DefaultPrefab; + private GameObject m_NgoPrefab; + + public SpawnCountAnalyticsTests(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + protected override bool UseUnifiedTests() + { + return true; + } + + protected override void OnServerAndClientsCreated() + { + m_DefaultPrefab = CreateNetworkObjectPrefab("SpawnCountDefault"); +#if UNIFIED_NETCODE + m_NgoPrefab = CreateNetworkObjectPrefab("SpawnCountNgo", false); +#else + m_NgoPrefab = CreateNetworkObjectPrefab("SpawnCountNgo"); +#endif + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator SpawnsAreCountedOnEveryPeer() + { + // Player objects are already spawned, so only the increase is checked. + var spawnCounts = new int[m_NetworkManagers.Length]; + var hybridSpawnCounts = new int[m_NetworkManagers.Length]; + for (int i = 0; i < m_NetworkManagers.Length; i++) + { + spawnCounts[i] = m_NetworkManagers[i].SpawnManager.SpawnCount; + hybridSpawnCounts[i] = m_NetworkManagers[i].SpawnManager.HybridSpawnCount; + } + + var authority = GetAuthorityNetworkManager(); + var instances = SpawnObjects(m_DefaultPrefab, authority, k_DefaultInstances); + instances.AddRange(SpawnObjects(m_NgoPrefab, authority, k_NgoInstances)); + foreach (var instance in instances) + { + yield return WaitForSpawnedOnAllOrTimeOut(instance.GetComponent()); + AssertOnTimeout($"Failed to spawn {instance.name} on all clients!"); + } + + var expectedHybridSpawns = m_UseUnifiedTests ? k_DefaultInstances : 0; + for (int i = 0; i < m_NetworkManagers.Length; i++) + { + var spawnManager = m_NetworkManagers[i].SpawnManager; + var clientId = m_NetworkManagers[i].LocalClientId; + Assert.AreEqual(k_DefaultInstances + k_NgoInstances, spawnManager.SpawnCount - spawnCounts[i], $"Client-{clientId} counted the wrong number of spawns!"); + Assert.AreEqual(expectedHybridSpawns, spawnManager.HybridSpawnCount - hybridSpawnCounts[i], $"Client-{clientId} counted the wrong number of hybrid spawns!"); + } + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs.meta new file mode 100644 index 0000000000..2dcbf0e25a --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69dc919eea8f49b6801cbb8b868f9237 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From b461ed112dccf525985ca03b29ec435f63407ccf Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 20 Oct 2026 17:06:29 -0500 Subject: [PATCH 2/3] feat: report whether the session was hybrid in editor analytics HybridSession is true when the session started with hybrid prefabs registered, so a hybrid session that spawned no hybrid instances is still identifiable. --- .../Editor/Analytics/NetcodeAnalytics.cs | 5 +++++ .../Editor/Analytics/NetworkManagerAnalytics.cs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs index fb62293d96..cf29198f89 100644 --- a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs @@ -18,6 +18,7 @@ internal struct NetworkSessionInfo public bool UsedCMBService; public string Transport; public NetworkConfig NetworkConfig; + public bool HybridSession; public int NgoSpawnCount; public int HybridSpawnCount; } @@ -125,6 +126,9 @@ internal override void SessionStopped(NetworkManager networkManager) { return; } +#if UNIFIED_NETCODE + recentSession.HybridSession = networkManager.NetworkConfig.Prefabs.HasGhostPrefabs; +#endif recentSession.UsedCMBService = networkManager.CMBServiceConnection; var spawnManager = networkManager.SpawnManager; if (spawnManager != null) @@ -228,6 +232,7 @@ private NetworkManagerAnalytics GetNetworkManagerAnalytics(NetworkSessionInfo ne NetworkTransport = networkSession.Transport, EnableSceneManagement = networkSession.NetworkConfig.EnableSceneManagement, TickRate = (int)networkSession.NetworkConfig.TickRate, + HybridSession = networkSession.HybridSession, NgoSpawnCount = networkSession.NgoSpawnCount, HybridSpawnCount = networkSession.HybridSpawnCount, }; diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs index 7b45227f4f..df2887c758 100644 --- a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs @@ -17,6 +17,7 @@ internal struct NetworkManagerAnalytics : IAnalytic.IData, IEquatable Date: Wed, 21 Oct 2026 09:05:16 -0500 Subject: [PATCH 3/3] fix: guard the unified cases in SpawnCountAnalyticsTests --- .../NetworkObject/SpawnCountAnalyticsTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs index a83f9de2c2..8a04d4a585 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/SpawnCountAnalyticsTests.cs @@ -13,7 +13,9 @@ namespace Unity.Netcode.RuntimeTests /// - Hybrid instances are also counted on their own.
///
[TestFixture(HostOrServer.Host)] +#if UNIFIED_NETCODE [TestFixture(HostOrServer.UnifiedHost)] +#endif internal class SpawnCountAnalyticsTests : NetcodeIntegrationTest { private const int k_DefaultInstances = 3; @@ -29,10 +31,12 @@ public SpawnCountAnalyticsTests(HostOrServer hostOrServer) : base(hostOrServer) { } +#if UNIFIED_NETCODE protected override bool UseUnifiedTests() { return true; } +#endif protected override void OnServerAndClientsCreated() { @@ -66,7 +70,13 @@ public IEnumerator SpawnsAreCountedOnEveryPeer() AssertOnTimeout($"Failed to spawn {instance.name} on all clients!"); } - var expectedHybridSpawns = m_UseUnifiedTests ? k_DefaultInstances : 0; + var expectedHybridSpawns = 0; +#if UNIFIED_NETCODE + if (m_UseUnifiedTests) + { + expectedHybridSpawns = k_DefaultInstances; + } +#endif for (int i = 0; i < m_NetworkManagers.Length; i++) { var spawnManager = m_NetworkManagers[i].SpawnManager;