计算游戏中的帧率

在游戏中计算帧率的好算法是什么?我想把它显示为屏幕角落里的一个数字。如果我只是看它花了多长时间渲染的最后一帧数字变化太快。

如果你的答案更新了每一帧,并且当帧速率增加或者减少时不会有不同的收敛,那么这就是额外的加分。

123745 次浏览

你需要一个平滑的平均值,最简单的方法是取当前的答案(画最后一帧的时间) ,然后将它与之前的答案结合起来。

// eg.
float smoothing = 0.9; // larger=more smoothing
measurement = (measurement * smoothing) + (current * (1.0-smoothing))

通过调整0.9/0.1的比率,你可以改变“时间常数”——这就是数字对变化的响应速度。支持旧答案的较大一部分人给出的变化速度较慢,支持新答案的较大一部分人给出的变化速度较快。显然,这两个因素必须加在一起!

每次呈现屏幕时增加一个计数器,并在一段时间间隔内清除该计数器,以便测量帧速率。

每3秒钟,得到计数器/3,然后清除计数器。

设置为零。每次绘制帧时计数器递增。每秒钟打印一次计数器。泡沫,冲洗,重复。如果你想要额外的学分,保留一个跑步计数器,除以跑步平均时间的总秒数。

您可以保留一个计数器,在每个帧呈现后递增它,然后在新的秒上重置计数器(将前一个值存储为最后一秒呈现的帧的 #)

存储一个开始时间,并增加你的帧计数器一次循环?每隔几秒你就可以打印 frameccount/(Now-starttime)然后重新初始化它们。

编辑: 哎呀,双忍者

当然

frames / sec = 1 / (sec / frame)

But, as you point out, there's a lot of variation in the time it takes to render a single frame, and from a UI perspective updating the fps value at the frame rate is not usable at all (unless the number is very stable).

What you want is probably a moving average or some sort of binning / resetting counter.

例如,您可以维护一个队列数据结构,该结构可以保存最后30、60、100帧或随便什么帧的每一帧的呈现时间(您甚至可以将其设计为在运行时可以调整限制)。要确定一个合适的 fps 近似值,可以从队列中所有呈现时间中确定平均 fps:

fps = # of rendering times in queue / total rendering time

When you finish rendering a new frame you enqueue a new rendering time and dequeue an old rendering time. Alternately, you could dequeue only when the total of the rendering times exceeded some preset value (e.g. 1 sec). You can maintain the "last fps value" and a last updated timestamp so you can trigger when to update the fps figure, if you so desire. Though with a moving average if you have consistent formatting, printing the "instantaneous average" fps on each frame would probably be ok.

另一种方法是使用复位计数器。维护精确的(毫秒)时间戳、帧计数器和 fps 值。当您完成呈现一个框架时,增加计数器。当计数器达到预先设定的限制(例如100帧)或时间戳已经超过某个预先设定的值(例如1秒)时,计算 fps:

fps = # frames / (current time - start time)

然后将计数器重置为0,并将时间戳设置为当前时间。

这是我在很多游戏中使用过的。

#define MAXSAMPLES 100
int tickindex=0;
int ticksum=0;
int ticklist[MAXSAMPLES];


/* need to zero out the ticklist array before starting */
/* average will ramp up until the buffer is full */
/* returns average ticks per frame over the MAXSAMPLES last frames */


double CalcAverageTick(int newtick)
{
ticksum-=ticklist[tickindex];  /* subtract value falling off */
ticksum+=newtick;              /* add new value */
ticklist[tickindex]=newtick;   /* save new value so it can be subtracted later */
if(++tickindex==MAXSAMPLES)    /* inc buffer index */
tickindex=0;


/* return average */
return((double)ticksum/MAXSAMPLES);
}

在(c + + like)伪代码中,这两个是我在工业图像处理应用程序中使用的,这些应用程序必须处理来自一组外部触发相机的图像。在“帧速率”的变化有一个不同的来源(慢或更快的生产带) ,但问题是相同的。(我假设您有一个简单的 timer.pek()调用,它提供类似 msec (nsec?)的 nr自应用程序启动或最后一次调用以来)

解决方案1: 快速但不更新每帧

do while (1)
{
ProcessImage(frame)
if (frame.framenumber%poll_interval==0)
{
new_time=timer.peek()
framerate=poll_interval/(new_time - last_time)
last_time=new_time
}
}

解决方案2: 每帧更新一次,需要更多的内存和 CPU

do while (1)
{
ProcessImage(frame)
new_time=timer.peek()
delta=new_time - last_time
last_time = new_time
total_time += delta
delta_history.push(delta)
framerate= delta_history.length() / total_time
while (delta_history.length() > avg_interval)
{
oldest_delta = delta_history.pop()
total_time -= oldest_delta
}
}

答得好。您如何实现它取决于您需要它的目的。我更喜欢上面那个家伙的“ time = time * 0.9 + last _ frame * 0.1”。

然而,我个人更喜欢将我的平均值加权到更新的数据上,因为在游戏中,最难压制的是 SPIKES,因此也是我最感兴趣的。所以我会使用一些更像.7.3分割将使一个尖峰显示得更快(虽然它的效果将下降到屏幕以外的速度也更快。.见下文)

如果你的注意力集中在渲染时间上,那么.9.1的分割工作得相当不错,它往往更加平滑。尽管对于游戏性/人工智能/物理学峰值来说更值得关注,因为那通常会让你的游戏看起来波涛汹涌(这通常比低帧率更糟糕,假设我们没有下降到20帧/秒以下)

那么,我要做的就是加上这样的东西:

#define ONE_OVER_FPS (1.0f/60.0f)
static float g_SpikeGuardBreakpoint = 3.0f * ONE_OVER_FPS;
if(time > g_SpikeGuardBreakpoint)
DoInternalBreakpoint()

(填写3.0 f,无论你认为是一个不可接受的峰值) 这将让你找到,因此 解决 FPS 发布的帧结束他们发生。

至少有两种方法:


第一个是其他人在我之前提到过的。 我认为这是最简单和首选的方式。你只需要跟踪

  • Cn: 计算你渲染了多少帧
  • Time _ start: 自从开始计时以来的时间
  • Time _ now: 当前时间

在这种情况下,计算 fps 就像计算这个公式一样简单:

  • FPS = cn / (time_now - time_start).

Then there is the uber cool way you might like to use some day:

让我们假设您有“ i”框架要考虑。我将使用这个符号: f [0] ,f [1] ,... ,f [ i-1]来描述分别渲染帧0、帧1、 ... 和帧(i-1)所需的时间。

Example where i = 3


|f[0]      |f[1]         |f[2]   |
+----------+-------------+-------+------> time

然后,数学定义的 fps 后 i 帧将

(1) fps[i]   = i     / (f[0] + ... + f[i-1])

And the same formula but only considering i-1 frames.

(2) fps[i-1] = (i-1) / (f[0] + ... + f[i-2])

这里的诀窍是修改公式(1)的右边,使其包含公式(2)的右边,并将其替换为公式(2)的左边。

Like so (you should see it more clearly if you write it on a paper):

fps[i] = i / (f[0] + ... + f[i-1])
= i / ((f[0] + ... + f[i-2]) + f[i-1])
= (i/(i-1)) / ((f[0] + ... + f[i-2])/(i-1) + f[i-1]/(i-1))
= (i/(i-1)) / (1/fps[i-1] + f[i-1]/(i-1))
= ...
= (i*fps[i-1]) / (f[i-1] * fps[i-1] + i - 1)

所以根据这个公式(虽然我的数学推导技巧有点生疏) ,要计算新的 fps,你需要知道前一帧的 fps,渲染最后一帧所花的时间和渲染的帧数。

qx.Class.define('FpsCounter', {
extend: qx.core.Object


,properties: {
}


,events: {
}


,construct: function(){
this.base(arguments);
this.restart();
}


,statics: {
}


,members: {
restart: function(){
this.__frames = [];
}






,addFrame: function(){
this.__frames.push(new Date());
}






,getFps: function(averageFrames){
debugger;
if(!averageFrames){
averageFrames = 2;
}
var time = 0;
var l = this.__frames.length;
var i = averageFrames;
while(i > 0){
if(l - i - 1 >= 0){
time += this.__frames[l - i] - this.__frames[l - i - 1];
}
i--;
}
var fps = averageFrames / time * 1000;
return fps;
}
}


});

我是怎么做到的!

boolean run = false;


int ticks = 0;


long tickstart;


int fps;


public void loop()
{
if(this.ticks==0)
{
this.tickstart = System.currentTimeMillis();
}
this.ticks++;
this.fps = (int)this.ticks / (System.currentTimeMillis()-this.tickstart);
}

换句话说,滴答的时钟跟踪滴答的声音。如果这是第一次,它将获取当前时间并将其放入“ tickstart”中。在第一次滴答之后,它使变量‘ fps’等于滴答时钟的滴答次数除以第一次滴答的时间。

Fps 是一个整数,因此是“(int)”。

Here's how I do it (in Java):

private static long ONE_SECOND = 1000000L * 1000L; //1 second is 1000ms which is 1000000ns


LinkedList<Long> frames = new LinkedList<>(); //List of frames within 1 second


public int calcFPS(){
long time = System.nanoTime(); //Current time in nano seconds
frames.add(time); //Add this frame to the list
while(true){
long f = frames.getFirst(); //Look at the first element in frames
if(time - f > ONE_SECOND){ //If it was more than 1 second ago
frames.remove(); //Remove it from the list of frames
} else break;
/*If it was within 1 second we know that all other frames in the list
* are also within 1 second
*/
}
return frames.size(); //Return the size of the list
}

This might be overkill for most people, that's why I hadn't posted it when I implemented it. But it's very robust and flexible.

它存储一个具有最后一帧时间的 Queue,因此它可以精确地计算平均 FPS 值,这比仅仅考虑最后一帧要好得多。

It also allows you to ignore one frame, if you are doing something that you know is going to artificially screw up that frame's time.

它还允许您在 Queue 运行时更改存储在 Queue 中的帧的数量,这样您就可以动态地测试对您来说什么是最佳值。

// Number of past frames to use for FPS smooth calculation - because
// Unity's smoothedDeltaTime, well - it kinda sucks
private int frameTimesSize = 60;
// A Queue is the perfect data structure for the smoothed FPS task;
// new values in, old values out
private Queue<float> frameTimes;
// Not really needed, but used for faster updating then processing
// the entire queue every frame
private float __frameTimesSum = 0;
// Flag to ignore the next frame when performing a heavy one-time operation
// (like changing resolution)
private bool _fpsIgnoreNextFrame = false;


//=============================================================================
// Call this after doing a heavy operation that will screw up with FPS calculation
void FPSIgnoreNextFrame() {
this._fpsIgnoreNextFrame = true;
}


//=============================================================================
// Smoothed FPS counter updating
void Update()
{
if (this._fpsIgnoreNextFrame) {
this._fpsIgnoreNextFrame = false;
return;
}


// While looping here allows the frameTimesSize member to be changed dinamically
while (this.frameTimes.Count >= this.frameTimesSize) {
this.__frameTimesSum -= this.frameTimes.Dequeue();
}
while (this.frameTimes.Count < this.frameTimesSize) {
this.__frameTimesSum += Time.deltaTime;
this.frameTimes.Enqueue(Time.deltaTime);
}
}


//=============================================================================
// Public function to get smoothed FPS values
public int GetSmoothedFPS() {
return (int)(this.frameTimesSize / this.__frameTimesSum * Time.timeScale);
}

一个比使用大量旧帧率更好的系统是这样做:

new_fps = old_fps * 0.99 + new_fps * 0.01

这种方法使用更少的内存,需要更少的代码,更重视最近的帧率比旧帧率,同时仍然平滑的影响突然帧率变化。

JavaScript:

// Set the end and start times
var start = (new Date).getTime(), end, FPS;
/* ...
* the loop/block your want to watch
* ...
*/
end = (new Date).getTime();
// since the times are by millisecond, use 1000 (1000ms = 1s)
// then multiply the result by (MaxFPS / 1000)
// FPS = (1000 - (end - start)) * (MaxFPS / 1000)
FPS = Math.round((1000 - (end - start)) * (60 / 1000));

下面是一个完整的示例,使用 Python (但很容易适应任何语言)。它在 Martin 的回答中使用了平滑方程,因此几乎没有内存开销,而且我选择了适合我的值(可以随意使用常量来适应您的用例)。

import time


SMOOTHING_FACTOR = 0.99
MAX_FPS = 10000
avg_fps = -1
last_tick = time.time()


while True:
# <Do your rendering work here...>


current_tick = time.time()
# Ensure we don't get crazy large frame rates, by capping to MAX_FPS
current_fps = 1.0 / max(current_tick - last_tick, 1.0/MAX_FPS)
last_tick = current_tick
if avg_fps < 0:
avg_fps = current_fps
else:
avg_fps = (avg_fps * SMOOTHING_FACTOR) + (current_fps * (1-SMOOTHING_FACTOR))
print(avg_fps)

在 Type Script 中,我使用这个算法来计算帧率和帧时间平均值:

let getTime = () => {
return new Date().getTime();
}


let frames: any[] = [];
let previousTime = getTime();
let framerate:number = 0;
let frametime:number = 0;


let updateStats = (samples:number=60) => {
samples = Math.max(samples, 1) >> 0;


if (frames.length === samples) {
let currentTime: number = getTime() - previousTime;


frametime = currentTime / samples;
framerate = 1000 * samples / currentTime;


previousTime = getTime();


frames = [];
}


frames.push(1);
}

用途:

statsUpdate();


// Print
stats.innerHTML = Math.round(framerate) + ' FPS ' + frametime.toFixed(2) + ' ms';

提示: 如果样本为1,结果是实时帧率和帧时间。

这是基于 KPexEA 的答案,并给出了简单移动平均线。整理并转换为 TypeScript 以便于复制和粘贴:

变量声明:

fpsObject = {
maxSamples: 100,
tickIndex: 0,
tickSum: 0,
tickList: []
}

功能:

calculateFps(currentFps: number): number {
this.fpsObject.tickSum -= this.fpsObject.tickList[this.fpsObject.tickIndex] || 0
this.fpsObject.tickSum += currentFps
this.fpsObject.tickList[this.fpsObject.tickIndex] = currentFps
if (++this.fpsObject.tickIndex === this.fpsObject.maxSamples) this.fpsObject.tickIndex = 0
const smoothedFps = this.fpsObject.tickSum / this.fpsObject.maxSamples
return Math.floor(smoothedFps)
}

用法(可能因应用程序的不同而有所不同) :

this.fps = this.calculateFps(this.ticker.FPS)

我将@KPexEA 的答案改编为 Go,将全局变量移动到 struct 字段中,允许可配置示例的数量,并使用 time.Duration代替普通的整数和浮点数。

type FrameTimeTracker struct {
samples []time.Duration
sum     time.Duration
index   int
}


func NewFrameTimeTracker(n int) *FrameTimeTracker {
return &FrameTimeTracker{
samples: make([]time.Duration, n),
}
}


func (t *FrameTimeTracker) AddFrameTime(frameTime time.Duration) (average time.Duration) {
// algorithm adapted from https://stackoverflow.com/a/87732/814422
t.sum -= t.samples[t.index]
t.sum += frameTime
t.samples[t.index] = frameTime
t.index++
if t.index == len(t.samples) {
t.index = 0
}
return t.sum / time.Duration(len(t.samples))
}

使用具有纳秒级精度的 time.Duration,消除了计算平均帧时间所需的浮点运算,但是代价是需要为相同数量的样本提供两倍的内存。

你可以这样使用它:

// track the last 60 frame times
frameTimeTracker := NewFrameTimeTracker(60)


// main game loop
for frame := 0;; frame++ {
// ...
if frame > 0 {
// prevFrameTime is the duration of the last frame
avgFrameTime := frameTimeTracker.AddFrameTime(prevFrameTime)
fps := 1.0 / avgFrameTime.Seconds()
}
// ...


}

由于这个问题的上下文是游戏编程,我将添加一些关于性能和优化的注释。上面的方法是惯用的 Go,但总是涉及两个堆分配: 一个用于结构本身,另一个用于支持样本片的数组。如果像上面指出的那样使用,这些分配是长期的,因此它们不会真正对垃圾收集器造成负担。优化之前的配置文件,一如既往。

但是,如果业绩是一个主要问题,可以作出一些改变,以消除拨款和间接费用:

  • Change samples from a slice of []time.Duration to an array of [N]time.Duration where N is fixed at compile time. This removes the flexibility of changing the number of samples at runtime, but in most cases that flexibility is unnecessary.
  • 然后,完全消除 NewFrameTimeTracker构造函数,使用 var frameTimeTracker FrameTimeTracker声明(在包级别或 main的本地)。与 C 语言不同,Go 将预先调零所有相关内存。

Unfortunately, most of the answers here don't provide either accurate enough or sufficiently "slow responsive" FPS measurements. Here's how I do it in Rust using a measurement queue:

use std::collections::VecDeque;
use std::time::{Duration, Instant};


pub struct FpsCounter {
sample_period: Duration,
max_samples: usize,
creation_time: Instant,
frame_count: usize,
measurements: VecDeque<FrameCountMeasurement>,
}


#[derive(Copy, Clone)]
struct FrameCountMeasurement {
time: Instant,
frame_count: usize,
}


impl FpsCounter {
pub fn new(sample_period: Duration, samples: usize) -> Self {
assert!(samples > 1);


Self {
sample_period,
max_samples: samples,
creation_time: Instant::now(),
frame_count: 0,
measurements: VecDeque::new(),
}
}


pub fn fps(&self) -> f32 {
match (self.measurements.front(), self.measurements.back()) {
(Some(start), Some(end)) => {
let period = (end.time - start.time).as_secs_f32();
if period > 0.0 {
(end.frame_count - start.frame_count) as f32 / period
} else {
0.0
}
}


_ => 0.0,
}
}


pub fn update(&mut self) {
self.frame_count += 1;


let current_measurement = self.measure();
let last_measurement = self
.measurements
.back()
.copied()
.unwrap_or(FrameCountMeasurement {
time: self.creation_time,
frame_count: 0,
});
if (current_measurement.time - last_measurement.time) >= self.sample_period {
self.measurements.push_back(current_measurement);
while self.measurements.len() > self.max_samples {
self.measurements.pop_front();
}
}
}


fn measure(&self) -> FrameCountMeasurement {
FrameCountMeasurement {
time: Instant::now(),
frame_count: self.frame_count,
}
}
}

使用方法:

  1. 创建计数器: let mut fps_counter = FpsCounter::new(Duration::from_millis(100), 5);
  2. 对每个画面调用 fps_counter.update()
  3. 只要您想显示当前的 FPS,就可以调用 fps_counter.fps()

现在,关键在于 FpsCounter::new()方法的参数: sample_periodfps()对帧率变化的响应速度,而 samples控制 fps()上升或下降到实际帧率的速度。因此,如果你选择10毫秒和100个样本,fps()几乎会立即对帧率的任何变化作出反应-基本上,屏幕上的 FPS 值会抖动得像疯了一样,但因为它是100个样本,它会花1秒钟来匹配实际的帧率。

所以我选择的100毫秒和5个样品意味着显示的 FPS 计数器不会让你的眼睛流血疯狂快速变化,它会匹配你的实际帧率半秒后,它的变化,这是足够明智的游戏。

由于 sample_period * samples是平均时间跨度,你不希望它太短,如果你想要一个合理准确的 FPS 计数器。