Fix: FPS counter
This commit is contained in:
@@ -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,116 +2,119 @@ 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>
|
||||||
public static PerformanceOverlay Instance { get; private set; }
|
/// Displays FPS, frame time, and estimated input lag.
|
||||||
|
/// Singleton that persists across scene loads. Safe to place in every scene — duplicates are destroyed automatically.
|
||||||
void Awake()
|
/// Requires a Canvas Text component assigned in the Inspector.
|
||||||
|
/// </summary>
|
||||||
|
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 = "<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()
|
||||||
{
|
{
|
||||||
// A valid instance already exists — destroy this duplicate.
|
if (Instance != null && Instance != this)
|
||||||
Destroy(gameObject);
|
{
|
||||||
return;
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance = this;
|
private void Update()
|
||||||
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<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
|
|
||||||
|
|
||||||
// Rolling window
|
|
||||||
_frameTimes.Enqueue(dt);
|
|
||||||
_accumulatedTime += dt;
|
|
||||||
if (_frameTimes.Count > sampleSize)
|
|
||||||
_accumulatedTime -= _frameTimes.Dequeue();
|
|
||||||
|
|
||||||
_timeSinceLastUpdate += dt;
|
|
||||||
if (_timeSinceLastUpdate >= updateInterval)
|
|
||||||
{
|
{
|
||||||
_timeSinceLastUpdate = 0f;
|
float dt = Time.unscaledDeltaTime;
|
||||||
RefreshDisplay();
|
|
||||||
|
_frameTimes.Enqueue(dt);
|
||||||
|
_accumulatedTime += dt;
|
||||||
|
if (_frameTimes.Count > _sampleSize)
|
||||||
|
_accumulatedTime -= _frameTimes.Dequeue();
|
||||||
|
|
||||||
|
_timeSinceLastUpdate += dt;
|
||||||
|
if (_timeSinceLastUpdate >= _updateInterval)
|
||||||
|
{
|
||||||
|
_timeSinceLastUpdate = 0f;
|
||||||
|
RefreshDisplay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
/// <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 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 ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>Toggle the overlay on / off (e.g. bind to F3).</summary>
|
|
||||||
public void Toggle() => displayText.gameObject.SetActive(
|
|
||||||
!displayText.gameObject.activeSelf);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user