如何实现 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;Queue

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,但使用 C++,`std::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```

当你恰好需要这种方式,并且不打算做太多更改时,这种方法很合适,它比使用队列的方法消耗的内存更少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值