Расчет кадров в секунду в игре

110

Какой хороший алгоритм подсчета кадров в секунду в игре? Я хочу показать это числом в углу экрана. Если я просто посмотрю, сколько времени потребовалось для рендеринга последнего кадра, число изменится слишком быстро.

Бонусные баллы, если ваш ответ обновляет каждый кадр и не сходится по-разному, когда частота кадров увеличивается или уменьшается.

Tod
источник

Ответы:

100

Вам нужно сглаженное среднее, самый простой способ - взять текущий ответ (время рисования последнего кадра) и объединить его с предыдущим ответом.

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

Регулируя соотношение 0,9 / 0,1, вы можете изменить «постоянную времени» - то есть, насколько быстро число реагирует на изменения. Большая доля в пользу старого ответа дает более медленное и плавное изменение, большая доля в пользу нового ответа дает более быстрое изменение значения. Очевидно, что эти два фактора должны складываться в один!

Мартин Беккет
источник
14
Тогда для защиты от ошибок и аккуратности вам, вероятно, понадобится что-то вроде float weightRatio = 0.1; и время = время * (1.0 - weightRatio) + last_frame * weightRatio
корона
2
В принципе звучит хорошо и просто, но на самом деле сглаживание такого подхода едва заметно. Не хорошо.
Petrucio
1
@Petrucio, если сглаживание слишком низкое, просто увеличьте постоянную времени (weightRatio = 0,05, 0,02, 0,01 ...)
Джон Дворжак
8
@Petrucio: last_frameне означает (или, по крайней мере, не должно означать) продолжительность предыдущего кадра; это должно означать значение, timeкоторое вы рассчитали для последнего кадра. Таким образом, будут включены все предыдущие кадры, причем самые последние кадры будут иметь наибольший вес.
j_random_hacker
1
Имя переменной last_frame вводит в заблуждение. "current_frame" было бы более наглядным. Также важно знать, что переменная «время» в примере должна быть глобальной (т.е. сохранять ее значение во всех кадрах) и, в идеале, числом с плавающей запятой. Он содержит среднее / совокупное значение и обновляется для каждого кадра. Чем выше коэффициент, тем больше времени потребуется, чтобы установить переменную «время» в сторону значения (или отклонить ее от него).
jox
52

Это то, что я использовал во многих играх.

#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);
}
KPexEA
источник
Мне очень нравится такой подход. Есть ли конкретная причина, по которой вы устанавливаете MAXSAMPLES на 100?
Zolomon
1
MAXSAMPLES - это количество значений, которые усредняются для получения значения fps.
Кори Гросс
8
Это простая скользящая средняя (SMA)
KindDragon
Отлично, спасибо! Я настроил его в своей игре, так что функция тика недействительна, а другая функция возвращает FPS, тогда я могу запускать основную функцию каждый тик, даже если код рендеринга не показывает FPS.
TheJosh
2
Пожалуйста, используйте модуль, а не if. tickindex = (tickindex + 1) % MAXSAMPLES;
Феликс К.
25

Ну конечно

frames / sec = 1 / (sec / frame)

Но, как вы указываете, время, необходимое для рендеринга одного кадра, сильно различается, и с точки зрения пользовательского интерфейса обновление значения fps с такой частотой кадров вообще неприменимо (если только число не очень стабильное).

Вероятно, вам нужно скользящее среднее или какой-то счетчик биннинга / сброса.

Например, вы можете поддерживать структуру данных очереди, в которой хранится время рендеринга для каждого из последних 30, 60, 100 или каких-то дополнительных кадров (вы даже можете спроектировать ее так, чтобы ограничение можно было регулировать во время выполнения). Чтобы определить приличное приближение fps, вы можете определить средний fps для всех времен рендеринга в очереди:

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

Когда вы заканчиваете рендеринг нового кадра, вы ставите в очередь новое время рендеринга и удаляете из очереди старое время рендеринга. В качестве альтернативы, вы могли исключить из очереди только тогда, когда общее время рендеринга превысило некоторое предустановленное значение (например, 1 секунду). Вы можете поддерживать «последнее значение частоты кадров в секунду» и последнюю обновленную метку времени, чтобы при желании можно было указать, когда следует обновлять число кадров в секунду. Хотя со скользящим средним, если у вас есть согласованное форматирование, печать «мгновенного среднего» fps для каждого кадра, вероятно, будет приемлемой.

Другой способ - установить счетчик сброса. Поддерживайте точную (миллисекундную) метку времени, счетчик кадров и значение fps. Когда вы закончите рендеринг кадра, увеличьте счетчик. Когда счетчик достигает заданного предела (например, 100 кадров) или когда время с момента отметки времени прошло некоторое заданное значение (например, 1 секунду), вычислите частоту кадров в секунду:

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

Затем сбросьте счетчик на 0 и установите метку времени на текущее время.

Клин
источник
12

Увеличивайте счетчик каждый раз, когда вы визуализируете экран, и сбрасывайте этот счетчик на некоторый интервал времени, в течение которого вы хотите измерить частоту кадров.

Т.е. Каждые 3 секунды снимайте счетчик / 3, а затем сбрасывайте счетчик.

апандит
источник
+1 Хотя это даст вам новое значение только в интервалах, это легко понять и не требует ни массивов, ни предположений значений и является правильным с научной точки зрения.
opatut 06
10

Сделать это можно как минимум двумя способами:


Первый - это тот, о котором говорили здесь до меня. Я считаю, что это самый простой и предпочтительный способ. Вы просто следите за

  • cn: счетчик количества обработанных кадров
  • time_start: время с момента начала отсчета
  • time_now: текущее время

Вычислить частоту кадров в этом случае так же просто, как вычислить эту формулу:

  • FPS = cn / (time_now - время_старт).

Тогда есть отличный способ, которым вы, возможно, захотите когда-нибудь воспользоваться:

Допустим, у вас есть «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)

Итак, согласно этой формуле (хотя мои навыки математического вывода немного устарели), чтобы вычислить новые кадры в секунду, вам нужно знать количество кадров в секунду из предыдущего кадра, продолжительность, затраченную на рендеринг последнего кадра, и количество кадров, которые вы получили. оказано.

Петр Янкуляк
источник
1
+1 за второй метод. Я полагаю, это было бы хорошо для сверхточных вычислений: 3
zeboidlund
5

Это может быть излишним для большинства людей, поэтому я не опубликовал его, когда реализовал. Но он очень прочный и гибкий.

Он хранит очередь с последним временем кадра, поэтому он может точно рассчитать среднее значение 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);
}
Петруччо
источник
2

Здесь хорошие ответы. То, как вы его реализуете, зависит от того, для чего вам это нужно. Я предпочитаю скользящее среднее значение "time = time * 0.9 + last_frame * 0.1" автора выше.

однако мне лично нравится больше взвешивать свое среднее значение по отношению к новым данным, потому что в игре именно ШИПЫ сложнее всего раздавить, и поэтому они представляют для меня наибольший интерес. Поэтому я бы использовал что-то более похожее на разделение .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.0f любой величиной, которую вы сочтете неприемлемой). Это позволит вам найти и, таким образом, решить проблемы FPS в конце кадра, которые они возникают.

Дэвид Френкель
источник
Мне нравится time = time * 0.9 + last_frame * 0.1средний расчет, который позволяет плавно изменять отображение.
Фабьен Кватраво,
2

Гораздо лучшая система, чем использование большого массива старых кадровых частот, - это просто сделать что-то вроде этого:

new_fps = old_fps * 0.99 + new_fps * 0.01

Этот метод использует гораздо меньше памяти, требует гораздо меньше кода и придает большее значение недавней частоте кадров, чем старой, но при этом сглаживает эффекты внезапных изменений частоты кадров.

Барри Смит
источник
1

Вы можете сохранить счетчик, увеличивать его после рендеринга каждого кадра, а затем сбрасывать счетчик, когда вы находитесь на новой секунде (сохраняя предыдущее значение как количество визуализированных кадров за последнюю секунду)

Майк Стоун
источник
1

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));
Эфеллон Данцлер
источник
1

Вот полный пример, использующий 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)
jd20
источник
0

Установите счетчик на ноль. Каждый раз, когда вы рисуете кадр, увеличивайте счетчик. Через каждую секунду выведите счетчик. вспенить, промыть, повторить. Если вам нужен дополнительный кредит, держите текущий счетчик и разделите его на общее количество секунд, чтобы получить среднее значение.

Брайан Окли
источник
0

В псевдокоде (похожем на C ++) эти два я использовал в промышленных приложениях для обработки изображений, которые должны были обрабатывать изображения с набора камер с внешним запуском. Изменения в «частоте кадров» имели другой источник (более медленное или быстрое воспроизведение на ленте), но проблема та же. (Я предполагаю, что у вас есть простой вызов timer.peek (), который дает вам что-то вроде номера 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
   }
} 
Jilles de Wit
источник
0
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;
        }
    }

});
Totty.js
источник
0

Как я это делаю!

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);
}

Проще говоря, тикающие часы отслеживают тиканье. Если это первый раз, он берет текущее время и помещает его в «тикстарт». После первого тика он делает переменную «fps» равной количеству тиков тикового таймера, разделенному на время минус время первого тика.

Fps - это целое число, поэтому "(int)".

БутылкаФакт
источник
1
Не рекомендую никому. Разделение общего количества тиков на общее количество секунд приближает FPS к чему-то вроде математического предела, когда он в основном устанавливается на 2-3 значения через долгое время и отображает неточные результаты.
Kartik Chugh
0

Вот как я это делаю (на 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
}
авантюристOK
источник
0

В 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, результатом будет частота кадров и время кадра в реальном времени.

Донаван Карвалью
источник
0

Это основано на ответе 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)
danday74
источник
-1

сохранить время начала и увеличивать счетчик кадров один раз за цикл? каждые несколько секунд вы можете просто печатать framecount / (сейчас - время начала), а затем повторно инициализировать их.

редактировать: упс. двойной ниндзя

Джимми
источник