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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
111
0
111 075
19

Ответы 19

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

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

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

Тогда для защиты от ошибок и аккуратности вам, вероятно, понадобится что-то вроде float weightRatio = 0.1; и время = время * (1.0 - weightRatio) + last_frame * weightRatio

korona 11.11.2008 13:04

В принципе, звучит хорошо и просто, но на самом деле сглаживание такого подхода едва заметно. Не хорошо.

Petrucio 10.12.2012 21:15

@Petrucio, если сглаживание слишком низкое, просто увеличьте постоянную времени (weightRatio = 0,05, 0,02, 0,01 ...)

John Dvorak 18.05.2013 21:44

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

Petrucio 23.05.2013 23:37

@Petrucio: last_frame не означает (или, по крайней мере, не следует означает) продолжительность предыдущего кадра; это должно означать значение time, которое вы рассчитали для последнего кадра. Таким образом, будут включены предыдущие кадры все, причем самые последние кадры будут иметь наибольший вес.

j_random_hacker 24.05.2013 00:37

@j_random_hacker: Достаточно честно, но я тестировал это на практике, и результаты оказались не очень хорошими.

Petrucio 24.05.2013 10:56

@Petrucio, возможно, вы слишком рано приводили к целочисленному типу и усекали значение дробного веса?

Lynden Shields 28.06.2013 06:56

Имя переменной last_frame вводит в заблуждение. "current_frame" было бы более наглядным. Также важно знать, что переменная «время» в примере должна быть глобальной (т.е. сохранять ее значение во всех кадрах) и, в идеале, числом с плавающей запятой. Он содержит среднее / совокупное значение и обновляется для каждого кадра. Чем выше коэффициент, тем больше времени потребуется, чтобы установить переменную «время» в сторону значения (или отодвинуть ее от него).

jox 07.06.2015 01:08

Что представляют собой переменные time и last_frame в этом примере?

Alex Spurling 05.10.2015 17:31

Думаю, имеет смысл указать коэффициент сглаживания, нормализованный по секундам, например double damping = 0.05 /* per second */; double oldWeight = pow(damping, secondsSinceListMeasurement); measurement = oldWeight * measurement + (1-oldWeight)*fpsSinceLastMeasurement; Таким образом, демпфирование всегда имеет одну и ту же постоянную времени независимо от частоты обновлений. 0,05 здесь примерно коэффициент 0,9 на кадр при 30 кадрах в секунду.

Tilman Vogel 24.03.2016 23:22

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

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

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

opatut 06.10.2012 16:22

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

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

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

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

Ну конечно

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 и установите метку времени на текущее время.

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

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

Мне очень нравится такой подход. Есть ли конкретная причина, по которой вы устанавливаете MAXSAMPLES на 100?

Zolomon 28.11.2011 00:12

MAXSAMPLES - это количество значений, которые усредняются для получения значения fps.

Cory Gross 07.07.2012 21:25

Это простая скользящая средняя (SMA)

KindDragon 27.07.2012 19:34

Отлично, спасибо! Я настроил его в своей игре, так что функция тика недействительна, а другая функция возвращает FPS, тогда я могу запускать основную функцию каждый тик, даже если код рендеринга не показывает FPS.

TheJosh 14.02.2013 02:02

Рассмотрите возможность использования std :: deque для хранения произвольного количества выборок. Затем, когда вы суммируете отсчеты, вы можете удалить отсчеты старше 1 секунды и использовать количество оставшихся отсчетов в качестве «частоты кадров с последней 1 секунды».

michaelmoo 05.03.2016 01:46

Что, если частота кадров изменится настолько резко, что вы не сможете установить MAXSAMPLES?

Rivera 15.03.2016 19:33

ticksum можно сразу объявить как double, так что вам не нужно преобразовывать его при делении на MAXSAMPLES.

heltonbiker 06.06.2017 22:34

Пожалуйста, используйте модуль, а не if. tickindex = (tickindex + 1) % MAXSAMPLES;

Felix K. 05.04.2018 11:30

В псевдокоде (похожем на 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
   }
} 

Здесь хорошие ответы. То, как вы его реализуете, зависит от того, для чего вам это нужно. Я предпочитаю скользящее среднее значение "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, благодаря которому отображение меняется плавно.

Fabien Quatravaux 27.09.2012 17:00

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


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

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

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

  • 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 за второй метод. Я полагаю, это было бы хорошо для сверхточных вычислений: 3

zeboidlund 24.10.2013 03:39

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

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

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

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

Kartik Chugh 03.04.2017 08:16

Вот как я это делаю (на 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
}

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

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

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

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 (но легко адаптированный к любому языку). Он использует уравнение сглаживания в ответе Мартина, поэтому почти нет накладных расходов на память, и я выбрал значения, которые сработали для меня (не стесняйтесь поиграть с константами, чтобы адаптироваться к вашему варианту использования).

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)

В 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)

Другие вопросы по теме