[英]Calculating frames per second in a game
在游戏中计算每秒帧数的好算法是什么? 我想将它显示为屏幕一角的数字。 如果我只看渲染最后一帧需要多长时间,数字变化太快。
如果您的答案更新每一帧并且在帧速率增加与减少时不会以不同方式收敛,则会获得奖励积分。
您需要一个平滑的平均值,最简单的方法是获取当前答案(绘制最后一帧的时间)并将其与之前的答案结合起来。
// eg.
float smoothing = 0.9; // larger=more smoothing
measurement = (measurement * smoothing) + (current * (1.0-smoothing))
通过调整 0.9 / 0.1 比率,您可以更改“时间常数”——即数字对变化的响应速度。 支持旧答案的较大部分给出较慢的平滑变化,支持新答案的较大部分给出更快的变化值。 显然这两个因素必须相加!
这是我在很多游戏中使用的。
#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);
}
嗯,当然
frames / sec = 1 / (sec / frame)
但是,正如您所指出的,渲染单个帧所需的时间有很多变化,并且从 UI 的角度来看,以帧速率更新 fps 值根本不可用(除非数字非常稳定)。
您想要的可能是移动平均线或某种分箱/重置计数器。
例如,您可以维护一个队列数据结构,该结构保存最后 30、60、100 帧或其他帧中的每一帧的渲染时间(您甚至可以对其进行设计,以便在运行时调整限制)。 要确定合适的 fps 近似值,您可以从队列中的所有渲染时间确定平均 fps:
fps = # of rendering times in queue / total rendering time
当您完成渲染一个新帧时,您将一个新的渲染时间排入队列并将旧的渲染时间排入队列。 或者,您可以仅在渲染时间总和超过某个预设值(例如 1 秒)时出列。 您可以维护“最后 fps 值”和最后更新的时间戳,以便您可以触发何时更新 fps 数字,如果您愿意的话。 尽管如果您具有一致的格式,则使用移动平均值,但在每帧上打印“瞬时平均值” fps 可能就可以了。
另一种方法是有一个重置计数器。 维护一个精确(毫秒)的时间戳、一个帧计数器和一个 fps 值。 完成渲染帧后,递增计数器。 当计数器达到预设限制(例如 100 帧)或自时间戳以来的时间已超过某个预设值(例如 1 秒)时,计算 fps:
fps = # frames / (current time - start time)
然后将计数器重置为 0,并将时间戳设置为当前时间。
每次渲染屏幕时增加一个计数器,并在要测量帧速率的某个时间间隔内清除该计数器。
IE。 每 3 秒,获取 counter/3,然后清除计数器。
至少有两种方法可以做到:
第一个是其他人在我之前提到的。 我认为这是最简单和首选的方式。 你只是为了跟踪
在这种情况下计算 fps 就像评估这个公式一样简单:
然后是你有一天可能会喜欢使用的超级酷的方式:
假设您要考虑“i”帧。 我将使用这个符号:f[0], f[1],..., f[i-1] 来描述渲染第 0 帧、第 1 帧、...、帧 (i-1 ) 分别。
Example where i = 3
|f[0] |f[1] |f[2] |
+----------+-------------+-------+------> time
然后,在 i 帧之后 fps 的数学定义将是
(1) fps[i] = i / (f[0] + ... + f[i-1])
和相同的公式,但只考虑 i-1 帧。
(2) fps[i-1] = (i-1) / (f[0] + ... + f[i-2])
现在这里的技巧是修改公式(1)的右侧,使其包含公式(2)的右侧并将其替换为左侧。
像这样(如果你把它写在纸上,你应该看得更清楚):
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,渲染最后一帧所花费的持续时间以及你的帧数呈现。
这对大多数人来说可能是矫枉过正,这就是为什么我在实现它时没有发布它。 但它非常健壮和灵活。
它存储一个包含最后一帧时间的队列,因此它可以准确地计算平均 FPS 值,这比仅考虑最后一帧要好得多。
它还允许您忽略一帧,如果您正在做一些您知道会人为地破坏该帧时间的事情。
它还允许您在队列运行时更改要存储在队列中的帧数,因此您可以即时测试它对您来说最有价值。
// 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);
}
这里有很好的答案。 您如何实现它取决于您需要它的用途。 我更喜欢上面那个人自己的运行平均值“时间=时间* 0.9 + last_frame * 0.1”。
但是我个人更喜欢将我的平均值更多地放在更新的数据上,因为在游戏中,最难压扁的是 SPIKES,因此我最感兴趣。 所以我会使用更像 .7 \ .3 分割的东西,这会使尖峰出现得更快(尽管它的效果也会更快地从屏幕上消失..见下文)
如果您的重点是渲染时间,那么 .9.1 分割效果非常好,b/c 它往往更平滑。 尽管对于游戏玩法/AI/物理峰值更令人担忧,因为这通常会使您的游戏看起来不稳定(假设我们没有低于 20 fps,这通常比低帧速率更糟糕)
所以,我要做的也是添加这样的东西:
#define ONE_OVER_FPS (1.0f/60.0f)
static float g_SpikeGuardBreakpoint = 3.0f * ONE_OVER_FPS;
if(time > g_SpikeGuardBreakpoint)
DoInternalBreakpoint()
(在 3.0f 中填写您认为是不可接受的峰值的任何幅度)这将让您找到并解决帧结束时发生的 FPS 问题。
比使用大量旧帧速率更好的系统是执行以下操作:
new_fps = old_fps * 0.99 + new_fps * 0.01
这种方法使用更少的内存,需要更少的代码,并且比旧的帧率更重视最近的帧率,同时仍然平滑突然的帧率变化的影响。
// 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)
您可以保留一个计数器,在渲染每一帧后将其递增,然后在新的一秒时重置计数器(将前一个值存储为上一秒渲染的帧数)
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)”。
这是我的做法(在 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
}
在 Typescript 中,我使用此算法来计算帧速率和帧时间平均值:
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';
提示:如果 samples 为 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 的回答,将全局变量移到结构字段中,允许可配置样本数量,并使用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,但总是涉及两种堆分配:一种用于结构本身,另一种用于支持样本切片的数组。 如果如上所述使用,这些是长期分配,因此它们不会真正对垃圾收集器征税。 优化前的配置文件,一如既往。
但是,如果性能是一个主要问题,可以进行一些更改以消除分配和间接:
[]time.Duration
切片更改为[N]time.Duration
数组,其中N
在编译时固定。 这消除了在运行时更改样本数量的灵活性,但在大多数情况下,这种灵活性是不必要的。NewFrameTimeTracker
构造函数并使用var frameTimeTracker FrameTimeTracker
声明(在包级别或main
本地)。 与 C 不同,Go 会将所有相关内存预归零。不幸的是,这里的大多数答案都没有提供足够准确或足够“响应缓慢”的 FPS 测量。 下面是我在 Rust 中使用测量队列的方法:
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,
}
}
}
如何使用:
let mut fps_counter = FpsCounter::new(Duration::from_millis(100), 5);
fps_counter.update()
。fps_counter.fps()
。 现在,关键在于FpsCounter::new()
方法的参数: sample_period
是fps()
对帧率变化的响应程度,而samples
控制fps()
上升或下降到实际帧率的速度。 因此,如果您选择 10 毫秒和 100 个样本, fps()
几乎会立即对帧速率的任何变化做出反应 - 基本上,屏幕上的 FPS 值会像疯了一样抖动,但由于它是 100 个样本,因此需要 1 秒才能匹配实际帧率。
所以我选择 100 毫秒和 5 个样本意味着显示的 FPS 计数器不会因为快速变化而让你的眼睛流血,它会在变化后半秒与你的实际帧速率匹配,这对于游戏来说已经足够明智了。
由于sample_period * samples
是平均时间跨度,如果您想要一个相当准确的 FPS 计数器,您不希望它太短。
将计数器设置为零。 每次绘制帧时,计数器都会增加。 每秒钟打印一次计数器。 起泡,冲洗,重复。 如果您想要额外的积分,请保留一个运行计数器并除以总秒数以获得运行平均值。
在(c++ 之类的)伪代码中,这两个是我在工业图像处理应用程序中使用的,它必须处理来自一组外部触发相机的图像。 “帧速率”的变化有不同的来源(传送带上的生产速度较慢或较快),但问题是相同的。 (我假设您有一个简单的 timer.peek() 调用,它为您提供了自应用程序启动或上次调用以来的毫秒数(nsec?)之类的东西)
解决方案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
}
}
存储开始时间并在每个循环中增加一次帧计数器? 每隔几秒钟你就可以打印 framecount/(Now - starttime) 然后重新初始化它们。
编辑:哎呀。 双忍
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.