namespace Fusion { using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using StatsInternal; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; #endif /// /// Creates and controls a Canvas with one or multiple telemetry graphs. Can be created as a scene object or prefab, /// or be created at runtime using the methods. If created as the child of a /// then will automatically be set to true. /// [ScriptHelp(BackColor = ScriptHeaderBackColor.Olive)] [ExecuteAlways] public partial class FusionStats : Fusion.Behaviour { /// /// Options for displaying stats as screen overlays or world GameObjects. /// public enum StatCanvasTypes { Overlay, GameObject, } /// /// Predefined layout default options. /// public enum DefaultLayouts { Custom, Left, Right, UpperLeft, UpperRight, Full, } /// /// Interval (in seconds) between Graph redraws. Higher values (longer intervals) reduce CPU overhead, draw calls and garbage collection. /// [InlineHelp] [Unit(Units.Seconds)]//, DecimalPlaces = 2)] [Range(0f, 1f)] public float RedrawInterval = .1f; /// /// Selects between displaying Canvas as screen overlay, or a world GameObject. /// [Header("Layout")] [InlineHelp] [SerializeField] StatCanvasTypes _canvasType; /// /// Selects between displaying Canvas as screen overlay, or a world GameObject. /// public StatCanvasTypes CanvasType { get => _canvasType; set { _canvasType = value; //_canvas.enabled = false; DirtyLayout(2); } } /// /// Enables text labels for the control buttons. /// [InlineHelp] [SerializeField] bool _showButtonLabels = true; /// /// Enables text labels for the control buttons. /// public bool ShowButtonLabels { get => _showButtonLabels; set { _showButtonLabels = value; DirtyLayout(); } } /// /// Height of button region at top of the stats panel. Values less than or equal to 0 hide the buttons, and reduce the header size. /// [InlineHelp] [SerializeField] [Range(0, 200)] int _maxHeaderHeight = 70; /// /// Height of button region at top of the stats panel. Values less than or equal to 0 hide the buttons, and reduce the header size. /// public int MaxHeaderHeight { get => _maxHeaderHeight; set { _maxHeaderHeight = value; DirtyLayout(); } } /// /// The size of the canvas when is set to . /// [InlineHelp] [DrawIf(nameof(_canvasType), (long)StatCanvasTypes.GameObject, Hide = true)] [Range(0, 20f)] public float CanvasScale = 5f; /// /// The distance on the Z axis the canvas will be positioned. Allows moving the canvas in front of or behind the parent GameObject. /// [InlineHelp] [DrawIf(nameof(_canvasType), (long)StatCanvasTypes.GameObject, Hide = true)] [Range(-10, 10f)] public float CanvasDistance = 0f; /// /// The Rect which defines the position of the stats canvas on a GameObject. Sizes are normalized percentages.(ranges of 0f-1f). /// [InlineHelp] [SerializeField] [DrawIf(nameof(_canvasType), (long)StatCanvasTypes.GameObject, Hide = true)] [NormalizedRect(aspectRatio: 1)] Rect _gameObjectRect = new Rect(0.0f, 0.0f, 0.3f, 1.0f); public Rect GameObjectRect { get => _gameObjectRect; set { _gameObjectRect = value; DirtyLayout(); } } /// /// The Rect which defines the position of the stats canvas overlay on the screen. Sizes are normalized percentages.(ranges of 0f-1f). /// [InlineHelp] [SerializeField] [DrawIf(nameof(_canvasType), (long)StatCanvasTypes.Overlay, Hide = true)] [NormalizedRect] Rect _overlayRect = new Rect(0.0f, 0.0f, 0.3f, 1.0f); public Rect OverlayRect { get => _overlayRect; set { _overlayRect = value; DirtyLayout(); } } /// /// value which all child components will use if their value is set to Auto. /// [Header("Fusion Graphs Layout")] [InlineHelp] [SerializeField] FusionStatsGraph.Layouts _defaultLayout; public FusionStatsGraph.Layouts DefaultLayout { get => _defaultLayout; set { _defaultLayout = value; DirtyLayout(); } } /// /// UI Text on FusionGraphs can only overlay the bar graph if the canvas is perfectly facing the camera. /// Any other angles will result in ZBuffer fighting between the text and the graph bar shader. /// For uses where perfect camera billboarding is not possible (such as VR), this toggle prevents FusionGraph layouts being used where text and graphs overlap. /// Normally leave this unchecked, unless you are experiencing corrupted text rendering. /// [InlineHelp] [SerializeField] bool _noTextOverlap; public bool NoTextOverlap { get => _noTextOverlap; set { _noTextOverlap = value; DirtyLayout(); } } /// /// Disables the bar graph in , and uses a text only layout. /// Enable this if is not rendering correctly in VR. /// [InlineHelp] [SerializeField] bool _noGraphShader; public bool NoGraphShader { get => _noGraphShader; set { _noGraphShader = value; DirtyLayout(); } } /// /// Force graphs layout to use X number of columns. /// [InlineHelp] [Range(0, 16)] public int GraphColumnCount = 1; /// /// If is set to zero, then columns will automatically be added as needed to limit graphs to this width or less. /// [InlineHelp] [SerializeField] [DrawIf(nameof(GraphColumnCount), 0)] [Range(30, SCREEN_SCALE_W)] int _graphMaxWidth = SCREEN_SCALE_W / 4; /// /// If is set to zero, then columns will automatically be added as needed to limit graphs to this width or less. /// public int GraphMaxWidth { get => _graphMaxWidth; set { _graphMaxWidth = value; DirtyLayout(); } } [Header("Network Object Stats")] [SerializeField] private int _playerRef; public PlayerRef PlayerRef { get => PlayerRef.FromIndex(_playerRef); set { _playerRef = value.AsIndex; // TODO: Not needed? DirtyLayout(); } } /// /// Enables/Disables all NetworkObject related elements. /// [Header("Network Object Stats")] [InlineHelp] [SerializeField] bool _enableObjectStats; public bool EnableObjectStats { get => _enableObjectStats; set { _enableObjectStats = value; DirtyLayout(); } } /// /// The source for any specific telemetry. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_enableObjectStats))] internal NetworkObject _object; /// /// Returns the set serialized for this stat window. If that is null, returns the static MonitoredNetworkObject, /// which can be set using . /// public NetworkObject Object { get { if (_object) { return _object; } // no local object set - fallback to the global one. if (_runner == null) { // Will not be ble to lookup a network object without a valid runner. null for now. return default; } if (EnableObjectStats) { return _runner.FindObject(MonitoredNetworkObjectId); } return default; } } /// /// Height of Object title region at top of the stats panel. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_enableObjectStats))] [Range(0, 200)] int _objectTitleHeight = 48; public int ObjectTitleHeight { get => _objectTitleHeight; set { _objectTitleHeight = value; DirtyLayout(); } } /// /// Height of Object info region at top of the stats panel. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_enableObjectStats))] [Range(0, 200)] int _objectIdsHeight = 60; public int ObjectIdsHeight { get => _objectIdsHeight; set { _objectIdsHeight = value; DirtyLayout(); } } /// /// Height of Object info region at top of the stats panel. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_enableObjectStats))] [Range(0, 200)] int _objectMetersHeight = 90; public int ObjectMetersHeight { get => _objectMetersHeight; set { _objectIdsHeight = value; DirtyLayout(); } } /// /// The currently associated with this component and graphs. /// [Header("Data")] [SerializeField] [InlineHelp] [ReadOnly] NetworkRunner _runner; public NetworkRunner Runner { get { if (Application.isPlaying == false) { return null; } // Be sure the current runner is the correct runner. this.ValidateRunner(_runner); return _runner; } } public void SetRunner(NetworkRunner value) { if (_runner == value) { return; } // Keep track of which runners have active stats windows - needed so pause/unpause can affect all (since pause affects other panels) DisassociateWithRunner(_runner); _runner = value; AssociateWithRunner(value); UpdateTitle(); } /// /// Editor-Only. If no is set, this FusionStats will attempt to connect to the NetworkRunner for the current selected GameObject. /// [InlineHelp] [SerializeField] public bool RunnerFromSelected; /// /// Initializes a for all available stats, even if not initially included. /// If disabled, graphs added after initialization will be added to the bottom of the interface stack. /// [InlineHelp] public bool InitializeAllGraphs; /// /// When is null and no exists in the current scene, FusionStats will continuously attempt to find and connect to an active which matches these indicated modes. /// [InlineHelp] [ExpandableEnum(ShowInlineHelp = true)] public SimulationModes ConnectTo = SimulationModes.Host | SimulationModes.Server | SimulationModes.Client; /// /// Selects which NetworkObject stats should be displayed. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_enableObjectStats))] [ExpandableEnum(ShowInlineHelp = true)] public FieldsMask _includedObjStats = new (typeof(NetworkObjectStats).GetDefaults); /// /// Selects which NetConnection stats should be displayed. /// [InlineHelp] [SerializeField] [ExpandableEnum(ShowInlineHelp = true)] public FieldsMask _includedNetStats = new(typeof(SimulationConnectionStats).GetDefaults); /// /// Selects which Simulation stats should be displayed. /// [InlineHelp] [SerializeField] [ExpandableEnum(ShowInlineHelp = true)] public FieldsMask _includedSimStats = new(typeof(SimulationStats).GetDefaults); /// /// Automatically destroys this GameObject if the associated runner is null or inactive. /// Otherwise attempts will continuously be made to find an new active runner which is running in specified by , and connect to that. /// [Header("Life-Cycle")] [InlineHelp] [SerializeField] public bool AutoDestroy; /// /// Only one instance with the can exist if there is no associated . Will destroy any additional instances on Awake. /// [InlineHelp] [SerializeField] public bool EnforceSingle = true; /// /// Identifier used to enforce single instances of when running in Multi-Peer mode. /// When is enabled, only one instance of with this GUID will be active at any time, /// regardless of the total number of peers running. /// [InlineHelp] [DrawIf(nameof(EnforceSingle))] [SerializeField] public string Guid; /// /// The font to be used for all non-number labels. /// [Header("Customization")] [SerializeField] public Font LabelFont; /// /// The font to be used for all number labels. /// [InlineHelp] [SerializeField] public Font ValueFont; [SerializeField][HideInInspector] internal Shader GraphShader; /// /// Shows/hides controls in the inspector for defining element colors. /// [InlineHelp] [SerializeField] private bool _modifyColors; /// /// The color used for the telemetry graph data. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _graphColorGood = new Color(0.1f, 0.5f, 0.1f, 1.0f); /// /// The color used for the telemetry graph data. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _graphColorWarn = new Color(0.75f, 0.75f, 0.2f, 1.0f); /// /// The color used for the telemetry graph data. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _graphColorBad = new Color(0.9f, 0.2f, 0.2f, 1.0f); /// /// The color used for the telemetry graph data. /// [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _graphColorFlag = new Color(0.8f, 0.75f, 0.0f, 1.0f); [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _fontColor = new Color(1.0f, 1.0f, 1.0f, 1f); [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color PanelColor = new Color(0.3f, 0.3f, 0.3f, 1.0f); [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _simDataBackColor = new Color(0.1f, 0.08f, 0.08f, 1.0f); [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _netDataBackColor = new Color(0.15f, 0.14f, 0.09f, 1.0f); [InlineHelp] [SerializeField] [DrawIf(nameof(_modifyColors), Hide = true)] Color _objDataBackColor = new Color(0.0f, 0.2f, 0.4f, 1.0f); // IFusionStats interface requirements public Color FontColor => _fontColor; public Color GraphColorGood => _graphColorGood; public Color GraphColorWarn => _graphColorWarn; public Color GraphColorBad => _graphColorBad; public Color GraphColorFlag => _graphColorFlag; public Color SimDataBackColor => _simDataBackColor; public Color NetDataBackColor => _netDataBackColor; public Color ObjDataBackColor => _objDataBackColor; public Rect CurrentRect => _canvasType == StatCanvasTypes.GameObject ? _gameObjectRect : _overlayRect; Font _font; bool _hidden; bool _paused; int _layoutDirty; bool _activeDirty; double _currentDrawTime; double _delayDrawUntil; #if UNITY_EDITOR void OnValidate() { if (EnforceSingle && Guid == "") { Guid = System.Guid.NewGuid().ToString().Substring(0, 13); } _activeDirty = true; if (_layoutDirty <= 0) { _layoutDirty = 2; // Some aspects of Layout will throw warnings if run from OnValidate, so defer. // Stop deferring when entering play mode, as this will cause null errors (thanks unity). if (Application.isPlaying) { UnityEditor.EditorApplication.delayCall += CalculateLayout; } else { UnityEditor.EditorApplication.delayCall -= CalculateLayout; } } } void Reset() { ResetLayout(); } #endif /// /// Resets the layout of the stats panel to the default layout for the current . /// /// Optional parameter to enable or disable object stats. If null, the current setting is used. /// Optional parameter to set the layout for the object stats. If null, the current setting is used. /// Optional parameter to set the layout for the screen stats. If null, the current setting is used. public void ResetLayout(bool? enableObjectStats = null, DefaultLayouts? objectLayout = null, DefaultLayouts? screenLayout = null) { // Destroy existing built graphs var canv = GetComponentInChildren(); if (canv) { DestroyImmediate(canv.gameObject); } if (TryGetComponent(out var _) == false) { gameObject.AddComponent().UpdateLookAt(); } bool hasNetworkObject = GetComponentInParent(); // If attached to a NetObject if (enableObjectStats.GetValueOrDefault() || (enableObjectStats.GetValueOrDefault(true) && hasNetworkObject)) { EnableObjectStats = true; _canvasType = StatCanvasTypes.GameObject; EnforceSingle = false; GraphColumnCount = 1; } else { // If not attached to a GameObject (sim only) GraphColumnCount = 0; if (transform.parent) { _canvasType = StatCanvasTypes.GameObject; EnforceSingle = false; } else { _canvasType = StatCanvasTypes.Overlay; EnforceSingle = true; } } ApplyDefaultLayout(objectLayout.GetValueOrDefault(hasNetworkObject ? DefaultLayouts.UpperRight : DefaultLayouts.Full), StatCanvasTypes.GameObject); ApplyDefaultLayout(screenLayout.GetValueOrDefault(DefaultLayouts.Right), StatCanvasTypes.Overlay); Guid = System.Guid.NewGuid().ToString().Substring(0, 13); GenerateGraphs(); } void Awake() { if (_object == null) { if (TryGetComponent(out var no)) { _object = no; } else { _object = GetComponentInParent(true); } } if (Application.isPlaying == false) { #if UNITY_EDITOR if (_canvas) { // Hide canvas for rebuild, Unity makes this ugly. if (EditorApplication.isCompiling == false) { UnityEditor.EditorApplication.delayCall += CalculateLayout; } _layoutDirty = 2; } return; #endif } else { _foundViews = new List(); GetComponentsInChildren(true, _foundViews); } if (Guid == "") { Guid = System.Guid.NewGuid().ToString().Substring(0, 13); } if (EnforceSingle && Guid != null) { if (_activeGuids.ContainsKey(Guid)) { Destroy(this.gameObject); return; } _activeGuids.Add(Guid, this); } if (EnforceSingle && Object == null && _canvasType == StatCanvasTypes.Overlay) { DontDestroyOnLoad(gameObject); } } void Start() { if (Application.isPlaying) { Initialize(); _activeDirty = true; _layoutDirty = 2; } } void OnDestroy() { // Try to unregister this Stats in case it hasn't already. DisassociateWithRunner(_runner); // If this is the current enforce single instance of this GUID, remove it from the record. if (Guid != null) { if (_activeGuids.TryGetValue(Guid, out var stats)) { if (stats == this) { _activeGuids.Remove(Guid); } } } } private bool _graphCanvasExists => _canvasRT != null; [EditorButton("Destroy Graphs", dirtyObject: true)] [DrawIf(nameof(_graphCanvasExists), Hide = true)] void DestroyGraphs() { if (_canvasRT) { DestroyImmediate(_canvasRT.gameObject); } _canvasRT = null; } static bool? _newInputSystemFound; public static bool NewInputSystemFound { get { if (_newInputSystemFound == null) { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { var asmtypes = asm.GetTypes(); foreach (var type in asmtypes) { if (type.Namespace == "UnityEngine.InputSystem") { _newInputSystemFound = true; return true; } } } _newInputSystemFound = false; return false; } return _newInputSystemFound.Value; } } void Initialize() { // Only add an event system if no active event systems exist. if (Application.isPlaying) { if (NewInputSystemFound) { // New Input System } else { if (FindFirstObjectByType() == null) { var eventSystemGO = new GameObject("Event System"); eventSystemGO.AddComponent(); eventSystemGO.AddComponent(); if (Application.isPlaying) { DontDestroyOnLoad(eventSystemGO); } } } } if (_canvasRT == false) { GenerateGraphs(); } // Already existed before runtime. (Scene object) if (_canvasRT) { InitializeControls(); GetComponentsInChildren(true, _foundViews); foreach (var g in _foundViews) { g.Initialize(); } _layoutDirty = 1; } } void AssociateWithRunner(NetworkRunner runner) { if (runner != null) { if (_statsForRunnerLookup.TryGetValue(runner, out var runnerStats) == false) { _statsForRunnerLookup.Add(runner, new List() { this }); } else { runnerStats.Add(this); } // Notify FusionGraphs that they need to reconnect to a new runner. if (_foundGraphs != null) { foreach (var graph in _foundGraphs) { graph.Disconnect(); } } } } void DisassociateWithRunner(NetworkRunner runner) { if (runner != null && _statsForRunnerLookup.TryGetValue(runner, out var oldrunnerstats)) { if (oldrunnerstats.Contains(this)) { oldrunnerstats.Remove(this); } } } void LateUpdate() { // Use of the Runner getter here is intentional - this forces a test of the existing Runner having gone null or inactive. var runner = Runner; bool runnerIsNull = runner == null; if (AutoDestroy && runnerIsNull) { Destroy(this.gameObject); return; } if (_activeDirty) { ReapplyEnabled(); } if (_layoutDirty > 0) { CalculateLayout(); } if (Application.isPlaying == false) { return; } // NetConnection stats do not like being polled after shutdown and will throw assert fails. if (runnerIsNull || runner.IsShutdown) { return; } if (_paused) { return; } // Cap redraw rate - rate of 0 = disabled. if (RedrawInterval > 0) { var currentime = Time.timeAsDouble; if (currentime > _delayDrawUntil) { _currentDrawTime = currentime; while (_delayDrawUntil <= currentime) { _delayDrawUntil += RedrawInterval; } } if (currentime != _currentDrawTime) { return; } } if (EnableObjectStats) { RefreshObjectValues(); } foreach (var graph in _foundViews) { if (graph != null && graph.isActiveAndEnabled) { graph.Refresh(); } } } string _previousObjectTitle; void RefreshObjectValues() { var obj = Object; if (obj == null) { _objectNameText.text = "No Object"; _previousObjectTitle = "No Object"; return; } var objectName = obj.name; if (_previousObjectTitle != objectName) { _objectNameText.text = objectName; _previousObjectTitle = objectName; } } // returns true if a graph has been added. void ReapplyEnabled() { _activeDirty = false; if (_simGraphs == null || _simGraphs.Length == 0) { return; } // This is null if the children were deleted. Stop execution, or new Graphs will be created without a parent. if (_graphsLayoutRT == null) { return; } for (int i = 0; i < _simGraphs.Length; ++i) { var graph = _simGraphs[i]; bool enabled = (((long)1 << i) & _includedSimStats.Mask) != 0; if (graph == null) { if (enabled) { graph = CreateGraph(StatSourceTypes.Simulation, i, _graphsLayoutRT); _simGraphs[i] = graph; } else { continue; } } graph.gameObject.SetActive(enabled); } for (int i = 0; i < _objGraphs.Length; ++i) { var graph = _objGraphs[i]; bool enabled = _enableObjectStats && (((long)1 << i) & _includedObjStats.Mask) != 0; if (graph == null) { if (enabled) { graph = CreateGraph(StatSourceTypes.NetworkObject, i, _graphsLayoutRT); _objGraphs[i] = graph; } else { continue; } } if (_objGraphs[i] != null) { graph.gameObject.SetActive(enabled); } } for (int i = 0; i < _netGraphs.Length; ++i) { var graph = _netGraphs[i]; bool enabled = (((long)1 << i) & _includedNetStats.Mask) != 0; if (graph == null) { if (enabled) { graph = CreateGraph(StatSourceTypes.NetConnection, i, _graphsLayoutRT); _netGraphs[i] = graph; } else { continue; } } if (_netGraphs[i] != null) { graph.gameObject.SetActive(enabled); } } } } }