Подтвердить что ты не робот

Рассчитать FPS в Canvas, используя requestAnimationFrame

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

window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame || 
    window.webkitRequestAnimationFrame   || 
    window.mozRequestAnimationFrame      || 
    window.oRequestAnimationFrame        || 
    window.msRequestAnimationFrame       || 
    function(callback, element){
        window.setTimeout(function(){
            callback(+new Date);
        }, 1000 / 60);
    };
})();

(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) requestAnimFrame(gameLoop);

    }
    
    gameLoop();

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

Ответ 1

Не используйте new Date()

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

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

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

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

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

API-интерфейс Performance специально разработан для таких случаев использования и может быть эквивалентен 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

А для окон:

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

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

Ответ 2

Вы можете отслеживать последний запрос 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/

Ответ 3

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

Ответ 4

У меня другой подход, потому что если вы вычислите 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

Ответ 5

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

Ответ 6

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

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/

Ответ 7

Здесь другое решение:

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 измеряется за последнюю секунду, поэтому число не будет скачкообразно перемещаться, особенно для приложений с одиночными длинными кадрами.

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

Ответ 8

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

  • Используйте 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

Ответ 9

Мне не хватало реализации, позволяющей настроить размер выборки для усредненного значения 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>