Add Framerate Counter (capped at 60)

This commit is contained in:
Jahnisse
2026-06-03 16:10:28 +02:00
parent 3c8e9080f3
commit c95a59c771
3 changed files with 352 additions and 0 deletions
+233
View File
@@ -241,6 +241,108 @@ RectTransform:
m_CorrespondingSourceObject: {fileID: 5856627074613423090, guid: 10b589291a51b4a4f858c6c79ab6772a, type: 3}
m_PrefabInstance: {fileID: 23563224}
m_PrefabAsset: {fileID: 0}
--- !u!1 &431578943
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 431578944}
- component: {fileID: 431578947}
- component: {fileID: 431578946}
- component: {fileID: 431578945}
m_Layer: 0
m_Name: Canvas_FPS
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &431578944
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 431578943}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1195733056}
m_Father: {fileID: 1741102573}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!114 &431578945
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 431578943}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.GraphicRaycaster
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &431578946
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 431578943}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.CanvasScaler
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &431578947
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 431578943}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!1001 &522028756
PrefabInstance:
m_ObjectHideFlags: 0
@@ -1211,6 +1313,135 @@ Transform:
- {fileID: 1411514848}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1195136603
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1195136605}
- component: {fileID: 1195136604}
m_Layer: 0
m_Name: PerfManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1195136604
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195136603}
m_Enabled: 1
m_EditorHideFlags: 0
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
--- !u!4 &1195136605
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195136603}
serializedVersion: 2
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_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1195733053
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1195733056}
- component: {fileID: 1195733055}
- component: {fileID: 1195733054}
m_Layer: 0
m_Name: PerfText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1195733054
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195733053}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Text
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: 8c54d6a69a6e0604b9408d95d425ffc0, type: 3}
m_FontSize: 25
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 8
m_MaxSize: 102
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: New Text
--- !u!222 &1195733055
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195733053}
m_CullTransparentMesh: 1
--- !u!224 &1195733056
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1195733053}
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_Father: {fileID: 431578944}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 80, y: -15}
m_SizeDelta: {x: 160, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &1291952569
GameObject:
m_ObjectHideFlags: 0
@@ -2002,6 +2233,7 @@ 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
@@ -2956,3 +3188,4 @@ SceneRoots:
- {fileID: 1530196629}
- {fileID: 1741102573}
- {fileID: 1034573946}
- {fileID: 1195136605}
@@ -0,0 +1,117 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
/// <summary>
/// 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 ────────────────────────────────────────────────────────────
public static PerformanceOverlay Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
// A valid instance already exists — destroy this duplicate.
Destroy(gameObject);
return;
}
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<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;
RefreshDisplay();
}
}
// ── 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);
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 508ae74360f7e5249b100f7873b182f1