#region copyright
//-------------------------------------------------------
// Copyright (C) Dmitriy Yukhanov [https://codestage.net]
//-------------------------------------------------------
#endregion
using System.Text;
#pragma warning disable 169
namespace CodeStage.AdvancedFPSCounter
{
using System.Collections.Generic;
using CountersData;
using Labels;
using UnityEngine;
using UnityEngine.UI;
using Utils;
using UnityEngine.SceneManagement;
///
/// Allows to see frames per second counter, memory usage counter and some simple hardware information right in running app on any device.
/// Just call AFPSCounter.AddToScene() to use it.
///
/// You also may add it to GameObject (without any child or parent objects, with zero rotation, zero position and 1,1,1 scale) as usual or through the
/// "GameObject > Create Other > Code Stage > Advanced FPS Counter" menu.
[AddComponentMenu(MenuPath)]
[DisallowMultipleComponent]
[HelpURL("http://codestage.net/uas_files/afps/api/class_code_stage_1_1_advanced_f_p_s_counter_1_1_a_f_p_s_counter.html")]
public class AFPSCounter : MonoBehaviour
{
// ----------------------------------------------------------------------------
// constants
// ----------------------------------------------------------------------------
private const string MenuPath = "Code Stage/🚀 Advanced FPS Counter";
private const string ComponentName = "Advanced FPS Counter";
#if UNITY_EDITOR
internal const string LogPrefix = "[AFPSCounter]: ";
#else
internal const string LogPrefix = "[AFPSCounter]: ";
#endif
internal const char NewLine = '\n';
internal const char Space = ' ';
// ----------------------------------------------------------------------------
// public fields
// ----------------------------------------------------------------------------
///
/// Frames Per Second counter.
///
public FPSCounterData fpsCounter = new FPSCounterData();
///
/// Mono or heap memory counter.
///
public MemoryCounterData memoryCounter = new MemoryCounterData();
///
/// Device hardware info.
/// Shows CPU name, cores (threads) count, GPU name, total VRAM, total RAM, screen DPI and screen size.
///
public DeviceInfoCounterData deviceInfoCounter = new DeviceInfoCounterData();
///
/// Used to enable / disable plugin at runtime. Set to KeyCode.None to disable.
///
[Tooltip("Used to enable / disable plugin at runtime.\nSet to None to disable.")]
public KeyCode hotKey = KeyCode.BackQuote;
///
/// Used to enable / disable plugin at runtime. Make two circle gestures with your finger \ mouse to switch plugin on and off.
///
[Tooltip("Used to enable / disable plugin at runtime.\nMake two circle gestures with your finger \\ mouse to switch plugin on and off.")]
public bool circleGesture;
///
/// Hot key modifier: any Control on Windows or any Command on Mac.
///
[Tooltip("Hot key modifier: any Control on Windows or any Command on Mac.")]
public bool hotKeyCtrl;
///
/// Hot key modifier: any Shift.
///
[Tooltip("Hot key modifier: any Shift.")]
public bool hotKeyShift;
///
/// Hot key modifier: any Alt.
///
[Tooltip("Hot key modifier: any Alt.")]
public bool hotKeyAlt;
[Tooltip("Prevents current or other topmost Game Object from destroying on level (scene) load.\nApplied once, on Start phase.")]
[SerializeField]
private bool keepAlive = true;
// ----------------------------------------------------------------------------
// private fields
// ----------------------------------------------------------------------------
private Canvas canvas;
private CanvasScaler canvasScaler;
private bool externalCanvas;
private DrawableLabel[] labels;
private int anchorsCount;
private int cachedVSync = -1;
private int cachedFrameRate = -1;
private bool inited;
/* circle gesture variables */
private readonly List gesturePoints = new List();
private int gestureCount;
// ----------------------------------------------------------------------------
// properties
// ----------------------------------------------------------------------------
///
/// Read-only property allowing to figure out current keepAlive state.
///
public bool KeepAlive
{
get { return keepAlive; }
}
#region OperationMode
[Tooltip("Disabled: removes labels and stops all internal processes except Hot Key listener.\n\n" +
"Background: removes labels keeping counters alive; use for hidden performance monitoring.\n\n" +
"Normal: shows labels and runs all internal processes as usual.")]
[SerializeField]
private OperationMode operationMode = OperationMode.Normal;
///
/// Use it to change %AFPSCounter operation mode.
///
/// Disabled: removes labels and stops all internal processes except Hot Key listener.
/// Background: removes labels keeping counters alive. May be useful for hidden performance monitoring and benchmarking. Hot Key has no effect in this mode.
/// Normal: shows labels and runs all internal processes as usual.
public OperationMode OperationMode
{
get { return operationMode; }
set
{
if (operationMode == value || !Application.isPlaying) return;
operationMode = value;
if (operationMode != OperationMode.Disabled)
{
if (operationMode == OperationMode.Background)
{
for (var i = 0; i < anchorsCount; i++)
{
labels[i].Clear();
}
}
OnEnable();
fpsCounter.UpdateValue();
memoryCounter.UpdateValue();
deviceInfoCounter.UpdateValue();
UpdateTexts();
}
else
{
OnDisable();
}
}
}
#endregion
#region ForceFrameRate
[Tooltip("Allows to see how your game performs on specified frame rate.\n" +
"Does not guarantee selected frame rate. Set -1 to render as fast as possible in current conditions.\n" +
"IMPORTANT: this option disables VSync while enabled!")]
[SerializeField]
private bool forceFrameRate;
///
/// Allows to see how your game performs on specified frame rate.
/// \htmlonlyIMPORTANT:\endhtmlonly this option disables VSync while enabled!
///
/// Useful to check how physics performs on slow devices for example.
public bool ForceFrameRate
{
get { return forceFrameRate; }
set
{
if (forceFrameRate == value || !Application.isPlaying) return;
forceFrameRate = value;
if (operationMode == OperationMode.Disabled) return;
RefreshForcedFrameRate();
}
}
#endregion
#region ForcedFrameRate
[Range(-1, 200)]
[SerializeField]
private int forcedFrameRate = -1;
///
/// Desired frame rate for ForceFrameRate option, does not guarantee selected frame rate.
/// Set to -1 to render as fast as possible in current conditions.
///
public int ForcedFrameRate
{
get { return forcedFrameRate; }
set
{
if (forcedFrameRate == value || !Application.isPlaying) return;
forcedFrameRate = value;
if (operationMode == OperationMode.Disabled) return;
RefreshForcedFrameRate();
}
}
#endregion
/* look and feel settings */
#region Background
[Tooltip("Background for all texts. Cheapest effect. Overhead: 1 Draw Call.")]
[SerializeField]
private bool background = true;
///
/// Background for all texts.
///
public bool Background
{
get { return background; }
set
{
if (background == value || !Application.isPlaying) return;
background = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeBackground(background);
}
}
}
#endregion
#region BackgroundColor
[Tooltip("Color of the background.")]
[SerializeField]
private Color backgroundColor = new Color32(0, 0, 0, 155);
///
/// Color of the background.
///
public Color BackgroundColor
{
get { return backgroundColor; }
set
{
if (backgroundColor == value || !Application.isPlaying) return;
backgroundColor = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeBackgroundColor(backgroundColor);
}
}
}
#endregion
#region BackgroundPadding
[Tooltip("Padding of the background.")]
[Range(0, 30)]
[SerializeField]
private int backgroundPadding = 5;
///
/// Padding of the background. Change forces the HorizontalLayoutGroup.SetLayoutHorizontal() call.
///
public int BackgroundPadding
{
get { return backgroundPadding; }
set
{
if (backgroundPadding == value || !Application.isPlaying) return;
backgroundPadding = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeBackgroundPadding(backgroundPadding);
}
}
}
#endregion
#region Shadow
[Tooltip("Shadow effect for all texts. This effect uses extra resources. Overhead: medium CPU and light GPU usage.")]
[SerializeField]
private bool shadow;
///
/// Shadow effect for all texts.
///
public bool Shadow
{
get { return shadow; }
set
{
if (shadow == value || !Application.isPlaying) return;
shadow = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeShadow(shadow);
}
}
}
#endregion
#region ShadowColor
[Tooltip("Color of the shadow effect.")]
[SerializeField]
private Color shadowColor = new Color32(0, 0, 0, 128);
///
/// Color of the shadow effect.
///
public Color ShadowColor
{
get { return shadowColor; }
set
{
if (shadowColor == value || !Application.isPlaying) return;
shadowColor = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeShadowColor(shadowColor);
}
}
}
#endregion
#region ShadowDistance
[Tooltip("Distance of the shadow effect.")]
[SerializeField]
private Vector2 shadowDistance = new Vector2(1, -1);
///
/// Distance of the shadow effect.
///
public Vector2 ShadowDistance
{
get { return shadowDistance; }
set
{
if (shadowDistance == value || !Application.isPlaying) return;
shadowDistance = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeShadowDistance(shadowDistance);
}
}
}
#endregion
#region Outline
[Tooltip("Outline effect for all texts. Resource-heaviest effect. Overhead: huge CPU and medium GPU usage. Not recommended for use unless really necessary.")]
[SerializeField]
private bool outline;
///
/// Outline effect for all texts.
///
public bool Outline
{
get { return outline; }
set
{
if (outline == value || !Application.isPlaying) return;
outline = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeOutline(outline);
}
}
}
#endregion
#region OutlineColor
[Tooltip("Color of the outline effect.")]
[SerializeField]
private Color outlineColor = new Color32(0, 0, 0, 128);
///
/// Color of the outline effect.
///
public Color OutlineColor
{
get { return outlineColor; }
set
{
if (outlineColor == value || !Application.isPlaying) return;
outlineColor = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeOutlineColor(outlineColor);
}
}
}
#endregion
#region OutlineDistance
[Tooltip("Distance of the outline effect.")]
[SerializeField]
private Vector2 outlineDistance = new Vector2(1, -1);
///
/// Distance of the outline effect.
///
public Vector2 OutlineDistance
{
get { return outlineDistance; }
set
{
if (outlineDistance == value || !Application.isPlaying) return;
outlineDistance = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeOutlineDistance(outlineDistance);
}
}
}
#endregion
#region AutoScale
[Tooltip("Controls own Canvas Scaler scale mode. Check to use ScaleWithScreenSize. Otherwise ConstantPixelSize will be used.")]
[SerializeField]
private bool autoScale;
///
/// Controls own Canvas Scaler scale mode. Check to use ScaleWithScreenSize. Otherwise ConstantPixelSize will be used.
///
public bool AutoScale
{
get { return autoScale; }
set
{
if (autoScale == value || !Application.isPlaying) return;
autoScale = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
if (canvasScaler == null) return;
canvasScaler.uiScaleMode = autoScale
? CanvasScaler.ScaleMode.ScaleWithScreenSize
: CanvasScaler.ScaleMode.ConstantPixelSize;
}
}
#endregion
#region ScaleFactor
[Tooltip("Controls global scale of all texts.")]
[Range(0f, 30f)]
[SerializeField]
private float scaleFactor = 1;
///
/// Controls global scale of all texts.
///
public float ScaleFactor
{
get { return scaleFactor; }
set
{
if (System.Math.Abs(scaleFactor - value) < 0.001f || !Application.isPlaying) return;
scaleFactor = value;
if (operationMode == OperationMode.Disabled || canvasScaler == null) return;
canvasScaler.scaleFactor = scaleFactor;
}
}
#endregion
#region LabelsFont
[Tooltip("Leave blank to use default font.")]
[SerializeField]
private Font labelsFont;
///
/// Font to render labels with.
///
public Font LabelsFont
{
get { return labelsFont; }
set
{
if (labelsFont == value || !Application.isPlaying) return;
labelsFont = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeFont(labelsFont);
}
}
}
#endregion
#region FontSize
[Tooltip("Set to 0 to use font size specified in the font importer.")]
[Range(0, 100)]
[SerializeField]
private int fontSize = 14;
///
/// The font size to use (for dynamic fonts).
///
/// If this is set to a non-zero value, the font size specified in the font importer is overridden with a custom size. This is only supported for fonts set to use dynamic font rendering. Other fonts will always use the default font size.
public int FontSize
{
get { return fontSize; }
set
{
if (fontSize == value || !Application.isPlaying) return;
fontSize = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeFontSize(fontSize);
}
}
}
#endregion
#region LineSpacing
[Tooltip("Space between lines in labels.")]
[Range(0f, 10f)]
[SerializeField]
private float lineSpacing = 1;
///
/// Space between lines.
///
public float LineSpacing
{
get { return lineSpacing; }
set
{
if (System.Math.Abs(lineSpacing - value) < 0.001f || !Application.isPlaying) return;
lineSpacing = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeLineSpacing(lineSpacing);
}
}
}
#endregion
#region CountersSpacing
[Tooltip("Lines count between different counters in a single label.")]
[Range(0, 10)]
[SerializeField]
private int countersSpacing;
///
/// Lines count between different counters in a single label.
///
public int CountersSpacing
{
get { return countersSpacing; }
set
{
if (countersSpacing == value || !Application.isPlaying) return;
countersSpacing = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
UpdateTexts();
for (var i = 0; i < anchorsCount; i++)
{
labels[i].dirty = true;
}
}
}
#endregion
#region PaddingOffset
[Tooltip("Pixel offset for anchored labels. Automatically applied to all labels.")]
[SerializeField]
private Vector2 paddingOffset = new Vector2(5, 5);
///
/// Pixel offset for anchored labels. Automatically applied to all labels.
///
public Vector2 PaddingOffset
{
get { return paddingOffset; }
set
{
if (paddingOffset == value || !Application.isPlaying) return;
paddingOffset = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
for (var i = 0; i < anchorsCount; i++)
{
labels[i].ChangeOffset(paddingOffset);
}
}
}
#endregion
#region PixelPerfect
[Tooltip("Controls own canvas Pixel Perfect property.")]
[SerializeField]
private bool pixelPerfect = true;
///
/// Controls own canvas Pixel Perfect property.
///
public bool PixelPerfect
{
get { return pixelPerfect; }
set
{
if (pixelPerfect == value || !Application.isPlaying) return;
pixelPerfect = value;
if (operationMode == OperationMode.Disabled || labels == null) return;
canvas.pixelPerfect = pixelPerfect;
}
}
#endregion
/* advanced settings */
#region SortingOrder
[Tooltip("Sorting order to use for the canvas.\nSet higher value to get closer to the user.")]
[SerializeField]
private int sortingOrder = 10000;
///
/// Sorting order to use for the canvas.
///
/// Set higher value to get closer to the user.
public int SortingOrder
{
get { return sortingOrder; }
set
{
if (sortingOrder == value || !Application.isPlaying) return;
sortingOrder = value;
if (operationMode == OperationMode.Disabled || canvas == null) return;
canvas.sortingOrder = sortingOrder;
}
}
#endregion
// preventing direct instantiation
private AFPSCounter() { }
// ----------------------------------------------------------------------------
// instance
// ----------------------------------------------------------------------------
///
/// Allows reaching public properties from code. Can be null.
/// \sa AddToScene()
///
public static AFPSCounter Instance { get; private set; }
private static AFPSCounter GetOrCreateInstance(bool keepAlive)
{
if (Instance != null) return Instance;
var counter = FindObjectOfType();
if (counter != null)
{
Instance = counter;
}
else
{
var newInstance = CreateInScene(false);
newInstance.keepAlive = keepAlive;
}
return Instance;
}
// ----------------------------------------------------------------------------
// public static methods
// ----------------------------------------------------------------------------
///
/// Creates and adds new %AFPSCounter instance to the scene if it doesn't exists with keepAlive set to true.
/// Use it to instantiate %AFPSCounter from code before using AFPSCounter.Instance.
///
/// Existing or new %AFPSCounter instance.
public static AFPSCounter AddToScene()
{
return AddToScene(true);
}
///
/// Creates and adds new %AFPSCounter instance to the scene if it doesn't exists.
/// Use it to instantiate %AFPSCounter from code before using AFPSCounter.Instance.
///
/// Set true to prevent AFPSCounter's Game Object from destroying on level (scene) load.
/// Applies to the new Instance only.
/// Existing or new %AFPSCounter instance.
public static AFPSCounter AddToScene(bool keepAlive)
{
return GetOrCreateInstance(keepAlive);
}
[System.Obsolete("Please use SelfDestroy() instead. This method will be removed in future updates.")]
public static void Dispose()
{
SelfDestroy();
}
///
/// Use it to completely dispose current %AFPSCounter instance.
///
public static void SelfDestroy()
{
if (Instance != null) Instance.DisposeInternal();
}
// ----------------------------------------------------------------------------
// internal static methods
// ----------------------------------------------------------------------------
internal static string Color32ToHex(Color32 color)
{
return color.r.ToString("x2") + color.g.ToString("x2") + color.b.ToString("x2") + color.a.ToString("x2");
}
// ----------------------------------------------------------------------------
// private static methods
// ----------------------------------------------------------------------------
private static AFPSCounter CreateInScene(bool lookForExistingContainer = true)
{
var container = lookForExistingContainer ? GameObject.Find(ComponentName) : null;
if (container == null)
{
container = new GameObject(ComponentName)
{
layer = LayerMask.NameToLayer("UI")
};
#if UNITY_EDITOR
if (!Application.isPlaying)
{
UnityEditor.Undo.RegisterCreatedObjectUndo(container, "Create " + ComponentName);
if (UnityEditor.Selection.activeTransform != null)
{
container.transform.parent = UnityEditor.Selection.activeTransform;
}
UnityEditor.Selection.activeObject = container;
}
#endif
}
var newInstance = container.AddComponent();
return newInstance;
}
// ----------------------------------------------------------------------------
// unity callbacks
// ----------------------------------------------------------------------------
#region unity callbacks
private void Awake()
{
/* checks for duplication */
if (Instance != null && Instance.keepAlive)
{
Destroy(this);
return;
}
/* editor-only checks */
#if UNITY_EDITOR
if (!IsPlacedCorrectly())
{
Debug.LogWarning(LogPrefix + "incorrect placement detected! Please, use \"" + GameObjectMenuGroup + MenuPath + "\" menu to fix it!", this);
}
#endif
/* initialization */
Instance = this;
fpsCounter.Init(this);
memoryCounter.Init(this);
deviceInfoCounter.Init(this);
ConfigureCanvas();
ConfigureLabels();
inited = true;
}
private void Start()
{
if (keepAlive)
{
// will keep alive itself or other topmost game object
DontDestroyOnLoad(transform.root.gameObject);
SceneManager.sceneLoaded += OnLevelWasLoadedNew;
}
}
private void Update()
{
if (!inited) return;
ProcessHotKey();
if (circleGesture && CircleGestureMade())
{
SwitchCounter();
}
}
private void OnLevelWasLoadedNew(Scene scene, LoadSceneMode mode)
{
OnLevelLoadedCallback();
}
private void OnLevelLoadedCallback()
{
if (!inited) return;
if (!fpsCounter.Enabled) return;
fpsCounter.OnLevelLoadedCallback();
}
private void OnEnable()
{
if (!inited) return;
if (operationMode == OperationMode.Disabled) return;
ActivateCounters();
Invoke("RefreshForcedFrameRate", 0.5f);
}
private void OnDisable()
{
if (!inited) return;
DeactivateCounters();
if (IsInvoking("RefreshForcedFrameRate")) CancelInvoke("RefreshForcedFrameRate");
RefreshForcedFrameRate(true);
for (var i = 0; i < anchorsCount; i++)
{
labels[i].Clear();
}
}
private void OnDestroy()
{
if (inited)
{
fpsCounter.Destroy();
memoryCounter.Destroy();
deviceInfoCounter.Destroy();
if (labels != null)
{
for (var i = 0; i < anchorsCount; i++)
{
labels[i].Destroy();
}
System.Array.Clear(labels, 0, anchorsCount);
labels = null;
}
inited = false;
}
if (canvas != null)
{
Destroy(canvas.gameObject);
}
if (transform.childCount <= 1)
{
Destroy(gameObject);
}
if (Instance == this) Instance = null;
}
#endregion
// ----------------------------------------------------------------------------
// internal methods
// ----------------------------------------------------------------------------
internal void MakeDrawableLabelDirty(LabelAnchor anchor)
{
if (operationMode == OperationMode.Normal)
{
labels[(int)anchor].dirty = true;
}
}
public System.Action OnAddLog;
internal void UpdateTexts()
{
if (operationMode != OperationMode.Normal) return;
var anyContentPresent = false;
if (fpsCounter.Enabled)
{
var label = labels[(int)fpsCounter.Anchor];
if (label.newText.Length > 0)
{
label.newText.Append(new string(NewLine, countersSpacing + 1));
}
label.newText.Append(fpsCounter.text);
label.dirty |= fpsCounter.dirty;
fpsCounter.dirty = false;
anyContentPresent = true;
}
if (memoryCounter.Enabled)
{
var label = labels[(int)memoryCounter.Anchor];
if (label.newText.Length > 0)
{
label.newText.Append(new string(NewLine, countersSpacing + 1));
}
label.newText.Append(memoryCounter.text);
label.dirty |= memoryCounter.dirty;
memoryCounter.dirty = false;
anyContentPresent = true;
}
if (deviceInfoCounter.Enabled)
{
var label = labels[(int)deviceInfoCounter.Anchor];
if (label.newText.Length > 0)
{
label.newText.Append(new string(NewLine, countersSpacing + 1));
}
label.newText.Append(deviceInfoCounter.text);
label.dirty |= deviceInfoCounter.dirty;
deviceInfoCounter.dirty = false;
anyContentPresent = true;
}
if (anyContentPresent)
{
for (var i = 0; i < anchorsCount; i++)
{
labels[i].CheckAndUpdate();
}
}
else
{
for (var i = 0; i < anchorsCount; i++)
{
labels[i].Clear();
}
}
}
// ----------------------------------------------------------------------------
// private methods
// ----------------------------------------------------------------------------
private void ConfigureCanvas()
{
var parentCanvas = GetComponentInParent