Расчет кадров в секунду в игре
Какой хороший алгоритм для расчета кадров в секунду в игре? Я хочу показать это как число в углу экрана. Если я просто посмотрю, сколько времени потребовалось для рендеринга последнего кадра, число меняется слишком быстро.
Бонусные баллы, если ваш ответ обновляет каждый кадр и не сходится по-разному, когда частота кадров увеличивается или уменьшается.
21 ответ
Вам нужно сглаженное среднее, самый простой способ - взять текущий ответ (время рисования последнего кадра) и объединить его с предыдущим ответом.
// 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)
Но, как вы указали, существует много различий во времени, необходимом для визуализации одного кадра, и с точки зрения пользовательского интерфейса обновление значения 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 и установите метку времени на текущее время.
Увеличивайте счетчик каждый раз при рендеринге экрана и очищайте его в течение некоторого интервала времени, в течение которого вы хотите измерить частоту кадров.
То есть. Каждые 3 секунды получите счетчик /3 и затем очистите счетчик.
Есть как минимум два способа сделать это:
Первое - это то, что другие упоминали здесь до меня. Я думаю, что это самый простой и предпочтительный способ. Вы просто следить за
- cn: счетчик того, сколько кадров вы рендерили
- time_start: время с начала подсчета
- time_now: текущее время
Вычисление fps в этом случае так же просто, как вычисление этой формулы:
- FPS = cn / (time_now - time_start).
Тогда есть очень крутой способ, которым вы хотели бы использовать когда-нибудь:
Допустим, у вас есть кадры '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])
И та же формула, но только с учетом 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 + последний_фрейм * 0,1" парнем выше.
Тем не менее, мне лично нравится более сильно взвешивать свои средние значения в сторону новых данных, потому что в игре спайки труднее всего раздавить и поэтому представляют для меня наибольший интерес. Так что я бы использовал что-то более похожее на разделение.7 \ .3, чтобы спайк проявился гораздо быстрее (хотя его эффект также исчезнет за пределами экрана.. см. Ниже)
Если ваше внимание сосредоточено на времени RENDERING, то разделение.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.0f с любой величиной, которую вы считаете недопустимым всплеском) Это позволит вам найти и, таким образом, решить проблемы FPS в конце кадра, в котором они происходят.
Гораздо лучшая система, чем использование большого массива старых кадров, состоит в следующем:
new_fps = old_fps * 0.99 + new_fps * 0.01
Этот метод использует намного меньше памяти, требует гораздо меньше кода и придает большее значение недавним частотам кадров, чем старым, в то же время сглаживая последствия внезапных изменений частоты кадров.
Вот полный пример использования Python (но его легко адаптировать к любому языку). Он использует уравнение сглаживания в ответе Мартина, так что почти нет перегрузок памяти, и я выбрал значения, которые сработали для меня (не стесняйтесь поиграть с константами, чтобы приспособиться к вашему варианту использования).
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)
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));
Вы можете сохранить счетчик, увеличивать его после рендеринга каждого кадра, а затем сбрасывать счетчик, когда вы находитесь в новую секунду (сохраняя предыдущее значение в качестве числа фреймов в последней секунде)
Как я это делаю!
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)".
Установите счетчик на ноль. Каждый раз, когда вы рисуете кадр, увеличивайте счетчик. После каждой секунды печатать счетчик. вспенить, промыть, повторить. Если вам нужен дополнительный кредит, сохраняйте счетчик и делите его на общее количество секунд для скользящего среднего.
В 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';
Совет: Если выборка равна 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)
Вот как я это делаю (на 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
}
Я адаптировал ответ @KPexEA для Go, переместил глобальные переменные в поля структуры, разрешил настраивать количество выборок и использовал их вместо простых целых чисел и чисел с плавающей запятой.
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))
}
Использование
Вы бы использовали это так:
// 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, но всегда включает в себя два распределения кучи: одно для самой структуры и одно для массива, поддерживающего срез образцов. При использовании, как указано выше, это долговременные выделения, поэтому они не будут облагать налогом сборщик мусора. Профилируйте перед оптимизацией, как всегда.
Однако, если производительность является серьезной проблемой, можно внести некоторые изменения, чтобы исключить распределения и косвенные ссылки:
- Измените сэмплы из кусочка
к массиву куда фиксируется во время компиляции. Это лишает возможности изменять количество выборок во время выполнения, но в большинстве случаев такая гибкость не требуется. - Затем удалите
конструктор полностью и используйте декларация (на уровне пакета или локально для ) вместо. В отличие от C, Go предварительно обнулит всю соответствующую память.
В (C++ like) псевдокоде эти два - то, что я использовал в приложениях для промышленной обработки изображений, которые должны были обрабатывать изображения с набора камер, запускаемых извне. Вариации "частоты кадров" имели другой источник (более медленное или более быстрое производство на ленте), но проблема та же. (Я предполагаю, что у вас есть простой вызов timer.peek(), который дает вам что-то вроде nr msec (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: обновляется каждый кадр, требует больше памяти и процессора
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
}
}
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;
}
}
});
К сожалению, большинство ответов здесь не дают достаточно точных или достаточно «медленных» измерений 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()
всякий раз, когда вы хотите отображать текущий FPS.
Теперь ключ находится в параметрах для
FpsCounter::new()
метод:
sample_period
насколько чувствителен к изменениям частоты кадров, и
samples
контролирует, насколько быстро увеличивается или уменьшается фактическая частота кадров. Итак, если вы выберете 10 мс и 100 выборок,
fps()
будет почти мгновенно реагировать на любое изменение частоты кадров - в основном, значение FPS на экране будет дрожать как сумасшедшее, но, поскольку это 100 выборок, потребуется 1 секунда, чтобы соответствовать фактической частоте кадров.
Таким образом, мой выбор 100 мс и 5 сэмплов означает, что отображаемый счетчик FPS не заставит вас кровоточить из глаз из-за безумно быстрого изменения, и он будет соответствовать вашей фактической частоте кадров через полсекунды после его изменения, что достаточно разумно для игры.
С
sample_period * samples
усредняет промежуток времени, вы не хотите, чтобы он был слишком коротким, если вам нужен достаточно точный счетчик FPS.
Хранить время начала и увеличивать свой framecounter один раз за цикл? каждые несколько секунд вы можете просто напечатать framecount/(сейчас - время начала) и затем повторно инициализировать их.
редактировать: упс двойной ninja'ed