diff --git a/com.unity.netcode.gameobjects/Editor/Analytics.meta b/com.unity.netcode.gameobjects/Editor/Analytics.meta new file mode 100644 index 0000000000..8894158a2b --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: df502b16f2458f1458f8546326dc5ef1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs b/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs new file mode 100644 index 0000000000..b366e225eb --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs @@ -0,0 +1,25 @@ +#if UNITY_EDITOR +using System; +using UnityEngine.Analytics; + +namespace Unity.Netcode.Editor +{ + internal class AnalyticsHandler : IAnalytic where T : IAnalytic.IData + { + private T m_Data; + + internal T Data => m_Data; + + public AnalyticsHandler(T data) + { + m_Data = data; + } + public bool TryGatherData(out IAnalytic.IData data, out Exception error) + { + data = m_Data; + error = null; + return data != null; + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs.meta b/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs.meta new file mode 100644 index 0000000000..e1dbb20b47 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/AnalyticsHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f65a17a86eb08c42be3b01b67fb6781 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs new file mode 100644 index 0000000000..4f1c4f8084 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs @@ -0,0 +1,229 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.Analytics; + +namespace Unity.Netcode.Editor +{ + /// + /// Used to collection network session configuration information + /// + internal struct NetworkSessionInfo + { + public int SessionIndex; + public bool SessionStopped; + public bool WasServer; + public bool WasClient; + public bool UsedCMBService; + public string Transport; + public NetworkConfig NetworkConfig; + } + + /// + /// Netcode for GameObjects Analytics Class + /// + internal class NetcodeAnalytics : NetworkManager.NetcodeAnalytics + { + /// + /// Determines if we are running an integration test of the analytics integration + /// + internal static bool IsIntegrationTest = false; +#if ENABLE_NGO_ANALYTICS_LOGGING + internal static bool EnableLogging = true; +#else + internal static bool EnableLogging = false; +#endif + + // Preserves the analytics enabled flag + private bool m_OriginalAnalyticsEnabled; + + internal override void OnOneTimeSetup() + { + m_OriginalAnalyticsEnabled = EditorAnalytics.enabled; + // By default, we always disable analytics during integration testing + EditorAnalytics.enabled = false; + } + + internal override void OnOneTimeTearDown() + { + // Reset analytics to the original value + EditorAnalytics.enabled = m_OriginalAnalyticsEnabled; + } + + internal List AnalyticsTestResults = new List(); + + internal List RecentSessions = new List(); + /// + /// Invoked from . + /// + /// The new state. + /// The current instance when play mode was entered. + internal override void ModeChanged(PlayModeStateChange playModeState, NetworkManager networkManager) + { + switch (playModeState) + { + case PlayModeStateChange.EnteredPlayMode: + { + if (IsIntegrationTest) + { + AnalyticsTestResults.Clear(); + } + break; + } + case PlayModeStateChange.ExitingPlayMode: + { + // Update analytics + UpdateAnalytics(networkManager); + break; + } + } + } + + /// + /// Editor Only + /// Invoked when the session is started. + /// + /// The instance when the session is started. + internal override void SessionStarted(NetworkManager networkManager) + { + // If analytics is disabled and we are not running an integration test, then exit early. + if (!EditorAnalytics.enabled && !IsIntegrationTest) + { + return; + } + + var newSession = new NetworkSessionInfo() + { + SessionIndex = RecentSessions.Count, + WasClient = networkManager.IsClient, + WasServer = networkManager.IsServer, + NetworkConfig = networkManager.NetworkConfig.Copy(), + Transport = networkManager.NetworkConfig.NetworkTransport != null ? networkManager.NetworkConfig.NetworkTransport.GetType().Name : "None", + }; + RecentSessions.Add(newSession); + } + + /// + /// Editor Only + /// Invoked when the session is stopped or upon exiting play mode. + /// + /// The instance. + internal override void SessionStopped(NetworkManager networkManager) + { + // If analytics is disabled and we are not running an integration test or there are no sessions, then exit early. + if ((!EditorAnalytics.enabled && !IsIntegrationTest) || RecentSessions.Count == 0) + { + return; + } + + var lastIndex = RecentSessions.Count - 1; + var recentSession = RecentSessions[lastIndex]; + // If the session has already been finalized, then exit early. + if (recentSession.SessionStopped) + { + return; + } + recentSession.UsedCMBService = networkManager.CMBServiceConnection; + recentSession.SessionStopped = true; + RecentSessions[lastIndex] = recentSession; + } + + /// + /// Invoked from within when exiting play mode. + /// + private void UpdateAnalytics(NetworkManager networkManager) + { + // If analytics is disabled and we are not running an integration test or there are no sessions to process, then exit early. + if ((!EditorAnalytics.enabled && !IsIntegrationTest) || RecentSessions.Count == 0) + { + return; + } + + // If the NetworkManager isn't null, then make sure the last entry is marked off as stopped. + // If the last session is stopped, then SessionStopped will exit early. + if (networkManager != null) + { + SessionStopped(networkManager); + } + + // Parse through all of the recent network sessions to generate and send NetworkManager analytics + for (int i = 0; i < RecentSessions.Count; i++) + { + var networkManagerAnalytics = GetNetworkManagerAnalytics(RecentSessions[i]); + + var isDuplicate = false; + foreach (var analytics in AnalyticsTestResults) + { + // If we have any sessions with identical configurations, + // then we want to ignore those. + if (analytics.Data.Equals(networkManagerAnalytics)) + { + isDuplicate = true; + break; + } + } + + if (isDuplicate) + { + continue; + } + + // If not running an integration test, then go ahead and send the anlytics event data. + if (!IsIntegrationTest) + { + var result = EditorAnalytics.SendAnalytic(new NetworkManagerAnalyticsHandler(networkManagerAnalytics)); + if (EnableLogging && result != AnalyticsResult.Ok) + { + Debug.LogWarning($"[Analytics] Problem sending analytics: {result}"); + } + } + else + { + AnalyticsTestResults.Add(new NetworkManagerAnalyticsHandler(networkManagerAnalytics)); + } + } + + if (IsIntegrationTest && EnableLogging) + { + var count = 0; + foreach (var entry in AnalyticsTestResults) + { + entry.Data.LogAnalyticData(count); + count++; + } + } + RecentSessions.Clear(); + } + + /// + /// Generates a based on the passed in + /// + /// Represents a network session with the used NetworkManager configuration + /// + private NetworkManagerAnalytics GetNetworkManagerAnalytics(NetworkSessionInfo networkSession) + { + var multiplayerSDKInstalled = false; +#if MULTIPLAYER_SERVICES_SDK_INSTALLED + multiplayerSDKInstalled = true; +#endif + if (EnableLogging && !networkSession.SessionStopped) + { + Debug.LogWarning($"Session-{networkSession.SessionIndex} was not considered stopped!"); + } + var networkManagerAnalytics = new NetworkManagerAnalytics() + { + IsDistributedAuthority = networkSession.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority, + WasServer = networkSession.WasServer, + WasClient = networkSession.WasClient, + UsedCMBService = networkSession.UsedCMBService, + IsUsingMultiplayerSDK = multiplayerSDKInstalled, + NetworkTransport = networkSession.Transport, + EnableSceneManagement = networkSession.NetworkConfig.EnableSceneManagement, + TickRate = (int)networkSession.NetworkConfig.TickRate, + }; + return networkManagerAnalytics; + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs.meta b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs.meta new file mode 100644 index 0000000000..757665a130 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetcodeAnalytics.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 05223af7b06843841868225a65c90fea \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs new file mode 100644 index 0000000000..9ba7e46142 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs @@ -0,0 +1,47 @@ +#if UNITY_EDITOR +using System; +using System.Text; +using UnityEngine; +using UnityEngine.Analytics; + +namespace Unity.Netcode.Editor +{ + [Serializable] + internal struct NetworkManagerAnalytics : IAnalytic.IData, IEquatable + { + public bool IsDistributedAuthority; + public bool WasServer; + public bool WasClient; + public bool UsedCMBService; + public bool IsUsingMultiplayerSDK; + public string NetworkTransport; + public bool EnableSceneManagement; + public int TickRate; + public override string ToString() + { + var message = new StringBuilder(); + message.AppendLine($"{nameof(IsDistributedAuthority)}: {IsDistributedAuthority}"); + message.AppendLine($"{nameof(WasServer)}: {WasServer}"); + message.AppendLine($"{nameof(WasClient)}: {WasClient}"); + message.AppendLine($"{nameof(UsedCMBService)}: {UsedCMBService}"); + message.AppendLine($"{nameof(IsUsingMultiplayerSDK)}: {IsUsingMultiplayerSDK}"); + message.AppendLine($"{nameof(NetworkTransport)}: {NetworkTransport}"); + message.AppendLine($"{nameof(EnableSceneManagement)}: {EnableSceneManagement}"); + message.AppendLine($"{nameof(TickRate)}: {TickRate}"); + return message.ToString(); + } + + internal void LogAnalyticData(int sessionNumber) + { + Debug.Log($"{nameof(NetworkManagerAnalytics)} Session-{sessionNumber}:\n {ToString()}"); + } + public bool Equals(NetworkManagerAnalytics other) + { + return IsDistributedAuthority == other.IsDistributedAuthority && WasServer == other.WasServer && WasClient == other.WasClient + && UsedCMBService == other.UsedCMBService && IsUsingMultiplayerSDK == other.IsUsingMultiplayerSDK + && EnableSceneManagement == other.EnableSceneManagement && TickRate == other.TickRate + && NetworkTransport.Equals(other.NetworkTransport); + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs.meta b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs.meta new file mode 100644 index 0000000000..10980f86d5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalytics.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a987d18ba709af44bbe4033d29b80cb6 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs new file mode 100644 index 0000000000..7866df9621 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs @@ -0,0 +1,12 @@ +#if UNITY_EDITOR +using UnityEngine.Analytics; + +namespace Unity.Netcode.Editor +{ + [AnalyticInfo("NGO_NetworkManager", "unity.netcode", 5, 100, 1000)] + internal class NetworkManagerAnalyticsHandler : AnalyticsHandler + { + public NetworkManagerAnalyticsHandler(NetworkManagerAnalytics networkManagerAnalytics) : base(networkManagerAnalytics) { } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs.meta b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs.meta new file mode 100644 index 0000000000..a088b0bb93 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/Analytics/NetworkManagerAnalyticsHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 087c955a14fef5448bcd1f7c7a95b21f \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/AssemblyInfo.cs b/com.unity.netcode.gameobjects/Editor/AssemblyInfo.cs index f3fd38bc17..41e6599661 100644 --- a/com.unity.netcode.gameobjects/Editor/AssemblyInfo.cs +++ b/com.unity.netcode.gameobjects/Editor/AssemblyInfo.cs @@ -3,5 +3,6 @@ #if UNITY_INCLUDE_TESTS #if UNITY_EDITOR [assembly: InternalsVisibleTo("Unity.Netcode.EditorTests")] +[assembly: InternalsVisibleTo("TestProject.RuntimeTests")] #endif // UNITY_EDITOR #endif // UNITY_INCLUDE_TESTS diff --git a/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs b/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs index 3138369d57..5cba906f2a 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs @@ -27,6 +27,7 @@ public class NetworkManagerHelper : NetworkManager.INetworkManagerHelper private static void InitializeOnload() { Singleton = new NetworkManagerHelper(); + NetworkManager.NetworkManagerHelper = Singleton; EditorApplication.playModeStateChanged -= EditorApplication_playModeStateChanged; EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; @@ -224,6 +225,17 @@ public bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool } return isParented; } + + internal NetcodeAnalytics NetcodeAnalytics = new NetcodeAnalytics(); + + /// + /// Directly define the interface method to keep this internal + /// + /// The instance which is derived from the abstract class. + NetworkManager.NetcodeAnalytics NetworkManager.INetworkManagerHelper.Analytics() + { + return NetcodeAnalytics; + } } #endif } diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs index fbbbf86ebb..826fb15ecf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs @@ -168,6 +168,46 @@ public class NetworkConfig [Tooltip("When enabled (default), the player prefab will automatically be spawned (client-side) upon the client being approved and synchronized.")] public bool AutoSpawnPlayerPrefabClientSide = true; +#if UNITY_EDITOR + /// + /// Creates a copy of the current + /// + /// a copy of this + internal NetworkConfig Copy() + { + var networkConfig = new NetworkConfig() + { + ProtocolVersion = ProtocolVersion, + NetworkTransport = NetworkTransport, + TickRate = TickRate, + ClientConnectionBufferTimeout = ClientConnectionBufferTimeout, + ConnectionApproval = ConnectionApproval, + EnableTimeResync = EnableTimeResync, + TimeResyncInterval = TimeResyncInterval, + EnsureNetworkVariableLengthSafety = EnsureNetworkVariableLengthSafety, + EnableSceneManagement = EnableSceneManagement, + ForceSamePrefabs = ForceSamePrefabs, + RecycleNetworkIds = RecycleNetworkIds, + NetworkIdRecycleDelay = NetworkIdRecycleDelay, + RpcHashSize = RpcHashSize, + LoadSceneTimeOut = LoadSceneTimeOut, + SpawnTimeout = SpawnTimeout, + EnableNetworkLogs = EnableNetworkLogs, + NetworkTopology = NetworkTopology, + UseCMBService = UseCMBService, + AutoSpawnPlayerPrefabClientSide = AutoSpawnPlayerPrefabClientSide, +#if MULTIPLAYER_TOOLS + NetworkMessageMetrics = NetworkMessageMetrics, +#endif + NetworkProfilingMetrics = NetworkProfilingMetrics, + }; + + return networkConfig; + } + +#endif + + #if MULTIPLAYER_TOOLS /// /// Controls whether network messaging metrics will be gathered. (defaults to true) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 9f257fb5e1..d564f0f8f3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -910,121 +910,6 @@ internal T Value internal Override PortOverride; - -#if UNITY_EDITOR - internal static INetworkManagerHelper NetworkManagerHelper; - - /// - /// Interface for NetworkManagerHelper - /// - internal interface INetworkManagerHelper - { - bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false); - void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false); - } - - internal delegate void ResetNetworkManagerDelegate(NetworkManager manager); - - internal static ResetNetworkManagerDelegate OnNetworkManagerReset; - - private void Reset() - { - OnNetworkManagerReset?.Invoke(this); - } - - protected virtual void OnValidateComponent() - { - - } - - private PackageInfo GetPackageInfo(string packageName) - { - return AssetDatabase.FindAssets("package").Select(AssetDatabase.GUIDToAssetPath).Where(x => AssetDatabase.LoadAssetAtPath(x) != null).Select(PackageInfo.FindForAssetPath).Where(x => x != null).First(x => x.name == packageName); - } - - internal void OnValidate() - { - if (NetworkConfig == null) - { - return; // May occur when the component is added - } - - // Do a validation pass on NetworkConfig properties - NetworkConfig.OnValidate(); - - if (GetComponentInChildren() != null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning($"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}."); - } - } - - var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); - - // If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information - if (!activeScene.isDirty || EditorApplication.isUpdating) - { - return; - } - - // During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it - NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.Clear(); - - var prefabs = NetworkConfig.Prefabs.Prefabs; - // Check network prefabs and assign to dictionary for quick look up - for (int i = 0; i < prefabs.Count; i++) - { - var networkPrefab = prefabs[i]; - var networkPrefabGo = networkPrefab?.Prefab; - if (networkPrefabGo == null) - { - continue; - } - - var networkObject = networkPrefabGo.GetComponent(); - if (networkObject == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogError($"Cannot register {NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root"); - } - - continue; - } - - { - var childNetworkObjects = new List(); - networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects); - if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning($"{NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)"); - } - } - } - } - - try - { - OnValidateComponent(); - } - catch (Exception ex) - { - Debug.LogException(ex); - } - } - - private void ModeChanged(PlayModeStateChange change) - { - if (IsListening && change == PlayModeStateChange.ExitingPlayMode) - { - OnApplicationQuit(); - } - } -#endif - /// /// Determines if the NetworkManager's GameObject is parented under another GameObject and /// notifies the user that this is not allowed for the NetworkManager. @@ -1285,6 +1170,9 @@ internal void Initialize(bool server) NetworkConfig.InitializePrefabs(); PrefabHandler.RegisterPlayerPrefab(); +#if UNITY_EDITOR + BeginNetworkSession(); +#endif } private enum StartType @@ -1584,6 +1472,10 @@ private void OnSceneUnloaded(Scene scene) internal void ShutdownInternal() { +#if UNITY_EDITOR + EndNetworkSession(); +#endif + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(ShutdownInternal)); @@ -1721,5 +1613,211 @@ private void ParseCommandLineOptions() ParseArg(k_OverridePortArg, ref PortOverride); #endif } + +#if UNITY_EDITOR + internal static INetworkManagerHelper NetworkManagerHelper; + + /// + /// Interface for NetworkManagerHelper + /// + internal interface INetworkManagerHelper + { + bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false); + + void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false); + + internal NetcodeAnalytics Analytics(); + } + + internal abstract class NetcodeAnalytics + { + internal abstract void ModeChanged(PlayModeStateChange playModeState, NetworkManager networkManager); + + internal abstract void SessionStarted(NetworkManager networkManager); + + internal abstract void SessionStopped(NetworkManager networkManager); + + internal abstract void OnOneTimeSetup(); + + internal abstract void OnOneTimeTearDown(); + } + + internal delegate void ResetNetworkManagerDelegate(NetworkManager manager); + + internal static ResetNetworkManagerDelegate OnNetworkManagerReset; + + private void Reset() + { + OnNetworkManagerReset?.Invoke(this); + } + + /// + /// Invoked when validating the component. + /// + protected virtual void OnValidateComponent() + { + + } + + private PackageInfo GetPackageInfo(string packageName) + { + return AssetDatabase.FindAssets("package").Select(AssetDatabase.GUIDToAssetPath).Where(x => AssetDatabase.LoadAssetAtPath(x) != null).Select(PackageInfo.FindForAssetPath).Where(x => x != null).First(x => x.name == packageName); + } + + internal void OnValidate() + { + if (NetworkConfig == null) + { + return; // May occur when the component is added + } + + // Do a validation pass on NetworkConfig properties + NetworkConfig.OnValidate(); + + if (GetComponentInChildren() != null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}."); + } + } + + var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); + + // If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information + if (!activeScene.isDirty || EditorApplication.isUpdating) + { + return; + } + + // During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it + NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.Clear(); + + var prefabs = NetworkConfig.Prefabs.Prefabs; + // Check network prefabs and assign to dictionary for quick look up + for (int i = 0; i < prefabs.Count; i++) + { + var networkPrefab = prefabs[i]; + var networkPrefabGo = networkPrefab?.Prefab; + if (networkPrefabGo == null) + { + continue; + } + + var networkObject = networkPrefabGo.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"Cannot register {NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root"); + } + + continue; + } + + { + var childNetworkObjects = new List(); + networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects); + if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)"); + } + } + } + } + + try + { + OnValidateComponent(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + + internal void ModeChanged(PlayModeStateChange playModeState) + { + if (playModeState == PlayModeStateChange.ExitingPlayMode) + { + if (IsListening) + { + OnApplicationQuit(); + } + } + try + { + NetworkManagerHelper?.Analytics()?.ModeChanged(playModeState, this); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + + } + + /// + /// Invoked when NetworkManager is started. + /// + private void BeginNetworkSession() + { + try + { + NetworkManagerHelper?.Analytics()?.SessionStarted(this); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + + /// + /// Invoked when NetworkManager is stopped or upon exiting play mode. + /// + private void EndNetworkSession() + { + try + { + NetworkManagerHelper?.Analytics()?.SessionStopped(this); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } +#endif + +#if UNITY_INCLUDE_TESTS + /// + /// Used for integration tests + /// + internal static void OnOneTimeSetup() + { +#if UNITY_EDITOR + try + { + NetworkManagerHelper?.Analytics()?.OnOneTimeSetup(); + } + catch { } +#endif + } + + /// + /// Used for integration tests + /// + internal static void OnOneTimeTearDown() + { +#if UNITY_EDITOR + try + { + NetworkManagerHelper?.Analytics()?.OnOneTimeTearDown(); + } + catch { } +#endif + } +#endif + } } diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/AssemblyInfo.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/AssemblyInfo.cs index b21e378297..e880ab216e 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/AssemblyInfo.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/AssemblyInfo.cs @@ -4,6 +4,7 @@ [assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")] [assembly: InternalsVisibleTo("TestProject.RuntimeTests")] #if UNITY_EDITOR +[assembly: InternalsVisibleTo("Unity.Netcode.Editor")] [assembly: InternalsVisibleTo("TestProject.EditorTests")] #endif // UNITY_EDITOR #if MULTIPLAYER_TOOLS diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index fc68b71b8b..95bf441e4d 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -301,6 +301,12 @@ public void OneTimeSetup() // Enable NetcodeIntegrationTest auto-label feature NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(true); + +#if UNITY_INCLUDE_TESTS + // Provide an external hook to be able to make adjustments to netcode classes prior to running any tests + NetworkManager.OnOneTimeSetup(); +#endif + OnOneTimeSetup(); VerboseDebug($"Exiting {nameof(OneTimeSetup)}"); @@ -1293,6 +1299,10 @@ public void OneTimeTearDown() UnloadRemainingScenes(); VerboseDebug($"Exiting {nameof(OneTimeTearDown)}"); +#if UNITY_INCLUDE_TESTS + // Provide an external hook to be able to make adjustments to netcode classes after running tests + NetworkManager.OnOneTimeTearDown(); +#endif IsRunning = false; } diff --git a/testproject/Assets/Tests/Runtime/AnalyticsTests.cs b/testproject/Assets/Tests/Runtime/AnalyticsTests.cs new file mode 100644 index 0000000000..3db6bc103f --- /dev/null +++ b/testproject/Assets/Tests/Runtime/AnalyticsTests.cs @@ -0,0 +1,202 @@ +#if UNITY_EDITOR +using System.Collections; +using NUnit.Framework; +using Unity.Netcode; +using Unity.Netcode.Editor; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.TestTools; + +namespace TestProject.RuntimeTests +{ + /// + /// In-Editor only test
+ /// Validates the analytics event data collection process.
+ /// + ///
+ internal class AnalyticsTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 0; + + private NetcodeAnalytics m_NetcodeAnalytics; + + protected override IEnumerator OnSetup() + { + NetcodeAnalytics.IsIntegrationTest = true; + m_NetcodeAnalytics = Unity.Netcode.Editor.NetworkManagerHelper.Singleton.NetcodeAnalytics; + yield return base.OnSetup(); + } + + protected override IEnumerator OnTearDown() + { + NetcodeAnalytics.IsIntegrationTest = false; + yield return base.OnTearDown(); + } + + private bool m_NetworkManagerStarted; + private bool m_NetworkManagerStopped; + private IEnumerator StartAndStopSession(int expectedCount, bool isHost, bool isDistributedAuthority) + { + m_NetworkManagerStarted = false; + m_NetworkManagerStopped = false; + + m_ServerNetworkManager.NetworkConfig.NetworkTopology = isDistributedAuthority ? NetworkTopologyTypes.DistributedAuthority : NetworkTopologyTypes.ClientServer; + + if (!isHost) + { + m_ServerNetworkManager.StartServer(); + } + else + { + m_ServerNetworkManager.StartHost(); + } + yield return WaitForConditionOrTimeOut(() => m_NetworkManagerStarted); + var serverOrHost = isHost ? "Host" : "Server"; + AssertOnTimeout($"Failed to start {nameof(NetworkManager)} as a {serverOrHost} using a {m_ServerNetworkManager.NetworkConfig.NetworkTopology} {nameof(NetworkConfig.NetworkTopology)}!"); + + yield return StopNetworkManager(); + } + + private IEnumerator StopNetworkManager() + { + var serverOrHost = m_ServerNetworkManager.IsHost ? "Host" : "Server"; + + m_ServerNetworkManager.Shutdown(); + yield return WaitForConditionOrTimeOut(() => m_NetworkManagerStopped); + AssertOnTimeout($"Failed to stop {nameof(NetworkManager)} as a {serverOrHost} using a {m_ServerNetworkManager.NetworkConfig.NetworkTopology} {nameof(NetworkConfig.NetworkTopology)}!"); + } + + private void OnServerStarted() + { + m_NetworkManagerStarted = true; + } + + private void OnServerStopped(bool wasHost) + { + m_NetworkManagerStopped = true; + } + + /// + /// Validates the NGO analytics gathering process:
+ /// - When entering play mode any previous analytics events should be cleared.
+ /// - Each session while in play mode should be tracked (minimal processing).
+ /// - When exiting play mode the sessions should be processed and cleared.
+ /// - There should only be one unique analytic data event per unique session configuration.
+ ///
+ [UnityTest] + public IEnumerator ValidateCollectionProcess() + { + var expectedCount = 0; + var currentCount = 0; + m_ServerNetworkManager.OnServerStarted += OnServerStarted; + m_ServerNetworkManager.OnServerStopped += OnServerStopped; + m_PlayerPrefab.SetActive(false); + m_ServerNetworkManager.NetworkConfig.PlayerPrefab = null; + yield return StopNetworkManager(); + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == 1, $"Expected 1 session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + Assert.True(m_NetcodeAnalytics.AnalyticsTestResults.Count == 0, $"Expected 0 analytics events but found: {m_NetcodeAnalytics.AnalyticsTestResults.Count}!"); + + // Simulate exiting play mode + m_NetcodeAnalytics.ModeChanged(UnityEditor.PlayModeStateChange.ExitingPlayMode, m_ServerNetworkManager); + Assert.True(m_NetcodeAnalytics.AnalyticsTestResults.Count == 1, $"Expected 1 analytics event but found: {m_NetcodeAnalytics.AnalyticsTestResults.Count}!"); + + // Simulate entering play mode + m_NetcodeAnalytics.ModeChanged(UnityEditor.PlayModeStateChange.EnteredPlayMode, m_ServerNetworkManager); + + Assert.IsFalse(m_ServerNetworkManager.IsListening, $"Networkmanager should be stopped but is still listening!"); + // Client-Server + // Start as a Server and then shutdown + yield return StartAndStopSession(expectedCount, false, false); + expectedCount++; + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == expectedCount, $"Expected {expectedCount} session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + + // Start as a server again with the same settings and then shutdown + // (this should be excluded in the final analytics event data) + yield return StartAndStopSession(expectedCount, false, false); + expectedCount++; + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == expectedCount, $"Expected {expectedCount} session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + + // Start as a Host and then shutdown + yield return StartAndStopSession(expectedCount, true, false); + expectedCount++; + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == expectedCount, $"Expected {expectedCount} session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + + // Start as a DAHost and then shutdown + yield return StartAndStopSession(expectedCount, true, true); + expectedCount++; + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == expectedCount, $"Expected {expectedCount} session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + + // Start as a Host again and then shutdown + // (this should be excluded in the final analytics event data) + yield return StartAndStopSession(expectedCount, true, false); + expectedCount++; + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == expectedCount, $"Expected {expectedCount} session but found: {m_NetcodeAnalytics.RecentSessions.Count}!"); + Assert.True(m_NetcodeAnalytics.AnalyticsTestResults.Count == 0, $"Expected 0 analytics events but found: {m_NetcodeAnalytics.AnalyticsTestResults.Count}!"); + + ////////////////////////////////////////////////// + // Validating that each session's configuration is tracked for generating the analytics data events. + // Verify session 1 + var session = m_NetcodeAnalytics.RecentSessions[currentCount]; + Assert.True(session.WasServer && !session.WasClient, $"Expected session to be started as a server session but it was not! Server: {session.WasServer} Client: {session.WasClient}"); + Assert.True(session.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer, $"Expected session to be a {NetworkTopologyTypes.ClientServer} session but it was {session.NetworkConfig.NetworkTopology}!"); + + // Verify session 2 + currentCount++; + session = m_NetcodeAnalytics.RecentSessions[currentCount]; + Assert.True(session.WasServer && !session.WasClient, $"Expected session to be started as a server session but it was not! Server: {session.WasServer} Client: {session.WasClient}"); + Assert.True(session.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer, $"Expected session to be a {NetworkTopologyTypes.ClientServer} session but it was {session.NetworkConfig.NetworkTopology}!"); + + // Verify session 3 + currentCount++; + session = m_NetcodeAnalytics.RecentSessions[currentCount]; + Assert.True(session.WasServer && session.WasClient, $"Expected session to be started as a host session but it was not! Server: {session.WasServer}Client: {session.WasClient}"); + Assert.True(session.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer, $"Expected session to be a {NetworkTopologyTypes.ClientServer} session but it was {session.NetworkConfig.NetworkTopology}!"); + + // Verify session 4 + currentCount++; + session = m_NetcodeAnalytics.RecentSessions[currentCount]; + Assert.True(session.WasServer && session.WasClient, $"Expected session to be started as a host session but it was not! Server: {session.WasServer}Client: {session.WasClient}"); + Assert.True(session.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority, $"Expected session to be a {NetworkTopologyTypes.DistributedAuthority} session but it was {session.NetworkConfig.NetworkTopology}!"); + + // Verify session 5 + currentCount++; + session = m_NetcodeAnalytics.RecentSessions[currentCount]; + Assert.True(session.WasServer == true && session.WasClient, $"Expected session to be started as a host session but it was not! Server: {session.WasServer}Client: {session.WasClient}"); + Assert.True(session.NetworkConfig.NetworkTopology == NetworkTopologyTypes.ClientServer, $"Expected session to be a {NetworkTopologyTypes.ClientServer} session but it was {session.NetworkConfig.NetworkTopology}!"); + + ////////////////////////////////////////////////// + // Validating that the analytics event data that would be sent should only contain information on the 3 unique session configurations + // Client-Server as a Server + // Client-Server as a Host + // Distributed Authority as a DAHost + m_NetcodeAnalytics.ModeChanged(UnityEditor.PlayModeStateChange.ExitingPlayMode, m_ServerNetworkManager); + + // Verify that we cleared out the NetcodeAnalytics.RecentSessions when we exited play mode + Assert.True(m_NetcodeAnalytics.RecentSessions.Count == 0, $"After exiting play mode we expected 0 RecentSessions but had {m_NetcodeAnalytics.RecentSessions.Count}"); + + // Adjust our expected count to be 2 less than the RecentSessions since we intentionally included two identical sessions. + // (There should only be unique session configurations included in the final analytics event data entries) + expectedCount -= 2; + + Assert.True(m_NetcodeAnalytics.AnalyticsTestResults.Count == expectedCount, $"Expected {expectedCount} analytics event but found: {m_NetcodeAnalytics.AnalyticsTestResults.Count}!"); + currentCount = 0; + // Verify event data 1 + var eventData = m_NetcodeAnalytics.AnalyticsTestResults[currentCount].Data; + Assert.True(eventData.WasServer && !eventData.WasClient, $"Expected session to be started as a server session but it was not! WasServer: {eventData.WasServer} WasClient: {eventData.WasClient}"); + Assert.True(!eventData.IsDistributedAuthority, $"Expected IsDistributedAuthority to be false but it was true!"); + + // Verify event data 2 + currentCount++; + eventData = m_NetcodeAnalytics.AnalyticsTestResults[currentCount].Data; + Assert.True(eventData.WasServer && eventData.WasClient, $"Expected session to be started as a Host session but it was not! WasServer: {eventData.WasServer} WasClient: {eventData.WasClient}"); + Assert.True(!eventData.IsDistributedAuthority, $"Expected IsDistributedAuthority to be false but it was true!"); + + // Verify event data 3 + currentCount++; + eventData = m_NetcodeAnalytics.AnalyticsTestResults[currentCount].Data; + Assert.True(eventData.WasServer && eventData.WasClient, $"Expected session to be started as a host session but it was not! WasServer: {eventData.WasServer} WasClient: {eventData.WasClient}"); + Assert.True(eventData.IsDistributedAuthority, $"Expected IsDistributedAuthority to be true but it was false!"); + yield return null; + } + } +} +#endif diff --git a/testproject/Assets/Tests/Runtime/AnalyticsTests.cs.meta b/testproject/Assets/Tests/Runtime/AnalyticsTests.cs.meta new file mode 100644 index 0000000000..0075e04b38 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/AnalyticsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 095c486606a56464ab25cccb457df562 \ No newline at end of file diff --git a/testproject/Assets/Tests/Runtime/testproject.runtimetests.asmdef b/testproject/Assets/Tests/Runtime/testproject.runtimetests.asmdef index 846250639c..80fb79d2d3 100644 --- a/testproject/Assets/Tests/Runtime/testproject.runtimetests.asmdef +++ b/testproject/Assets/Tests/Runtime/testproject.runtimetests.asmdef @@ -4,6 +4,7 @@ "references": [ "Unity.Netcode.Runtime", "Unity.Netcode.RuntimeTests", + "Unity.Netcode.Editor", "TestProject.ManualTests", "TestProject", "Unity.Addressables", diff --git a/testproject/ProjectSettings/ProjectSettings.asset b/testproject/ProjectSettings/ProjectSettings.asset index 9975ea1a15..492e94571c 100644 --- a/testproject/ProjectSettings/ProjectSettings.asset +++ b/testproject/ProjectSettings/ProjectSettings.asset @@ -17,8 +17,8 @@ PlayerSettings: defaultCursor: {fileID: 0} cursorHotspot: {x: 0, y: 0} m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} - m_ShowUnitySplashScreen: 1 - m_ShowUnitySplashLogo: 1 + m_ShowUnitySplashScreen: 0 + m_ShowUnitySplashLogo: 0 m_SplashScreenOverlayOpacity: 1 m_SplashScreenAnimation: 1 m_SplashScreenLogoStyle: 1