From c95a59c771a7a18a34bf73af61af9ec813678c83 Mon Sep 17 00:00:00 2001 From: Jahnisse Date: Wed, 3 Jun 2026 16:10:28 +0200 Subject: [PATCH] Add Framerate Counter (capped at 60) --- Assets/_Content/Scenes/TitleScreen.unity | 233 ++++++++++++++++++ .../Scripts/Core/Utils/PerformanceOverlay.cs | 117 +++++++++ .../Core/Utils/PerformanceOverlay.cs.meta | 2 + 3 files changed, 352 insertions(+) create mode 100644 Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs create mode 100644 Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta diff --git a/Assets/_Content/Scenes/TitleScreen.unity b/Assets/_Content/Scenes/TitleScreen.unity index a368ee10..3ad53f2f 100644 --- a/Assets/_Content/Scenes/TitleScreen.unity +++ b/Assets/_Content/Scenes/TitleScreen.unity @@ -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} diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs new file mode 100644 index 00000000..4caa2bf1 --- /dev/null +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs @@ -0,0 +1,117 @@ +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 +{ + // ── 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 _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) + { + _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 ─────────────────────────────────────────────────────── + + /// Toggle the overlay on / off (e.g. bind to F3). + public void Toggle() => displayText.gameObject.SetActive( + !displayText.gameObject.activeSelf); +} diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta new file mode 100644 index 00000000..449c60d4 --- /dev/null +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 508ae74360f7e5249b100f7873b182f1 \ No newline at end of file