Fix: FPS counter

This commit is contained in:
Jahnisse
2026-06-03 16:22:04 +02:00
parent c95a59c771
commit 1385d6a954
2 changed files with 118 additions and 115 deletions
+10 -10
View File
@@ -273,7 +273,7 @@ RectTransform:
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1195733056} - {fileID: 1195733056}
m_Father: {fileID: 1741102573} m_Father: {fileID: 1195136605}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0}
@@ -1342,12 +1342,12 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 508ae74360f7e5249b100f7873b182f1, type: 3} m_Script: {fileID: 11500000, guid: 508ae74360f7e5249b100f7873b182f1, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PerformanceOverlay m_EditorClassIdentifier: Assembly-CSharp::PerformanceOverlay
displayText: {fileID: 1195733054} _displayText: {fileID: 1195733054}
sampleSize: 60 _sampleSize: 60
updateInterval: 0.1 _updateInterval: 0.1
pipelineLatencyFrames: 2 _pipelineLatencyFrames: 1
goodFPS: 60 _goodFps: 60
warningFPS: 30 _warningFps: 30
--- !u!4 &1195136605 --- !u!4 &1195136605
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -1356,11 +1356,12 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195136603} m_GameObject: {fileID: 1195136603}
serializedVersion: 2 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_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children:
- {fileID: 431578944}
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1195733053 --- !u!1 &1195733053
@@ -2233,7 +2234,6 @@ Transform:
m_Children: m_Children:
- {fileID: 630642127} - {fileID: 630642127}
- {fileID: 1505757215} - {fileID: 1505757215}
- {fileID: 431578944}
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1757053106 stripped --- !u!114 &1757053106 stripped
@@ -2,22 +2,62 @@ using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using System.Collections.Generic; using System.Collections.Generic;
/// <summary> namespace OctoCore.UI
/// 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.
/// </summary>
public class PerformanceOverlay : MonoBehaviour
{ {
// ── singleton ──────────────────────────────────────────────────────────── /// <summary>
/// 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.
/// </summary>
public class PerformanceOverlay : MonoBehaviour
{
#region Constants and Static Fields
public static PerformanceOverlay Instance { get; private set; } public static PerformanceOverlay Instance { get; private set; }
void Awake() private const string GREEN = "<color=#00ff88>";
private const string YELLOW = "<color=#ffcc00>";
private const string RED = "<color=#ff4444>";
private const string END = "</color>";
#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<float> _frameTimes = new Queue<float>();
private float _accumulatedTime;
private float _timeSinceLastUpdate;
#endregion
#region Unity Lifecycle Methods
private void Awake()
{ {
if (Instance != null && Instance != this) if (Instance != null && Instance != this)
{ {
// A valid instance already exists — destroy this duplicate.
Destroy(gameObject); Destroy(gameObject);
return; return;
} }
@@ -26,92 +66,55 @@ public class PerformanceOverlay : MonoBehaviour
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
} }
[Header("UI Reference")] private void Update()
[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<float> _frameTimes = new Queue<float>();
private float _accumulatedTime;
private float _timeSinceLastUpdate;
// Colour tags (works with both Text and TMP richtext)
private const string GREEN = "<color=#00ff88>";
private const string YELLOW = "<color=#ffcc00>";
private const string RED = "<color=#ff4444>";
private const string END = "</color>";
// ── lifecycle ────────────────────────────────────────────────────────────
void Update()
{ {
float dt = Time.unscaledDeltaTime; // unscaled so pausing doesn't skew readings float dt = Time.unscaledDeltaTime;
// Rolling window
_frameTimes.Enqueue(dt); _frameTimes.Enqueue(dt);
_accumulatedTime += dt; _accumulatedTime += dt;
if (_frameTimes.Count > sampleSize) if (_frameTimes.Count > _sampleSize)
_accumulatedTime -= _frameTimes.Dequeue(); _accumulatedTime -= _frameTimes.Dequeue();
_timeSinceLastUpdate += dt; _timeSinceLastUpdate += dt;
if (_timeSinceLastUpdate >= updateInterval) if (_timeSinceLastUpdate >= _updateInterval)
{ {
_timeSinceLastUpdate = 0f; _timeSinceLastUpdate = 0f;
RefreshDisplay(); RefreshDisplay();
} }
} }
// ── display logic ──────────────────────────────────────────────────────── #endregion
void RefreshDisplay()
{
if (displayText == null) return;
// ── FPS ────────────────────────────────────────────────────────────── #region Public Methods
float avgFrameTime = _accumulatedTime / _frameTimes.Count; // seconds
/// <summary>Toggles the overlay on / off.</summary>
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 fps = 1f / avgFrameTime;
float frameTimeMs = avgFrameTime * 1000f; float frameTimeMs = avgFrameTime * 1000f;
// ── Input Lag estimate ─────────────────────────────────────────────── // HID polling floor (~8 ms for 125 Hz USB; ~1 ms for 1000 Hz)
// Input lag ≈ time from physical input to pixel on screen. float inputLagMs = frameTimeMs * (1 + _pipelineLatencyFrames) + 8f;
// 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
string fpsColour = fps >= goodFPS ? GREEN : fps >= _warningFps ? YELLOW
: fps >= warningFPS ? YELLOW
: RED; : RED;
// ── format ─────────────────────────────────────────────────────────── _displayText.text =
displayText.text =
$"{fpsColour}{fps,5:F0} FPS{END}\n" + $"{fpsColour}{fps,5:F0} FPS{END}\n" +
$"Frame {frameTimeMs,6:F2} ms\n" + $"Frame {frameTimeMs,6:F2} ms\n" +
$"Lag ≈ {inputLagMs,6:F2} ms"; $"Lag ≈ {inputLagMs,6:F2} ms";
} }
// ── public helpers ─────────────────────────────────────────────────────── #endregion
}
/// <summary>Toggle the overlay on / off (e.g. bind to F3).</summary>
public void Toggle() => displayText.gameObject.SetActive(
!displayText.gameObject.activeSelf);
} }