using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UI = UnityEngine.UI;
using Fusion;
using Stats = Fusion.Simulation.Statistics;
using Fusion.StatsInternal;
#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 = EditorHeaderBackColor.Olive)]
[ExecuteAlways]
public class FusionStats : Fusion.Behaviour {
#if UNITY_EDITOR
[MenuItem("Fusion/Add Fusion Stats", false, 1000)]
[MenuItem("GameObject/Fusion/Add Fusion Stats")]
public static void AddFusionStatsToScene() {
var selected = Selection.activeGameObject;
if (selected && PrefabUtility.IsPartOfPrefabAsset(selected)) {
Debug.LogWarning("Open prefabs before running 'Add Fusion Stats' on them.");
return;
}
var fs = new GameObject("FusionStats");
if (selected) {
fs.transform.SetParent(Selection.activeGameObject.transform);
}
fs.transform.localPosition = default;
fs.transform.localRotation = default;
fs.transform.localScale = Vector3.one;
fs.AddComponent();
fs.AddComponent();
EditorGUIUtility.PingObject(fs.gameObject);
Selection.activeGameObject = fs.gameObject;
}
#endif
///
/// 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,
}
// Lookup for all FusionStats associated with active runners.
static Dictionary> _statsForRunnerLookup = new Dictionary>();
// Record of active SimStats, used to prevent more than one _guid version from existing (in the case of SimStats existing in a scene that gets cloned in Multi-Peer).
static Dictionary _activeGuids = new Dictionary();
// Added to make calling by reflection cleaner internally. Used in RunnerVisibilityControls.
internal static FusionStats CreateInternal(NetworkRunner runner = null, DefaultLayouts layout = DefaultLayouts.Left, Stats.NetStatFlags? netStatsMask = null, Stats.SimStatFlags? simStatsMask = null) {
return Create(null, runner, layout, layout, netStatsMask, simStatsMask);
}
///
/// Creates a new GameObject with a component, attaches it to any supplied parent, and generates Canvas/Graphs.
///
///
/// Generated FusionStats component and GameObject will be added as a child of this transform.
/// Uses a predefined position.
/// The network stats to be enabled. If left null, default statistics will be used.
/// The simulation stats to be enabled. If left null, default statistics will be used.
///
public static FusionStats Create(Transform parent = null, NetworkRunner runner = null, DefaultLayouts? screenLayout = null, DefaultLayouts? objectLayout = null, Stats.NetStatFlags? netStatsMask = null, Stats.SimStatFlags? simStatsMask = null) {
var go = new GameObject($"{nameof(FusionStats)} {(runner ? runner.name : "null")}");
FusionStats stats;
if (parent) {
go.transform.SetParent(parent);
}
stats = go.AddComponent();
stats.ResetInternal(null, netStatsMask, simStatsMask, objectLayout, screenLayout);
stats.Runner = runner;
if (runner != null) {
stats.AutoDestroy = true;
}
return stats;
}
public static Stats.NetStatFlags DefaultNetStatsMask => Stats.NetStatFlags.RoundTripTime | Stats.NetStatFlags.ReceivedPacketSizes | Stats.NetStatFlags.SentPacketSizes;
///
/// The gets the default SimStats. Some are only intended for Fusion internal development and aren't useful to users.
///
#if FUSION_DEV
public const Stats.SimStatFlags DefaultSimStatsMask = (Stats.SimStatFlags)(-1);
#else
public const Stats.SimStatFlags DefaultSimStatsMask =
~(
Stats.SimStatFlags.InterpDiff |
Stats.SimStatFlags.InterpUncertainty |
Stats.SimStatFlags.InterpMultiplier |
Stats.SimStatFlags.InputOffsetTarget |
Stats.SimStatFlags.InputOffsetDeviation |
Stats.SimStatFlags.InputReceiveDeltaDeviation
);
#endif
const int SCREEN_SCALE_W = 1080;
const int SCREEN_SCALE_H = 1080;
const float TEXT_MARGIN = 0.25f;
const float TITLE_HEIGHT = 20f;
const int MARGIN = FusionStatsUtilities.MARGIN;
const int PAD = FusionStatsUtilities.PAD;
const string PLAY_TEXT = "PLAY";
const string PAUS_TEXT = "PAUSE";
const string SHOW_TEXT = "SHOW";
const string HIDE_TEXT = "HIDE";
const string CLER_TEXT = "CLEAR";
const string CNVS_TEXT = "CANVAS";
const string CLSE_TEXT = "CLOSE";
const string PLAY_ICON = "\u25ba";
const string PAUS_ICON = "\u05f0";
const string HIDE_ICON = "\u25bc";
const string SHOW_ICON = "\u25b2";
const string CLER_ICON = "\u1d13";
const string CNVS_ICON = "\ufb26"; //"\u2261";
const string CLSE_ICON = "x";
// Used by DrawIfAttribute to determine inspector visibility of fields are runtime.
bool ShowColorControls => !Application.isPlaying && _modifyColors;
bool IsPlaying => Application.isPlaying;
///
/// Interval (in seconds) between Graph redraws. Higher values (longer intervals) reduce CPU overhead, draw calls and garbage collection.
///
[InlineHelp]
[Unit(Units.Seconds, 1f, 0f, DecimalPlaces = 2)]
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 = 80;
///
/// 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, DrawIfHideType.Hide)]
[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, DrawIfHideType.Hide)]
[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, DrawIfHideType.Hide)]
[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, DrawIfHideType.Hide)]
[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]
FusionGraph.Layouts _defaultLayout;
public FusionGraph.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), compareToValue: (long)0, DrawIfHideType.ReadOnly)]
[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();
}
}
///
/// Enables/Disables all NetworkObject related elements.
///
[Header("Network Object Stats")]
[InlineHelp]
[SerializeField]
[WarnIf(nameof(ShowMissingNetObjWarning), true, "No NetworkObject found on this GameObject, nor parent. Object stats will be unavailable.")]
bool _enableObjectStats;
public bool EnableObjectStats {
get => _enableObjectStats;
set {
_enableObjectStats = value;
DirtyLayout();
}
}
bool ShowMissingNetObjWarning {
get => _enableObjectStats && this.Object == null;
}
///
/// The source for any specific telemetry.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(EnableObjectStats), true)]
NetworkObject _object;
public NetworkObject Object {
get {
if (_object == null) {
_object = GetComponentInParent();
}
return _object;
}
}
///
/// Height of Object title region at top of the stats panel.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(EnableObjectStats), true)]
[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), true)]
[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), true)]
[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]
[EditorDisabled]
NetworkRunner _runner;
public NetworkRunner Runner {
get {
if (Application.isPlaying == false) {
return null;
}
// If the current runner shutdown, reset the runner so a new one can be found
if (_runner) {
if (_runner.IsShutdown) {
Runner = null;
} else {
return _runner;
}
}
if (Object) {
var runner = _object.Runner;
if (runner && (!EnforceSingle || (runner.Mode & ConnectTo) != 0)) {
Runner = runner;
return _runner;
}
}
FusionStatsUtilities.TryFindActiveRunner(this, out var found, ConnectTo);
Runner = found;
return found;
}
set {
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();
}
}
///
/// 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]
[VersaMask]
public SimulationModes ConnectTo = /*SimulationModes.Host | SimulationModes.Server | */SimulationModes.Client;
///
/// Selects which NetworkObject stats should be displayed.
///
[InlineHelp]
[SerializeField]
[VersaMask]
[DrawIf(nameof(EnableObjectStats), true)]
Stats.ObjStatFlags _includedObjStats;
public Stats.ObjStatFlags IncludedObjectStats {
get => _includedObjStats;
set {
_includedObjStats = value;
_activeDirty = true;
}
}
///
/// Selects which NetConnection stats should be displayed.
///
[InlineHelp]
[SerializeField]
[VersaMask]
Stats.NetStatFlags _includedNetStats;
public Stats.NetStatFlags IncludedNetStats {
get => _includedNetStats;
set {
_includedNetStats = value;
_activeDirty = true;
}
}
///
/// Selects which Simulation stats should be displayed.
///
[InlineHelp]
[SerializeField]
[VersaMask]
Stats.SimStatFlags _includedSimStats;
public Stats.SimStatFlags IncludedSimStats {
get => _includedSimStats;
set {
_includedSimStats = value;
_activeDirty = true;
}
}
///
/// 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. Will destroy any clones 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), true)]
[SerializeField]
public string Guid;
[Header("Customization")]
///
/// Shows/hides controls in the inspector for defining element colors.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(IsPlaying), false, DrawIfHideType.Hide)]
private bool _modifyColors;
public bool ModifyColors => _modifyColors;
///
/// The color used for the telemetry graph data.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _graphColorGood = new Color(0.1f, 0.5f, 0.1f, 0.9f);
///
/// The color used for the telemetry graph data.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _graphColorWarn = new Color(0.75f, 0.75f, 0.2f, 0.9f);
///
/// The color used for the telemetry graph data.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _graphColorBad = new Color(0.9f, 0.2f, 0.2f, 0.9f);
///
/// The color used for the telemetry graph data.
///
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _graphColorFlag = new Color(0.8f, 0.75f, 0.0f, 1.0f);
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _fontColor = new Color(1.0f, 1.0f, 1.0f, 1f);
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color PanelColor = new Color(0.3f, 0.3f, 0.3f, 0.9f);
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _simDataBackColor = new Color(0.1f, 0.08f, 0.08f, 1.0f);
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
Color _netDataBackColor = new Color(0.15f, 0.14f, 0.09f, 1.0f);
[InlineHelp]
[SerializeField]
[DrawIf(nameof(ShowColorControls), true, DrawIfHideType.Hide)]
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;
//[Header("Graph Connections")]
[SerializeField] [HideInInspector] FusionGraph[] _simGraphs;
[SerializeField] [HideInInspector] FusionGraph[] _objGraphs;
[SerializeField] [HideInInspector] FusionGraph[] _netGraphs;
[NonSerialized] List _foundViews;
[NonSerialized] List _foundGraphs;
[SerializeField] [HideInInspector] UI.Text _titleText;
[SerializeField] [HideInInspector] UI.Text _clearIcon;
[SerializeField] [HideInInspector] UI.Text _pauseIcon;
[SerializeField] [HideInInspector] UI.Text _togglIcon;
[SerializeField] [HideInInspector] UI.Text _closeIcon;
[SerializeField] [HideInInspector] UI.Text _canvsIcon;
[SerializeField] [HideInInspector] UI.Text _clearLabel;
[SerializeField] [HideInInspector] UI.Text _pauseLabel;
[SerializeField] [HideInInspector] UI.Text _togglLabel;
[SerializeField] [HideInInspector] UI.Text _closeLabel;
[SerializeField] [HideInInspector] UI.Text _canvsLabel;
[SerializeField] [HideInInspector] UI.Text _objectNameText;
[SerializeField] [HideInInspector] UI.GridLayoutGroup _graphGridLayoutGroup;
[SerializeField] [HideInInspector] Canvas _canvas;
[SerializeField] [HideInInspector] RectTransform _canvasRT;
[SerializeField] [HideInInspector] RectTransform _rootPanelRT;
[SerializeField] [HideInInspector] RectTransform _guidesRT;
[SerializeField] [HideInInspector] RectTransform _headerRT;
[SerializeField] [HideInInspector] RectTransform _statsPanelRT;
[SerializeField] [HideInInspector] RectTransform _graphsLayoutRT;
[SerializeField] [HideInInspector] RectTransform _titleRT;
[SerializeField] [HideInInspector] RectTransform _buttonsRT;
[SerializeField] [HideInInspector] RectTransform _objectTitlePanelRT;
[SerializeField] [HideInInspector] RectTransform _objectIdsGroupRT;
[SerializeField] [HideInInspector] RectTransform _objectMetersPanelRT;
[SerializeField] [HideInInspector] RectTransform _clientIdPanelRT;
[SerializeField] [HideInInspector] RectTransform _authorityPanelRT;
[SerializeField] [HideInInspector] UI.Button _titleButton;
[SerializeField] [HideInInspector] UI.Button _objctButton;
[SerializeField] [HideInInspector] UI.Button _clearButton;
[SerializeField] [HideInInspector] UI.Button _togglButton;
[SerializeField] [HideInInspector] UI.Button _pauseButton;
[SerializeField] [HideInInspector] UI.Button _closeButton;
[SerializeField] [HideInInspector] UI.Button _canvsButton;
public Rect CurrentRect => _canvasType == StatCanvasTypes.GameObject ? _gameObjectRect : _overlayRect;
void UpdateTitle() {
var runnername = _runner ? _runner.name : "Disconnected";
if (_titleText) {
_titleText.text = runnername;
}
}
Shader Shader {
get => Resources.Load("FusionGraphShader");
}
Font _font;
bool _hidden;
bool _paused;
int _layoutDirty;
bool _activeDirty;
double _currentDrawTime;
double _delayDrawUntil;
void DirtyLayout(int minimumRefreshes = 1) {
if (_layoutDirty < minimumRefreshes) {
_layoutDirty = minimumRefreshes;
}
}
#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() {
ResetInternal();
}
#endif
void ResetInternal(
bool? enableObjectStats = null,
Stats.NetStatFlags? netStatsMask = null,
Stats.SimStatFlags? simStatsMask = null,
DefaultLayouts? objectLayout = null,
DefaultLayouts? screenLayout = null
) {
// Destroy existing built graphs
var canv = GetComponentInChildren