Рассчитать FPS в Canvas, используя requestAnimationFrame - PullRequest
31 голосов
/ 26 ноября 2011

Как я могу рассчитать FPS игрового приложения холста?Я видел несколько примеров, но ни один из них не использует requestAnimationFrame, и я не уверен, как применить свои решения там.Это мой код:

(function(window, document, undefined){

    var canvas       = document.getElementById("mycanvas"),
        context      = canvas.getContext("2d"),
        width        = canvas.width,
        height       = canvas.height,
        fps          = 0,
        game_running = true,
        show_fps     = true;

    function showFPS(){
        context.fillStyle = "Black";
        context.font      = "normal 16pt Arial";

        context.fillText(fps + " fps", 10, 26);
    }
    function gameLoop(){

        //Clear screen
        context.clearRect(0, 0, width, height);

        if (show_fps) showFPS();

        if (game_running) requestAnimationFrame(gameLoop);

    }
    
    gameLoop();

}(this, this.document))
canvas{
    border: 3px solid #fd3300;
}
<canvas id="mycanvas" width="300" height="150"></canvas>

Кстати, есть ли какая-нибудь библиотека, которую я мог бы добавить, чтобы контролировать производительность?

Ответы [ 9 ]

26 голосов
/ 26 ноября 2011

Вы можете отслеживать последний раз, когда requestAnimFrame был вызван.

var lastCalledTime;
var fps;

function requestAnimFrame() {

  if(!lastCalledTime) {
     lastCalledTime = Date.now();
     fps = 0;
     return;
  }
  delta = (Date.now() - lastCalledTime)/1000;
  lastCalledTime = Date.now();
  fps = 1/delta;
} 

http://jsfiddle.net/vZP3u/

23 голосов
/ 17 июля 2017

Не использовать new Date()

Этот API имеет несколько недостатков и полезен только для получения текущей даты + времени. Не для измерения временных интервалов.

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

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

Кроме того, Date-API очень неточен - часто намного меньше, чем 1 мс. Это делает его особенно бесполезным для измерений частоты кадров, когда один кадр 60 Гц требует ~ 17 мс.

Вместо этого используйте performance.now()

Performance API был специально создан для таких случаев использования и может использоваться эквивалентно new Date(). Просто возьмите один из ответов и замените new Date() на performance.now(), и вы готовы к работе.

Источники:

Также в отличие от Date.now (), значения, возвращаемые Performance.now () всегда увеличивается с постоянной скоростью, независимо от системных часов (который может быть скорректирован вручную или искажен программным обеспечением, таким как NTP). В противном случае, performance.timing.navigationStart + performance.now () будет быть примерно равным Date.now ().

https://developer.mozilla.org/en-US/docs/Web/API/Performance/now

и для windows:

[Служба времени] регулирует местную частоту часов, чтобы позволить ей сходиться к правильному времени. Если разница во времени между местными часами и [точной временной выборкой] слишком велика, чтобы скорректировать ее, отрегулировав местную тактовая частота, служба времени устанавливает местное время на правильное время.

https://technet.microsoft.com/en-us/library/cc773013(v=ws.10).aspx

17 голосов
/ 25 февраля 2017

Chrome имеет встроенный счетчик кадров в секунду: https://developer.chrome.com/devtools/docs/rendering-settings

enter image description here

Просто откройте консоль разработчика ( F12 ), откройте ящик ( Esc ) и добавьте вкладку «Рендеринг».

Здесь вы можете активировать оверлей FPS-Meter для просмотра текущей частоты кадров.(включая хороший график), а также потребление памяти графическим процессором.

Кросс-браузерное решение: Вы можете получить аналогичное наложение с библиотекой JavaScript stat.js: https://github.com/mrdoob/stats.js/

enter image description here

Он также обеспечивает хорошее наложение на частоту кадров (включая график) и очень прост в использовании.

При сравнении результатов из stats.js и инструментов разработчика Chrome оба показывают одинаковые измерения.Так что вы можете доверять этой библиотеке, чтобы она действовала правильно.

8 голосов
/ 04 ноября 2013

У меня другой подход, потому что если вы вычислите FPS, вы получите это мерцание при возврате числа.Я решил посчитать каждый кадр и возвращать его раз в секунду

window.countFPS = (function () {
  var lastLoop = (new Date()).getMilliseconds();
  var count = 1;
  var fps = 0;

  return function () {
    var currentLoop = (new Date()).getMilliseconds();
    if (lastLoop > currentLoop) {
      fps = count;
      count = 1;
    } else {
      count += 1;
    }
    lastLoop = currentLoop;
    return fps;
  };
}());

requestAnimationFrame(function () {
  console.log(countFPS());
});

jsfiddle

3 голосов
/ 09 октября 2018

На самом деле ни один из ответов не был достаточным для меня.Вот лучшее решение, которое:

  • Use's performance.now ()
  • Вычисляет фактическое среднее кадров в секунду
  • Среднее в секундуи десятичные разряды настраиваются

Код:

// Options
const outputEl         = document.getElementById('fps-output');
const decimalPlaces    = 2;
const updateEachSecond = 1;

// Cache values
const decimalPlacesRatio = Math.pow(10, decimalPlaces);
let timeMeasurements     = [];

// Final output
let fps = 0;

const tick = function() {
  timeMeasurements.push(performance.now());

  const msPassed = timeMeasurements[timeMeasurements.length - 1] - timeMeasurements[0];

  if (msPassed >= updateEachSecond * 1000) {
    fps = Math.round(timeMeasurements.length / msPassed * 1000 * decimalPlacesRatio) / decimalPlacesRatio;
    timeMeasurements = [];
  }

  outputEl.innerText = fps;

  requestAnimationFrame(() => {
    tick();
  });
}

tick();

JSFiddle

3 голосов
/ 30 декабря 2017

Вот еще одно решение:

var times = [];
var fps;

function refreshLoop() {
  window.requestAnimationFrame(function() {
    const now = performance.now();
    while (times.length > 0 && times[0] <= now - 1000) {
      times.shift();
    }
    times.push(now);
    fps = times.length;
    refreshLoop();
  });
}

refreshLoop();

Это улучшает некоторые из следующих способов:

  • performance.now() используется более Date.now() для повышения точности (, как описано в этом ответе )
  • FPS измеряется в течение последней секунды, поэтому число не будет меняться так беспорядочно, особенно для приложений с одиночными длинными кадрами.

Я написал об этом решении более подробно на моем сайте .

3 голосов
/ 06 апреля 2013

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

var GameLoop = function(fn, fps){
    var now;
    var delta;
    var interval;
    var then = new Date().getTime();

    var frames;
    var oldtime = 0;

    return (function loop(time){
        requestAnimationFrame(loop);

        interval = 1000 / (this.fps || fps || 60);
        now = new Date().getTime();
        delta = now - then;

        if (delta > interval) {
            // update time stuffs
            then = now - (delta % interval);

            // calculate the frames per second
            frames = 1000 / (time - oldtime)
            oldtime = time;

            // call the fn
            // and pass current fps to it
            fn(frames);
        }
    }(0));
};

Использование:

var set;
document.onclick = function(){
    set = true;
};

GameLoop(function(fps){
    if(set) this.fps = 30;
    console.log(fps);
}, 5);

http://jsfiddle.net/ARTsinn/rPAeN/

3 голосов
/ 26 ноября 2011

Просто проверьте разницу во времени между обратными вызовами AFR.AFR уже передает время в качестве аргумента для обратного вызова.Я обновил вашу скрипку, чтобы показать ее: http://jsfiddle.net/WCKhH/1/

1 голос
/ 12 апреля 2019

Мне не хватало реализации, позволяющей настроить размер выборки для усредненного значения FPS. Вот мой, он имеет следующие особенности:

  • Точный : на основе performance.now ()
  • Стабилизировано : Возвращенное значение FPS является усредненным значением (fps.value | fps.tick ())
  • Настраиваемый : размер массива сэмплов FPS можно настраивать (fps.samplesSize)
  • Эффективный : вращающаяся решетка для сбора образцов (избегает изменения размера решетки)

const fps = {
    sampleSize : 60,    
    value : 0,
    _sample_ : [],
    _index_ : 0,
    _lastTick_: false,
    tick : function(){
        // if is first tick, just set tick timestamp and return
        if( !this._lastTick_ ){
            this._lastTick_ = performance.now();
            return 0;
        }
        // calculate necessary values to obtain current tick FPS
        let now = performance.now();
        let delta = (now - this._lastTick_)/1000;
        let fps = 1/delta;
        // add to fps samples, current tick fps value 
        this._sample_[ this._index_ ] = Math.round(fps);
        
        // iterate samples to obtain the average
        let average = 0;
        for(i=0; i<this._sample_.length; i++) average += this._sample_[ i ];

        average = Math.round( average / this._sample_.length);

        // set new FPS
        this.value = average;
        // store current timestamp
        this._lastTick_ = now;
        // increase sample index counter, and reset it
        // to 0 if exceded maximum sampleSize limit
        this._index_++;
        if( this._index_ === this.sampleSize) this._index_ = 0;
        return this.value;
    }
}


// *******************
// test time...
// *******************

function loop(){
    let fpsValue = fps.tick();
    window.fps.innerHTML = fpsValue;
    requestAnimationFrame( loop );
}
// set FPS calulation based in the last 120 loop cicles 
fps.sampleSize = 120;
// start loop
loop()
<div id="fps">--</div>
...