如何实现 FPS 计数器?多种方法大揭秘!
突发探讨如何实现 FPS 计数器文章最初发布于 2025 年 8 月 17 日更新于 2025 年 9 月 17 日。文中提到简而言之不要基于特定数量的帧数来计算 FPS而应维护一个记录最近一秒内发生的帧数的滚动窗口并使用精确的计时器。作者曾在关于 2025 年 GMTK 游戏开发马拉松的文章中提及此话题现在想详细探讨。需求分析假设要在游戏中显示 FPS 计数器这在很多游戏中常见。首先要思考希望这个数据告知什么是想了解游戏性能以及在最近一段时间内游戏能否达到 30 或 60 FPS 的目标。当然也会想是否可直接测量处理每一帧所需的时间因为对于 60 FPS 的游戏每一帧需在 16.67 毫秒内完成准备和渲染若能持续保持在这个时间内就没问题。不过FPS 是通常展示给玩家的指标并且在游戏行业中被广泛用作衡量性能的标准。所以想知道游戏生成新帧的速度但会以 FPS 这个指标来展示那么具体该如何做呢错误的方法方法 1基于最新帧计算 FPS伪代码如下float fps 0;Time prev Time::current();while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); fps Duration::seconds(1) / (curr - prev); prev curr;}你可能不同意把计算放在渲染调用之后若愿意可把它放在前面。作者更喜欢这种方式因为是在循环的边界处进行每次测量。从帧处理时间的角度看这个方法告诉我们生成最新一帧所花费的时间这可能是一个有用的信息。但如果关心每一帧的处理时间为何不把它们都记录到文件中以便后续分析呢如果某一帧处理得非常快或非常慢FPS 计数器只会在这一帧显示异常然后又恢复正常很可能注意不到。FPS 从名称上看就是一个综合指标所以应该对多帧进行综合计算。方法 2基于最近 N 帧计算 FPS这种方法会跟踪最近几帧例如 5 或 10 帧的处理时间并根据滚动平均值显示 FPS。伪代码示例如下const int windowFrames ...;float fps 0;Queue processingTimes;Time prev Time::current();while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); if (processingTimes.size() windowFrames) processingTimes.pop(); processingTimes.push(curr - prev); fps Duration::seconds(1) / averageVal(processingTimes); prev curr;}我们希望根据最近的性能历史来测量 FPS但这段历史的时间长度取决于帧的生成速度即测量的历史长度取决于被测量的值。想象一下 FPS 图表x 轴表示时间y 轴表示 FPS这将是一个容易产生误导的图表因为 y 轴上的每个值都依赖于自身因为它的值会影响其在 x 轴上回溯的距离。这里有一个示例其中 3 帧比平时慢红色3 帧比平时快绿色窗口大小为 5 帧。注意与处理速度快的帧相比处理速度慢的帧对应的图表要平滑得多这个图表在显示的时间段内是不一致的这就是为什么历史记录需要有一个固定的持续时间。可行的方法方法 3基于每秒帧数每秒重置一次网上可能会找到的另一种测量 FPS 的方法伪代码如下float fps 0;int frames 0;Time prev Time::current();while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); if (prev Duration::seconds(1) curr) { fps frames; frames 0; while (prev Duration::seconds(1) curr) { prev Duration::seconds(1); } } frames;}作者也见过一些示例在 if 语句块中fps 会根据 frames 的连续值进行平滑处理。这个方法显示了每秒渲染的帧数但每秒只更新一次这在很大程度上符合我们想要展示的内容。一方面可能希望限制 UI 中 FPS 显示的更新频率以便于阅读因为它不会每一帧都显示不同的数字另一方面可能会觉得每秒更新一次间隔太长了。正确的方法在了解几种实现方法后来看看如何做得更好。首先谈谈实时监控对于来自 Web 开发领域的人来说应该很熟悉。假设你有一个服务或应用程序在执行某些任务想监控它的运行情况一个常见的例子是测量用户 HTTP 请求的数量。可以创建一个图表来显示当前正在处理的请求数量但这个值会剧烈波动图表会很难阅读或者如果服务不是持续接收请求很多时候值会一直为零。相反可以通过查看一个时间窗口而不是单个时间点来平滑波动图表上的每个点将是一个函数如平均值、计数或最大值应用于该时间点结束的时间窗口内所有记录事件的结果。以 HTTP 请求图表为例x 轴表示时间y 轴表示请求数量每个点的 y 轴值将是在该点 x 轴值对应的最后一个“时间窗口”内记录的事件即到达服务器的 HTTP 请求的计数。以下是一个这样的图表示例灰色圆点表示请求发生的时间点。当收到请求时图表上升并保持在高位直到自该请求以来经过了“窗口”长度的时间。选择窗口长度是一种权衡较短的窗口更适合跟踪快速变化而较长的窗口则更适合显示长期趋势。方法 4基于滚动窗口内的帧数计算 FPS了解以上内容后代码其实很简单const Duration window ...;float fps 0;QueueframeTimestamps;while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); frameTimestamps.push(curr); while (frameTimestamps.next() window curr) frameTimestamps.pop(); fps frameTimestamps.size() * Duration::seconds(1) / window;}一个队列存储最近帧完成的时间戳每次更新时所有早于一个窗口时间的帧都会被丢弃。如果愿意可以引入另一个 Time 变量来限制 FPS 显示的更新频率以便于阅读显示更新的频率不需要与滚动窗口的长度相对应。这里的实现方式意味着在第一个窗口期间报告的 FPS 会比较小随着队列收集时间戳FPS 会逐渐增加。如果你愿意可以修复这个问题但就作者个人而言不介意前一秒的数据有点偏差。方法 5基于滚动窗口内帧的处理时间计算 FPS前一种方法不错但可以做一个小改进struct FrameEvent { Time timestamp; Duration processingTime;};// ...const Duration window ...;float fps 0;Queue frameEvents;Time prev Time::current();while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); frameEvents.push(FrameEvent{curr, curr - prev}); while (frameEvents.next().timestamp window curr) frameEvents.pop(); fps Duration::seconds(1) / averageProcessingTime(frameEvents); prev curr;}在这里我们跟踪滚动窗口内每一帧的处理时间。对于 FPS我们首先计算平均处理时间然后根据这个时间计算 FPS 值。这很好因为可以在内部使用它来显示平均帧处理时间而不是 FPS而且它可以很容易地扩展以跟踪窗口内最慢的帧或处理时间的标准差等。最后的注意事项使用具有足够精度的计时器非常重要。如果你使用的是 SDL建议使用 SDL_GetPerformanceCounter() 和 SDL_GetPerformanceFrequency()如果你不使用 SDL但使用 Cstd::chrono::high_resolution_clock 应该是一个不错的选择。如果你无法使用队列实现或者不想让内存分配器偶尔触发你可以实现一个容量有限的循环缓冲区。要注意缓冲区满的边界情况你可以给它足够大的容量以确保在游戏中不会达到上限。如果缓冲区满了移除最旧的帧并添加最新的帧。方法 5 的实现将根据缓冲区中帧的处理时间计算 FPS。时间窗口可能比配置的短但作为 FPS 估计仍然是正确的。额外方法基于每秒帧数每秒重置两次发布这篇文章一段时间后作者想到了一种改进方法 3 的方式即让 FPS 显示每秒更新多次例如每秒更新两次。伪代码如下Uint64 horizon Time::current();size_t cntPrev0 0, cntPrev1 0, cntNext 0;while (true) { // 处理输入 // 更新游戏状态 // 渲染游戏并在 UI 中显示 FPS Time curr Time::current(); while (horizon Duration::seconds(0.5) curr) { horizon Duration::seconds(0.5); cntPrev0 cntPrev1; cntPrev1 cntNext; cntNext 0; } cntNext; // 要显示的 FPS 是 cntPrev0 cntPrev1}这个方法的思路是跟踪帧数但将其分为三个计数器每个计数器记录半秒的帧数。我们在 UI 中显示的是前两个“较旧”计数器记录的帧数。随着新帧的渲染我们增加第三个即“最新”计数器。每半秒我们将计数器向左移动并重置第三个计数器。以下是某个时间点的 ASCII 图示 cntPrev0 cntPrev1 cntNext | | | V V V[ 0.5s ][ 0.5s ][ 0.5s ]------------------------|------ ^ ^ | | horizon | present当“当前时间”比“水平线”向前移动超过半秒时我们向左移动 cntPrev0 cntPrev1 cntNext | | | V V V [ 0.5s ][ 0.5s ][ 0.5s ]----------------------------------|-- ^ ^ | | horizon | present当你恰好需要这种方式并且不打算做太多更改时这种方法很合适它比使用队列的方法消耗的内存更少。