Calculando quadros por segundo em um jogo


111

Qual é um bom algoritmo para calcular quadros por segundo em um jogo? Quero mostrá-lo como um número no canto da tela. Se eu apenas olhar quanto tempo levou para renderizar o último quadro, o número muda muito rápido.

Pontos de bônus se sua resposta atualizar cada quadro e não convergir de maneira diferente quando a taxa de quadros está aumentando ou diminuindo.

Respostas:


101

Você precisa de uma média suavizada, a maneira mais fácil é pegar a resposta atual (o tempo para desenhar o último quadro) e combiná-la com a resposta anterior.

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

Ajustando a proporção de 0,9 / 0,1, você pode alterar a 'constante de tempo' - ou seja, a rapidez com que o número responde às alterações. Uma fração maior a favor da resposta antiga dá uma mudança mais lenta e suave, uma fração grande a favor da nova resposta dá um valor de mudança mais rápido. Obviamente, os dois fatores devem se somar a um!


14
Então, para evitar erros e organizar, você provavelmente vai querer algo como float weightRatio = 0.1; e time = time * (1.0 - weightRatio) + last_frame * weightRatio
korona

2
Parece bom e simples em princípio, mas na realidade a suavização dessa abordagem é quase imperceptível. Nada de bom.
Petrucio

1
@Petrucio se a suavização for muito baixa, apenas aumente a constante de tempo (weightRatio = 0,05, 0,02, 0,01 ...)
John Dvorak

8
@Petrucio: last_framenão significa (ou pelo menos não deveria significar) a duração do frame anterior; deve significar o valor timeque você calculou para o último quadro. Desta forma, todos os frames anteriores serão incluídos, com os frames mais recentes mais pesados.
j_random_hacker

1
O nome da variável "last_frame" é enganoso. "current_frame" seria mais descritivo. Também é importante saber que a variável "tempo" no exemplo deve ser global (ou seja, manter seu valor em todos os quadros) e, idealmente, um número de ponto flutuante. Ele contém o valor médio / agregado e os get's atualizados em cada quadro. Quanto maior a proporção, mais tempo levará para estabelecer a variável "tempo" em direção a um valor (ou afastá-la dele).
jox

52

É o que tenho usado em muitos jogos.

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

Eu gosto muito dessa abordagem. Algum motivo específico para definir MAXSAMPLES como 100?
Zolomon

1
MAXSAMPLES aqui é o número de valores que são calculados em média para chegar a um valor para fps.
Cory Gross

8
É média móvel simples (SMA)
KindDragon

Perfeito, obrigado! Eu ajustei no meu jogo para que a função tick fique vazia e outra função retorne o FPS, então posso executar a principal a cada tick, mesmo se o código de renderização não tiver FPS mostrado.
TheJosh

2
Use o módulo e não um if. tickindex = (tickindex + 1) % MAXSAMPLES;
Felix K.

25

Bem, certamente

frames / sec = 1 / (sec / frame)

Mas, como você apontou, há muita variação no tempo que leva para renderizar um único quadro e, de uma perspectiva da IU, atualizar o valor fps na taxa de quadros não pode ser usado (a menos que o número seja muito estável).

O que você quer provavelmente é uma média móvel ou algum tipo de contador de binning / reset.

Por exemplo, você poderia manter uma estrutura de dados de fila que continha os tempos de renderização para cada um dos últimos 30, 60, 100 ou o que quer que fosse (você poderia até mesmo projetá-la de forma que o limite fosse ajustável em tempo de execução). Para determinar uma aproximação de fps decente, você pode determinar a média de fps de todos os tempos de renderização na fila:

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

Ao terminar de renderizar um novo quadro, você enfileira um novo tempo de renderização e retira da fila um tempo de renderização antigo. Como alternativa, você pode retirar da fila apenas quando o total dos tempos de renderização exceder algum valor predefinido (por exemplo, 1 segundo). Você pode manter o "último valor fps" e um carimbo de data / hora da última atualização para que possa acionar quando atualizar a figura fps, se desejar. Embora com uma média móvel se você tiver uma formatação consistente, imprimir a "média instantânea" de fps em cada quadro provavelmente seria aceitável.

Outro método seria ter um contador de reinicialização. Mantenha um carimbo de data / hora preciso (milissegundo), um contador de quadros e um valor de fps. Ao terminar de renderizar um quadro, incremente o contador. Quando o contador atinge um limite predefinido (por exemplo, 100 quadros) ou quando o tempo desde o carimbo de data / hora passou algum valor predefinido (por exemplo, 1 segundo), calcule o fps:

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

Em seguida, redefina o contador para 0 e defina o carimbo de data / hora para a hora atual.


12

Aumente um contador toda vez que você renderizar uma tela e limpe esse contador por algum intervalo de tempo sobre o qual deseja medir a taxa de quadros.

Ie. A cada 3 segundos, obtenha o contador / 3 e zere o contador.


+1 Embora isso só forneça um novo valor em intervalos, é fácil de entender e não requer matrizes nem adivinhação de valores e é cientificamente correto.
opatut

10

Existem pelo menos duas maneiras de fazer isso:


O primeiro é aquele que outros mencionaram aqui antes de mim. Acho que é a forma mais simples e preferida. Você apenas para acompanhar

  • cn: contador de quantos quadros você renderizou
  • time_start: o tempo desde que você começou a contar
  • time_now: a hora atual

Calcular o fps, neste caso, é tão simples quanto avaliar esta fórmula:

  • FPS = cn / (time_now - time_start).

Depois, há a maneira super legal que você gostaria de usar algum dia:

Digamos que você tenha frames 'i' a considerar. Usarei esta notação: f [0], f [1], ..., f [i-1] para descrever quanto tempo levou para renderizar o quadro 0, quadro 1, ..., quadro (i-1 ), respectivamente.

Example where i = 3

|f[0]      |f[1]         |f[2]   |
+----------+-------------+-------+------> time

Então, a definição matemática de fps após i frames seria

(1) fps[i]   = i     / (f[0] + ... + f[i-1])

E a mesma fórmula, mas considerando apenas frames i-1.

(2) fps[i-1] = (i-1) / (f[0] + ... + f[i-2]) 

Agora, o truque aqui é modificar o lado direito da fórmula (1) de modo que contenha o lado direito da fórmula (2) e substituí-lo por seu lado esquerdo.

Assim (você deve ver mais claramente se escrever em um papel):

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)

Portanto, de acordo com esta fórmula (embora minha habilidade de derivar matemática esteja um pouco enferrujada), para calcular os novos fps você precisa saber o fps do quadro anterior, a duração que levou para renderizar o último quadro e o número de quadros que você renderizado.


1
1 para o segundo método. Eu imagino que seria bom para uma computação precisa: 3
zeboidlund

5

Isso pode ser um exagero para a maioria das pessoas, é por isso que eu não postei quando o implementei. Mas é muito robusto e flexível.

Ele armazena uma fila com os tempos dos últimos quadros, para que possa calcular com precisão um valor médio de FPS muito melhor do que apenas levar o último quadro em consideração.

Também permite que você ignore um quadro, se estiver fazendo algo que sabe que vai atrapalhar artificialmente o tempo daquele quadro.

Ele também permite que você altere o número de quadros a serem armazenados na Fila à medida que ela é executada, para que você possa testar imediatamente qual é o melhor valor para você.

// 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

Boas respostas aqui. O modo como você o implementa depende do que você precisa. Eu prefiro a média de corrida para mim mesmo "tempo = tempo * 0,9 + último_frame * 0,1" pelo cara acima.

no entanto, eu pessoalmente gosto de pesar minha média mais fortemente em relação aos dados mais recentes porque em um jogo são os PONTOS que são mais difíceis de esmagar e, portanto, de maior interesse para mim. Então, eu usaria algo mais como uma divisão .7 \ .3 para fazer um pico aparecer muito mais rápido (embora seu efeito também caia da tela mais rápido .. veja abaixo)

Se o seu foco está no tempo de RENDIMENTO, então a divisão .9.1 funciona muito bem, mas tende a ser mais suave. Embora os picos de jogabilidade / IA / física sejam muito mais preocupantes, pois ISSO normalmente fará com que seu jogo pareça instável (o que costuma ser pior do que uma taxa de quadros baixa, supondo que não estamos caindo abaixo de 20 fps)

Então, o que eu faria é adicionar algo como isto:

#define ONE_OVER_FPS (1.0f/60.0f)
static float g_SpikeGuardBreakpoint = 3.0f * ONE_OVER_FPS;
if(time > g_SpikeGuardBreakpoint)
    DoInternalBreakpoint()

(preencha 3.0f com qualquer magnitude que você achar um pico inaceitável) Isso permitirá que você encontre e, assim, resolva os problemas de FPS no final do quadro em que eles acontecem.


Gosto do time = time * 0.9 + last_frame * 0.1cálculo médio que faz com que a tela mude suavemente.
Fabien Quatravaux

2

Um sistema muito melhor do que usar uma grande variedade de taxas de quadros antigas é apenas fazer algo assim:

new_fps = old_fps * 0.99 + new_fps * 0.01

Este método usa muito menos memória, requer muito menos código e dá mais importância às taxas de quadros recentes do que às antigas, ao mesmo tempo em que suaviza os efeitos de mudanças repentinas na taxa de quadros.


1

Você pode manter um contador, incrementá-lo após cada quadro ser renderizado e, em seguida, zerar o contador quando estiver em um novo segundo (armazenando o valor anterior como o número do último segundo de quadros renderizados)


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

Aqui está um exemplo completo, usando Python (mas facilmente adaptado para qualquer linguagem). Ele usa a equação de suavização na resposta de Martin, então quase nenhuma sobrecarga de memória, e eu escolhi valores que funcionaram para mim (sinta-se à vontade para brincar com as constantes para se adaptar ao seu caso de uso).

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)

0

Defina o contador para zero. Cada vez que você desenha um quadro, incremente o contador. Após cada segundo, imprima o contador. ensaboe, enxágue, repita. Se você quiser crédito extra, mantenha um contador contínuo e divida pelo número total de segundos para obter uma média contínua.


0

No pseudocódigo (tipo c ++), esses dois são o que eu usei em aplicativos de processamento de imagem industrial que tinham que processar imagens de um conjunto de câmeras acionadas externamente. As variações na "taxa de quadros" tinham uma origem diferente (produção mais lenta ou mais rápida na esteira), mas o problema é o mesmo. (Presumo que você tenha uma chamada timer.peek () simples que fornece algo como o número de msec (nsec?) Desde o início do aplicativo ou a última chamada)

Solução 1: rápido, mas não atualizado a cada quadro

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

Solução 2: atualizado a cada quadro, requer mais memória e CPU

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

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

});

0

Como eu faço isso!

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

Em palavras, um tique-taque rastreia os tiques Se for a primeira vez, ele pega a hora atual e a coloca em 'tickstart'. Após o primeiro tique, torna a variável 'fps' igual a quantos tiques do relógio de tique dividido pelo tempo menos o tempo do primeiro tique.

Fps é um número inteiro, portanto, "(int)".


1
Não recomendaria a ninguém. Dividir o número total de tiques pelo número total de segundos faz com que o FPS se aproxime de algo como um limite matemático, onde basicamente se estabelece em 2-3 valores após um longo tempo e exibe resultados imprecisos.
Kartik Chugh,

0

Veja como eu faço (em 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
}

0

No Typescript, eu uso este algoritmo para calcular as médias de framerate e frametime:

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

uso:

statsUpdate();

// Print
stats.innerHTML = Math.round(framerate) + ' FPS ' + frametime.toFixed(2) + ' ms';

Dica: Se samples for 1, o resultado será a taxa de quadros e o tempo de quadros em tempo real.


0

Isso é baseado na resposta do KPexEA e fornece a média móvel simples. Arrumado e convertido para TypeScript para fácil copiar e colar:

Declaração de variável:

fpsObject = {
  maxSamples: 100,
  tickIndex: 0,
  tickSum: 0,
  tickList: []
}

Função:

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

Uso (pode variar em seu aplicativo):

this.fps = this.calculateFps(this.ticker.FPS)

-1

armazenar uma hora de início e incrementar seu framecounter uma vez por loop? a cada poucos segundos, você pode apenas imprimir framecount / (Now - starttime) e reinicializá-los.

editar: oops. duplo ninja

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.