diff --git a/Assets/_Content/Scenes/TitleScreen.unity b/Assets/_Content/Scenes/TitleScreen.unity index 3ad53f2f..3c660922 100644 --- a/Assets/_Content/Scenes/TitleScreen.unity +++ b/Assets/_Content/Scenes/TitleScreen.unity @@ -273,7 +273,7 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1195733056} - m_Father: {fileID: 1741102573} + m_Father: {fileID: 1195136605} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -1342,12 +1342,12 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 508ae74360f7e5249b100f7873b182f1, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::PerformanceOverlay - displayText: {fileID: 1195733054} - sampleSize: 60 - updateInterval: 0.1 - pipelineLatencyFrames: 2 - goodFPS: 60 - warningFPS: 30 + _displayText: {fileID: 1195733054} + _sampleSize: 60 + _updateInterval: 0.1 + _pipelineLatencyFrames: 1 + _goodFps: 60 + _warningFps: 30 --- !u!4 &1195136605 Transform: m_ObjectHideFlags: 0 @@ -1356,11 +1356,12 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1195136603} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] + m_Children: + - {fileID: 431578944} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1195733053 @@ -2233,7 +2234,6 @@ Transform: m_Children: - {fileID: 630642127} - {fileID: 1505757215} - - {fileID: 431578944} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1757053106 stripped diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs index 4caa2bf1..079a712f 100644 --- a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs @@ -2,116 +2,119 @@ using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; -/// -/// PerformanceOverlay — displays FPS, frame time, and estimated input lag. -/// Singleton: only one instance will ever exist. Safe to place in every scene — -/// duplicates are destroyed automatically. Persists across scene loads. -/// Requires: a Canvas with a Text (or TMP_Text) component assigned in the Inspector. -/// -public class PerformanceOverlay : MonoBehaviour +namespace OctoCore.UI { - // ── singleton ──────────────────────────────────────────────────────────── - public static PerformanceOverlay Instance { get; private set; } - - void Awake() + /// + /// Displays FPS, frame time, and estimated input lag. + /// Singleton that persists across scene loads. Safe to place in every scene — duplicates are destroyed automatically. + /// Requires a Canvas Text component assigned in the Inspector. + /// + public class PerformanceOverlay : MonoBehaviour { - if (Instance != null && Instance != this) + #region Constants and Static Fields + + public static PerformanceOverlay Instance { get; private set; } + + private const string GREEN = ""; + private const string YELLOW = ""; + private const string RED = ""; + private const string END = ""; + + #endregion + + #region Serialized Fields + + [Header("UI Reference")] + [SerializeField] private Text _displayText; + + [Header("FPS Settings")] + [Tooltip("Number of frames to average for a smooth reading.")] + [SerializeField] private int _sampleSize = 60; + + [Tooltip("How often (seconds) the display refreshes.")] + [SerializeField] private float _updateInterval = 0.1f; + + [Header("Input Lag Settings")] + [Tooltip("Render pipeline latency in frames. 1 for Built-in/URP, 2 for HDRP.")] + [SerializeField] private int _pipelineLatencyFrames = 1; + + [Header("Thresholds")] + [SerializeField] private int _goodFps = 60; + [SerializeField] private int _warningFps = 30; + + #endregion + + #region Private Instance Fields + + private readonly Queue _frameTimes = new Queue(); + private float _accumulatedTime; + private float _timeSinceLastUpdate; + + #endregion + + #region Unity Lifecycle Methods + + private void Awake() { - // A valid instance already exists — destroy this duplicate. - Destroy(gameObject); - return; + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + + Instance = this; + DontDestroyOnLoad(gameObject); } - Instance = this; - DontDestroyOnLoad(gameObject); - } - - [Header("UI Reference")] - [Tooltip("Assign a UI Text or TextMeshPro Text component here.")] - public Text displayText; // swap to TMP_Text if using TextMeshPro - - [Header("FPS Settings")] - [Tooltip("How many frames to average over for a smooth FPS reading.")] - public int sampleSize = 60; - - [Tooltip("How often (seconds) the display refreshes.")] - public float updateInterval = 0.1f; - - [Header("Input Lag Settings")] - [Tooltip("Frames of render pipeline latency added on top of frame time.\n" + - "Typical values: 0–1 for Forward, 1–2 for Deferred / URP with buffering.")] - public int pipelineLatencyFrames = 1; - - [Header("Thresholds (for colour coding)")] - public int goodFPS = 60; - public int warningFPS = 30; - - // ── internals ──────────────────────────────────────────────────────────── - private readonly Queue _frameTimes = new Queue(); - private float _accumulatedTime; - private float _timeSinceLastUpdate; - - // Colour tags (works with both Text and TMP richtext) - private const string GREEN = ""; - private const string YELLOW = ""; - private const string RED = ""; - private const string END = ""; - - // ── lifecycle ──────────────────────────────────────────────────────────── - void Update() - { - float dt = Time.unscaledDeltaTime; // unscaled so pausing doesn't skew readings - - // Rolling window - _frameTimes.Enqueue(dt); - _accumulatedTime += dt; - if (_frameTimes.Count > sampleSize) - _accumulatedTime -= _frameTimes.Dequeue(); - - _timeSinceLastUpdate += dt; - if (_timeSinceLastUpdate >= updateInterval) + private void Update() { - _timeSinceLastUpdate = 0f; - RefreshDisplay(); + float dt = Time.unscaledDeltaTime; + + _frameTimes.Enqueue(dt); + _accumulatedTime += dt; + if (_frameTimes.Count > _sampleSize) + _accumulatedTime -= _frameTimes.Dequeue(); + + _timeSinceLastUpdate += dt; + if (_timeSinceLastUpdate >= _updateInterval) + { + _timeSinceLastUpdate = 0f; + RefreshDisplay(); + } } + + #endregion + + #region Public Methods + + /// Toggles the overlay on / off. + public void Toggle() => _displayText.gameObject.SetActive(!_displayText.gameObject.activeSelf); + + #endregion + + #region Private Methods + + private void RefreshDisplay() + { + if (_displayText == null) return; + + float avgFrameTime = _accumulatedTime / _frameTimes.Count; + float fps = 1f / avgFrameTime; + float frameTimeMs = avgFrameTime * 1000f; + + // HID polling floor (~8 ms for 125 Hz USB; ~1 ms for 1000 Hz) + float inputLagMs = frameTimeMs * (1 + _pipelineLatencyFrames) + 8f; + + string fpsColour = fps >= _goodFps ? GREEN + : fps >= _warningFps ? YELLOW + : RED; + + _displayText.text = + $"{fpsColour}{fps,5:F0} FPS{END}\n" + + $"Frame {frameTimeMs,6:F2} ms\n" + + $"Lag ≈ {inputLagMs,6:F2} ms"; + } + + #endregion } - - // ── display logic ──────────────────────────────────────────────────────── - void RefreshDisplay() - { - if (displayText == null) return; - - // ── FPS ────────────────────────────────────────────────────────────── - float avgFrameTime = _accumulatedTime / _frameTimes.Count; // seconds - float fps = 1f / avgFrameTime; - float frameTimeMs = avgFrameTime * 1000f; - - // ── Input Lag estimate ─────────────────────────────────────────────── - // Input lag ≈ time from physical input to pixel on screen. - // Best estimate without hardware: frame time + pipeline latency frames. - // - // inputLag ≈ frameTime + (pipelineLatencyFrames × frameTime) - // = frameTime × (1 + pipelineLatencyFrames) - // - // Add 8 ms as a rough USB / Bluetooth HID polling floor. - float hidPollMs = 8f; // ~1 frame @ 125 Hz polling; 1 ms @ 1000 Hz - float inputLagMs = frameTimeMs * (1 + pipelineLatencyFrames) + hidPollMs; - - // ── colour coding ──────────────────────────────────────────────────── - string fpsColour = fps >= goodFPS ? GREEN - : fps >= warningFPS ? YELLOW - : RED; - - // ── format ─────────────────────────────────────────────────────────── - displayText.text = - $"{fpsColour}{fps,5:F0} FPS{END}\n" + - $"Frame {frameTimeMs,6:F2} ms\n" + - $"Lag ≈ {inputLagMs,6:F2} ms"; - } - - // ── public helpers ─────────────────────────────────────────────────────── - - /// Toggle the overlay on / off (e.g. bind to F3). - public void Toggle() => displayText.gameObject.SetActive( - !displayText.gameObject.activeSelf); -} +} \ No newline at end of file