1. 项目概述为什么我们需要一个FPS显示器在Unity开发中尤其是涉及到性能优化、调试或者向玩家展示游戏运行状态时实时显示帧率Frames Per Second FPS是一个看似简单却极其核心的功能。它不仅仅是屏幕上跳动的几个数字更是我们洞察游戏性能瓶颈、评估优化效果、甚至提升玩家体验的“仪表盘”。很多开发者包括我自己在早期项目里都习惯依赖Unity Profiler或者第三方工具来查看帧率但在实际发布版本、移动端测试或者给策划、美术同事演示时一个内嵌的、简洁的FPS显示器就显得不可或缺。你可能遇到过这些场景测试反馈“游戏有点卡”但你不知道是偶发掉帧还是持续性能问题优化了某个Shader后想直观地看到帧率提升了多少或者在一个技术Demo里想向团队证明当前方案的流畅度。一个自制的FPS显示模块就能完美解决这些问题。它轻量、可定制、不依赖外部环境能集成到任何Unity支持的平台PC、移动端、主机、WebGL等。市面上虽然有Fraps、MSI Afterburner等优秀的第三方帧率显示工具但它们无法集成到最终的游戏包体中也无法在移动设备上运行。因此掌握在Unity中实现FPS显示的方法是每个追求性能与质量的开发者应该具备的基本功。2. 核心思路与方案选型实现FPS显示的核心逻辑并不复杂在每一帧或每隔一段时间计算上一段时间内渲染的平均帧数。但在Unity中我们可以通过多种路径达到这个目的每种方案都有其适用的场景和优缺点。选择哪种取决于你的具体需求是只需要一个简单的调试信息还是要一个美观的、可配置的UI组件2.1 方案一使用Unity自带的Application.targetFrameRate与Time类进行基础计算这是最经典、最直接的方法。其核心是利用Time类提供的两个关键属性Time.deltaTime上一帧的持续时间以秒为单位和Time.unscaledDeltaTime不受时间缩放影响的上一帧持续时间。核心公式FPS 1 / Time.deltaTime。这个公式计算的是瞬时帧率即当前这一帧的帧率。但瞬时帧率波动很大直接显示会频繁跳动阅读体验很差。因此我们通常采用平均帧率在一段时间内比如过去1秒用总帧数 / 这段时间来计算。为什么选择平均帧率游戏运行中由于场景复杂度变化、GC垃圾回收触发等原因帧时间deltaTime会有波动。瞬时FPS可能从60突然掉到30又立刻恢复如果直接显示这个值用户看到的就是疯狂闪烁的数字无法判断整体流畅度。平均帧率通过平滑处理能更稳定地反映近期性能趋势是更实用的指标。方案优势零依赖完全使用Unity引擎内置API无需任何第三方插件或Asset。跨平台在所有Unity支持的平台上都能完美运行。完全可控从计算逻辑到显示样式所有细节都由你掌控便于深度定制和集成到项目框架中。方案局限需要手动处理UI显示和更新逻辑。如果需要更高级的功能如帧时间分布图、历史曲线需要额外编写更多代码。2.2 方案二利用Unity的UI系统如UGUI/UIToolkit进行可视化无论采用哪种计算方案最终都需要将FPS数值显示在屏幕上。Unity的UGUIUnity GUI是目前最主流、最成熟的UI解决方案适合制作复杂的游戏内HUD。而UIToolkit原名UIElements是较新的、基于样式的UI系统特别适合编辑器扩展和运行时UI在2022 LTS及以后版本中运行时支持已趋于稳定。对于FPS显示器这种简单的文本显示UGUI的Text或TextMeshPro组件是绝佳选择。TextMeshPro是Unity官方推荐的文本渲染方案它提供了更清晰的字体渲染、丰富的文本效果如轮廓、阴影、字距调整和更好的性能几乎是现代Unity项目的标配。显示策略我们不需要每帧都更新UI文本那样会造成不必要的性能开销。一个常见的优化是间隔更新例如每0.2秒或0.5秒更新一次显示值。这样既能保证信息的实时性又能避免UI成为性能瓶颈本身。2.3 方案三考量其他方案如Debug.Log或第三方资产Debug.Log最简单粗暴的方法是在Update中Debug.Log(“FPS: ” fps)。但这会淹没你的控制台严重影响编辑器运行效率且发布后无法查看绝对不推荐用于正式的FPS显示需求。第三方资产Asset Store上有许多优秀的性能监控插件如“FPS Display”、“Advanced FPS Counter”等。它们功能强大可能包含内存监控、硬件温度显示等。如果你的项目急需一个功能全面的性能HUD且不想自己造轮子购买这类资产是高效的选择。但缺点也很明显需要付费可能带来额外的学习成本和潜在的兼容性问题并且其内部逻辑对你来说是黑盒定制化修改可能比较困难。综合建议对于大多数项目特别是需要将FPS显示作为内置功能或调试工具的项目方案一自研计算逻辑 方案二UGUI/TextMeshPro显示的组合是最佳实践。它平衡了灵活性、可控性、性能和跨平台能力。接下来我们将深入这个自研方案的实现细节。3. 分步实现一个可配置的FPS显示器我们将创建一个名为FPSDisplay的MonoBehaviour脚本并搭配一个简单的UGUI Canvas来实现。目标是制作一个既可用于开发调试也可直接打包到发布版本中的通用组件。3.1 步骤一创建UI与脚本创建UI Canvas在Unity Hierarchy窗口右键 - UI - Canvas。这将创建一个渲染UI的画布。在Canvas下右键 - UI - Text - TextMeshPro如果你已导入TMP Essentials或 TextLegacy。建议使用TextMeshPro - Text如果首次使用Unity会提示你导入TMP资源包确认即可。将创建的Text对象重命名为“FPS Text”。调整其锚点Anchor到屏幕右上角或其他你希望显示的位置并设置好字体大小、颜色。例如白色字体大小24带一点黑色阴影以在任何背景下都清晰可读。创建C#脚本在Project窗口右键 - Create - C# Script命名为FPSDisplay。双击脚本在编辑器中打开我们将开始编写核心逻辑。3.2 步骤二编写FPS计算核心逻辑FPSDisplay.cs脚本的完整实现如下我已添加了详尽的注释using UnityEngine; using TMPro; // 如果使用TextMeshPro // using UnityEngine.UI; // 如果使用Legacy Text public class FPSDisplay : MonoBehaviour { [Header(显示设置)] public TextMeshProUGUI fpsText; // 关联的TextMeshPro UI文本组件 // public Text fpsText; // 如果使用Legacy UI Text public float updateInterval 0.2f; // 更新显示的时间间隔秒 public bool showAverageFPS true; // 是否显示平均FPS public bool showMinFPS false; // 是否显示最低FPS统计周期内 public bool showMaxFPS false; // 是否显示最高FPS统计周期内 [Header(高级设置)] public int targetFrameRate 60; // 目标帧率用于颜色提示 public Color goodColor Color.green; // 帧率良好时的颜色 public Color warnColor Color.yellow; // 帧率警告时的颜色 public Color badColor Color.red; // 帧率较差时的颜色 public float warnThreshold 0.75f; // 警告阈值targetFrameRate的百分比 public float badThreshold 0.5f; // 差阈值targetFrameRate的百分比 // 私有变量用于计算 private float _accumulatedTime 0f; // 累计时间 private int _framesCount 0; // 累计帧数 private float _currentFPS 0f; // 当前计算出的FPS private float _minFPS float.MaxValue; // 统计周期内最低FPS private float _maxFPS 0f; // 统计周期内最高FPS private float _timeSinceLastReset 0f; // 距离上次重置统计的时间 void Start() { // 初始化如果没有指定Text组件尝试从当前GameObject上获取 if (fpsText null) { fpsText GetComponentTextMeshProUGUI(); // fpsText GetComponentText(); } // 可选设置目标帧率。注意在移动端为了省电可能需要不同的策略。 // Application.targetFrameRate targetFrameRate; // 重置统计变量 ResetStats(); } void Update() { // 1. 累积时间和帧数 float deltaTime Time.unscaledDeltaTime; // 使用不受时间缩放影响的时间 _accumulatedTime deltaTime; _framesCount; _timeSinceLastReset deltaTime; // 2. 计算瞬时FPS并更新最小/最大值 float instantFPS 1f / deltaTime; _minFPS Mathf.Min(_minFPS, instantFPS); _maxFPS Mathf.Max(_maxFPS, instantFPS); // 3. 如果累计时间达到了更新间隔则计算平均FPS并更新UI if (_accumulatedTime updateInterval) { // 计算平均FPS _currentFPS _framesCount / _accumulatedTime; // 更新UI显示 UpdateDisplay(); // 重置累计器和统计周期 ResetStats(); } } void ResetStats() { _accumulatedTime 0f; _framesCount 0; // 可选每N秒重置一次最小/最大值以反映近期性能 if (_timeSinceLastReset 5f) // 例如每5秒重置一次 { _minFPS float.MaxValue; _maxFPS 0f; _timeSinceLastReset 0f; } } void UpdateDisplay() { if (fpsText null) return; string displayText ; // 构建显示字符串 if (showAverageFPS) { displayText $FPS: {_currentFPS:F1}; // :F1 表示保留一位小数 } if (showMinFPS) { displayText $\nMin: {_minFPS:F1}; } if (showMaxFPS) { displayText $\nMax: {_maxFPS:F1}; } fpsText.text displayText; // 根据当前FPS设置文本颜色视觉提示 float fpsRatio _currentFPS / targetFrameRate; if (fpsRatio warnThreshold) fpsText.color goodColor; else if (fpsRatio badThreshold) fpsText.color warnColor; else fpsText.color badColor; } // 提供一个公共方法方便其他脚本获取当前FPS public float GetCurrentFPS() { return _currentFPS; } }关键逻辑解读Time.unscaledDeltaTimevsTime.deltaTime这里我选择了Time.unscaledDeltaTime。因为FPS是衡量硬件渲染性能的指标它不应该受到游戏内Time.timeScale时间缩放常用于慢动作效果的影响。使用unscaledDeltaTime能确保我们测量的是真实的、物理的帧间隔时间。间隔更新我们在Update中累积帧数和时间但只在累积时间超过updateInterval如0.2秒时才计算FPS并更新UI。这避免了每帧都进行字符串拼接和UI重绘的开销是一种重要的性能优化。最小/最大FPS统计通过记录一个统计周期内的瞬时FPS我们可以得到该周期内的性能波动范围。这对于发现偶发的卡顿表现为MinFPS很低特别有用。注意我设置了每5秒重置一次最小/最大值这是为了防止历史极值长期存在失去参考意义。这个重置周期你可以根据需求调整。颜色反馈通过将当前FPS与目标帧率比较并设置不同的颜色可以提供直观的性能状态提示。绿色代表良好黄色代表警告红色代表需要关注。3.3 步骤三配置与运行将FPSDisplay脚本拖拽到场景中任意一个GameObject上例如直接拖到Canvas上或创建一个空的“Managers”对象。在Inspector面板中将之前创建的“FPS Text”对象拖拽到脚本的Fps Text字段。调整参数Update Interval设置为0.2到0.5秒之间平衡实时性与性能。勾选你希望显示的统计信息平均、最小、最大FPS。调整颜色和阈值使其符合你的视觉偏好和性能标准例如对于目标60帧的游戏你可以设置45帧以上为绿色30-45帧为黄色低于30帧为红色。运行游戏。你应该能在屏幕指定位置看到实时更新的FPS信息并且颜色会根据帧率变化。注意在编辑器Editor中运行游戏时帧率可能会受到编辑器本身开销的影响而不如独立运行Build时稳定和高。评估性能时以打包后的版本为准更为准确。4. 高级功能与性能优化实战一个基础的FPS显示器已经完成但在实际项目中我们往往需要更多功能来应对复杂的调试和优化场景。4.1 实现帧时间Frame Time显示对于性能调优帧时间每帧消耗的毫秒数有时比FPS更直观。因为FPS是倒数关系FPS 1000 / FrameTime(ms)当帧时间从16.7ms60FPS增加到33.3ms30FPS时FPS下降了30但帧时间增加了16.6ms。优化时我们更关心的是“哪一帧慢了多久”。实现方法在计算平均FPS的同时计算平均帧时间。private float _currentFrameTimeMS 0f; // 当前平均帧时间毫秒 // 在计算平均FPS的地方同时计算 _currentFrameTimeMS (_accumulatedTime / _framesCount) * 1000f; // 秒转毫秒 // 在UpdateDisplay中添加到显示文本 displayText $\nFrameTime: {_currentFrameTimeMS:F2} ms;这样你就能同时看到“FPS: 55.6”和“FrameTime: 18.0 ms”对性能瓶颈有更精确的把握。4.2 添加性能告警与日志记录在开发过程中我们可能希望当帧率低于某个阈值时能自动记录日志或触发警告方便后续分析。实现思路在UpdateDisplay方法中添加条件判断。void UpdateDisplay() { // ... 原有的显示和颜色代码 ... // 性能告警 if (_currentFPS 30f _accumulatedTime 1.0f) // 持续1秒以上低于30帧 { // 避免每帧都打印可以加一个冷却时间 if (Time.unscaledTime - _lastWarningTime 5f) { Debug.LogWarning($”[性能告警] 持续低帧率: {_currentFPS:F1} FPS at {Time.unscaledTime:F2}s“); _lastWarningTime Time.unscaledTime; // 这里还可以触发更复杂的操作如自动截屏、记录场景信息等 } } } private float _lastWarningTime 0f;这个功能在自动化测试或长时间运行的压力测试中非常有用能帮你自动捕捉到性能下降的时刻。4.3 针对移动平台的特殊优化在Android或iOS设备上帧率显示需要考虑更多屏幕刷新率适配现代移动设备有60Hz、90Hz、120Hz等多种刷新率。我们的目标帧率不应硬编码为60。可以使用Screen.currentResolution.refreshRate来获取当前屏幕的刷新率返回值为整数如60、90、120并以此作为targetFrameRate的参考。但注意这个值可能不准确更常见的做法是让玩家在设置中选择帧率上限。发热与降频长时间高帧率运行会导致设备发热和CPU/GPU降频反而引起帧率波动。一个良好的做法是在非战斗或菜单界面主动限制帧率以节省电量。可以通过Application.targetFrameRate动态调整。绘制调用Draw Call监控在移动端Draw Call是重要的性能指标。你可以扩展FPS显示器同时统计UnityStats.batches合批后的绘制调用次数并显示出来。但这属于更高级的优化范畴。4.4 使用对象池优化UI更新如果你的游戏需要同时显示大量动态更新的UI文本虽然FPS显示器通常只有一个频繁地创建和销毁字符串会产生GC垃圾回收压力。对于FPS显示这种固定格式的文本可以使用StringBuilder来构建字符串避免每次更新都产生新的字符串垃圾。using System.Text; private StringBuilder _sb new StringBuilder(50); // 预分配容量 void UpdateDisplay() { if (fpsText null) return; _sb.Clear(); // 清空复用 _sb.Append(“FPS: “).Append(_currentFPS.ToString(”F1“)); if (showMinFPS) { _sb.Append(”\nMin: “).Append(_minFPS.ToString(”F1“)); } // ... 类似追加其他信息 ... fpsText.text _sb.ToString(); // ... 颜色设置 ... }对于单个FPS文本这种优化的收益微乎其微但这是一个良好的编程习惯特别是在性能敏感的移动项目中。5. 常见问题排查与实战心得即使代码看起来正确在实际集成和运行中你仍可能会遇到一些“坑”。以下是我在多个项目中总结的一些典型问题及解决方法。5.1 FPS显示为0、NaN或异常高值现象运行后FPS文本显示为0、NaN非数字或者一个不切实际的高值如几千。排查检查Time.deltaTime这是最常见的原因。在游戏刚启动的第一帧或者当游戏时间被暂停Time.timeScale 0时Time.deltaTime可能为0。如果用1 / 0计算就会得到无穷大显示为NaN或异常值。这就是为什么我们必须使用Time.unscaledDeltaTime并在计算前判断其是否大于一个极小值如Mathf.Epsilon。在上面的示例代码中我们使用了unscaledDeltaTime它即使在timeScale0时也会返回一个基于真实时间的小值避免了除零错误。检查UI引用确保脚本中fpsText变量正确引用了场景中的UI Text组件。如果引用为空文本自然不会更新。可以在Start或Awake方法中添加空值检查并尝试自动查找。检查更新间隔如果updateInterval设置得过大比如10秒你可能需要等待很久才能看到第一次更新。将其设为0.2-0.5秒比较合适。5.2 编辑器内帧率正常打包后帧率显示异常现象在Unity Editor里运行FPS稳定但打包成PC或移动端应用后FPS显示值很低或不稳定。排查发布构建的优化差异编辑器运行包含大量调试符号和开销。打包时确保使用了适当的编译优化如Release模式、IL2CPP后端、代码剥离。在Player Settings中检查这些设置。垂直同步VSync在Player Settings - Resolution and Presentation中VSync Count设置会影响帧率上限。如果设置为“Every V Blank”帧率会被限制在屏幕刷新率通常是60。如果你计算FPS时超过了这个值可能是计算逻辑有误。可以尝试在代码开始时设置QualitySettings.vSyncCount 0并启用Application.targetFrameRate -1无限制来测试。目标平台性能真机设备的性能远不如开发电脑。需要使用性能分析工具如Unity Profiler连接真机或Android的SystraceiOS的Instruments来定位是CPU瓶颈、GPU瓶颈还是内存问题。5.3 FPS显示器本身消耗了过多性能现象开启FPS显示后整体帧率下降了。优化间隔更新我们已经做了这是最重要的优化。确保updateInterval不是太小比如0.01秒。使用TextMeshPro对于动态文本TextMeshPro通常比Legacy Text性能更好尤其是在文本内容频繁变化时。禁用不必要的功能如果不需显示Min/Max FPS关闭对应的计算和字符串拼接。在发布版本中禁用最终发布给玩家的版本应该通过条件编译或运行时开关来彻底关闭FPS显示及其所有计算逻辑。#if !DEVELOPMENT_BUILD // 或者 #if !UNITY_EDITOR void Update() { } // 空Update不执行任何计算 #endif你可以在Unity的Build Settings中通过Scripting Define Symbols添加DEVELOPMENT_BUILD符号用于区分开发包和发布包。5.4 多分辨率与UI适配问题现象在不同屏幕分辨率或比例下FPS文本跑到屏幕外或位置不对。解决使用锚点Anchors在Unity UI中正确设置Text对象的Rect Transform锚点。如果你想固定在右上角就将锚点预设Anchor Presets设置为右上角Top-Right。使用Canvas Scaler确保Canvas上挂载了Canvas Scaler组件并设置为适合你项目的模式如Scale With Screen Size这样UI元素能按比例缩放在不同分辨率下保持相对位置和大小。我个人在实际项目中的心得是不要把FPS显示器当作一个孤立的调试工具。最好将它集成到你的游戏管理或调试系统中可以通过快捷键如按F3来开关显示并且可以扩展显示更多信息如内存使用量、Draw Call数量、当前场景名称等。这样它就能成为一个强大的、随时可用的运行时诊断面板。另外对于线上版本可以考虑做一个轻量级的、可远程开启的性能数据采样功能这样在收到玩家关于卡顿的反馈时可以请他们开启这个功能并上传数据帮助你更精准地定位问题。