namespace Fusion.Statistics { using System; using UnityEngine; using System.Collections.Generic; using System.Linq; using UnityEngine.EventSystems; using UnityEngine.Profiling; using UnityEngine.Serialization; [RequireComponent(typeof(NetworkRunner))] [AddComponentMenu("Fusion/Statistics/Fusion Statistics")] public class FusionStatistics : SimulationBehaviour, ISpawned { internal List ActiveGraphs => _statsGraph; // Setup prefabs private GameObject _statsCanvasPrefab; private FusionNetworkObjectStatsGraphCombine _objectGraphCombinePrefab; private const string STATS_CANVAS_PREFAB_PATH = "FusionStatsResources/FusionStatsRenderPanel"; private const string STATS_OBJECT_COMBINE_PREFAB_PATH = "FusionStatsResources/NetworkObjectStatistics"; private List _statsGraph; private FusionStatsPanelHeader _header; private FusionStatsConfig _config; private FusionStatsCanvas _statsCanvas; private GameObject _statsPanelObject; private Dictionary _objectStatsGraphCombines; [InlineHelp] [ExpandableEnum] [SerializeField] private RenderSimStats _statsEnabled; [InlineHelp] [SerializeField] private CanvasAnchor _canvasAnchor = CanvasAnchor.TopRight; [FormerlySerializedAs("_statsConfig")] [SerializeField] [Header("Custom configuration to override default values.\nSelect only one stat flag per configuration.")] private List _statsCustomConfig = new List(); internal List StatsCustomConfig => _statsCustomConfig; /// /// Gets a value indicating whether the statistics panel is active. /// public bool IsPanelActive => _statsPanelObject != false; [System.Serializable] public struct FusionStatisticsStatCustomConfig { public RenderSimStats Stat; public float Threshold1; public float Threshold2; public float Threshold3; public bool IgnoreZeroOnBuffer; public bool IgnoreZeroOnAverageCalculation; public int AccumulateTimeMs; } private void Awake() { _statsGraph = new List(); _statsCanvasPrefab = Resources.Load(STATS_CANVAS_PREFAB_PATH); _objectGraphCombinePrefab = Resources.Load(STATS_OBJECT_COMBINE_PREFAB_PATH); if (_statsCanvasPrefab == null || _objectGraphCombinePrefab == null) { Log.Error($"Error loading the required assets for Fusion Statistics, destroying stats instance. Make sure that the following paths are valid for the Fusion Statistics resource assets: \n 1. {STATS_CANVAS_PREFAB_PATH} \n 2. {STATS_OBJECT_COMBINE_PREFAB_PATH}"); Destroy(this); } } void ISpawned.Spawned() { SetupStatisticsPanel(); } /// /// Sets the custom configuration for Fusion Statistics. /// /// The list of custom configurations for Fusion Statistics. public void SetStatsCustomConfig(List customConfig) { if (customConfig == default) { Log.Warn("Trying to set a null Fusion Statistics custom stats config"); return; } _statsCustomConfig = customConfig; ApplyCustomConfig(); } /// /// Sets the anchor position of the Fusion Statistics canvas. /// /// The anchor position of the canvas (TopLeft or TopRight). public void SetCanvasAnchor(CanvasAnchor anchor) { _canvasAnchor = anchor; if (_statsCanvas == false) return; _statsCanvas.SetCanvasAnchor(anchor); } private void ApplyCustomConfig() { if (!_header) return; _header.ApplyStatsConfig(_statsCustomConfig); } /// /// Called from a custom editor script. /// Will update any editor information into the fusion statistics. /// public void OnEditorChange() { RenderEnabledStats(); ApplyCustomConfig(); SetCanvasAnchor(_canvasAnchor); } private void RenderEnabledStats() { if (IsPanelActive == false) return; _header.SetStatsToRender(_statsEnabled); } internal void UpdateStatsEnabled(RenderSimStats stats) { _statsEnabled = stats; } /// /// Sets up the statistics panel for Fusion statistic tracking. /// public void SetupStatisticsPanel() { if (IsPanelActive) return; // Was not registered on the Runner yet if (Runner == null) { var runner = GetComponent(); if (runner.IsRunning == false) { Log.Warn($"Network Runner on ({runner.gameObject}) is not yet running."); return; } runner.AddGlobal(this); // Return because when spawned is called the setup method will be called again. return; } _objectStatsGraphCombines = new Dictionary(); _statsPanelObject = Instantiate(_statsCanvasPrefab, transform); _statsCanvas = _statsPanelObject.GetComponentInChildren(); _statsCanvas.SetupStatsCanvas(this, _canvasAnchor, DestroyStatisticsPanel); _header = _statsPanelObject.GetComponentInChildren(); _header.SetupHeader(Runner.LocalPlayer.ToString(), this); _config = _statsPanelObject.GetComponentInChildren(true); _statsPanelObject.AddComponent(); ApplyCustomConfig(); Runner.AddVisibilityNodes(_statsPanelObject); if (_statsEnabled != 0) RenderEnabledStats(); // Setup Event system if (!EventSystem.current) { Log.Debug("Fusion Statistics: No event system detected, creating one."); new GameObject("EventSystem-FusionStatistics", typeof(EventSystem), typeof(StandaloneInputModule)); } } /// /// Sets the world anchor for Fusion Statistics. Set null to return to screen space overlay. /// /// The FusionStatsWorldAnchor component that defines the anchor object. Null to return to screen space overlay. /// The scale of the statistics panel. public void SetWorldAnchor(FusionStatsWorldAnchor anchor, float scale) { _config.SetWorldCanvasScale(scale); if (anchor == null) { _config.ResetToCanvasAnchor(); } else { _config.SetWorldAnchor(anchor.transform); } } /// /// Destroys the statistics panel. /// public void DestroyStatisticsPanel() { var keys = _objectStatsGraphCombines?.Keys.ToArray(); if (keys != null) { foreach (var fusionNetworkObjectStatistics in keys) { MonitorNetworkObject(fusionNetworkObjectStatistics.NetworkObject, fusionNetworkObjectStatistics, false); } } _objectStatsGraphCombines?.Clear(); _statsGraph.Clear(); Destroy(_statsPanelObject); _statsPanelObject = null; if (Runner) { if (Runner.TryGetFusionStatistics(out var statisticsManager)) { statisticsManager.ObjectStatisticsManager.ClearMonitoredNetworkObjects(); } } } public bool MonitorNetworkObject(NetworkObject networkObject, FusionNetworkObjectStatistics objectStatisticsInstance, bool monitor) { if (Runner.TryGetFusionStatistics(out var statisticsManager)) { statisticsManager.ObjectStatisticsManager.MonitorNetworkObjectStatistics(networkObject.Id, monitor); } if (monitor) { // If Id already monitored on the stats, return false to destroy the object statistics instance. if (_objectStatsGraphCombines.ContainsKey(objectStatisticsInstance)) return false; var graphCombine = Instantiate(_objectGraphCombinePrefab, _header.ContentRect); graphCombine.SetupNetworkObject(networkObject, this, objectStatisticsInstance); _objectStatsGraphCombines.Add(objectStatisticsInstance, graphCombine); } else { if (_objectStatsGraphCombines.Remove(objectStatisticsInstance, out var graphCombine)) { Destroy(graphCombine.gameObject); Destroy(objectStatisticsInstance); } } return true; } void UpdateAllGraphs(FusionStatisticsManager statisticsManager) { var now = DateTime.Now; foreach (var statsGraphBase in _statsGraph) { statsGraphBase.UpdateGraph(Runner, statisticsManager, ref now); } } public void RegisterGraph(FusionStatsGraphBase graph) { _statsGraph.Add(graph); } public void UnregisterGraph(FusionStatsGraphBase graph) { _statsGraph.Remove(graph); } private void Update() { // Safety exit if (!Runner) return; Profiler.BeginSample("Fusion Statistics Update Graph"); // Collect and update if (Runner.TryGetFusionStatistics(out var statisticsManager)) { UpdateAllGraphs(statisticsManager); } Profiler.EndSample(); } } }