Calcular fotogramas por segundo en un juego

110

¿Cuál es un buen algoritmo para calcular cuadros por segundo en un juego? Quiero mostrarlo como un número en la esquina de la pantalla. Si solo miro cuánto tiempo tomó renderizar el último cuadro, el número cambia demasiado rápido.

Puntos de bonificación si su respuesta actualiza cada cuadro y no converge de manera diferente cuando la velocidad de cuadros aumenta o disminuye.

Tod
fuente

Respuestas:

100

Necesita un promedio suavizado, la forma más fácil es tomar la respuesta actual (el tiempo para dibujar el último cuadro) y combinarla con la respuesta anterior.

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

Al ajustar la relación 0,9 / 0,1, puede cambiar la 'constante de tiempo', es decir, la rapidez con la que el número responde a los cambios. Una fracción más grande a favor de la respuesta anterior da un cambio más lento y suave, una fracción grande a favor de la nueva respuesta da un valor de cambio más rápido. ¡Obviamente, los dos factores deben sumarse a uno!

Martin Beckett
fuente
14
Entonces, para ser infalible y ordenado, probablemente quieras algo como float weightRatio = 0.1; and time = time * (1.0 - weightRatio) + last_frame * weightRatio
korona
2
Suena bien y simple en principio, pero en realidad la suavidad de este enfoque apenas se nota. No es bueno.
Petrucio
1
@Petrucio si el suavizado es demasiado bajo, simplemente aumente la constante de tiempo (weightRatio = 0.05, 0.02, 0.01 ...)
John Dvorak
8
@Petrucio: last_frameno significa (o al menos no debería significar) la duración del cuadro anterior; debería significar el valor timeque calculaste para el último fotograma. De esta manera, se incluirán todos los fotogramas anteriores, siendo los fotogramas más recientes los que se ponderan más.
j_random_hacker
1
El nombre de la variable "last_frame" es engañoso. "current_frame" sería más descriptivo. También es importante saber que la variable "tiempo" en el ejemplo debe ser global (es decir, mantener su valor en todos los marcos) e idealmente un número de punto flotante. Contiene el valor promedio / agregado y se actualiza en cada cuadro. Cuanto más alta sea la relación, más tiempo llevará asentar la variable "tiempo" hacia un valor (o desviarla).
jox
52

Esto es lo que he usado en muchos juegos.

#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
fuente
Me gusta mucho este enfoque. ¿Alguna razón específica por la que configuró MAXSAMPLES en 100?
Zolomon
1
MAXSAMPLES aquí es el número de valores que se promedian para obtener un valor para fps.
Cory Gross
8
Es una media móvil simple (SMA)
KindDragon
¡Perfecto gracias! Lo modifiqué en mi juego para que la función tick sea nula y otra función devuelva el FPS, luego puedo ejecutar la principal en cada tick, incluso si el código de renderizado no tiene FPS mostrado.
TheJosh
2
Utilice modulo y no if. tickindex = (tickindex + 1) % MAXSAMPLES;
Felix K.
25

Bueno, ciertamente

frames / sec = 1 / (sec / frame)

Pero, como señala, hay mucha variación en el tiempo que lleva renderizar un solo fotograma, y ​​desde la perspectiva de la interfaz de usuario, actualizar el valor de fps a la velocidad de fotogramas no se puede utilizar en absoluto (a menos que el número sea muy estable).

Lo que desea es probablemente un promedio móvil o algún tipo de contador de binning / reset.

Por ejemplo, podría mantener una estructura de datos de cola que contenga los tiempos de representación para cada uno de los últimos 30, 60, 100 o lo que sea que tenga (incluso podría diseñarlo para que el límite fuera ajustable en tiempo de ejecución). Para determinar una aproximación de fps decente, puede determinar el promedio de fps de todos los tiempos de renderizado en la cola:

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

Cuando termina de renderizar un nuevo fotograma, pone en cola un nuevo tiempo de renderizado y retira de la cola un tiempo de renderizado antiguo. Alternativamente, puede retirar de la cola solo cuando el total de los tiempos de renderizado excediera algún valor preestablecido (por ejemplo, 1 segundo). Puede mantener el "último valor de fps" y una marca de tiempo actualizada para poder activar cuándo actualizar la cifra de fps, si así lo desea. Aunque con un promedio móvil si tiene un formato consistente, imprimir el "promedio instantáneo" fps en cada cuadro probablemente estaría bien.

Otro método sería tener un contador de puesta a cero. Mantenga una marca de tiempo precisa (milisegundos), un contador de fotogramas y un valor de fps. Cuando termine de renderizar un fotograma, incremente el contador. Cuando el contador alcanza un límite preestablecido (por ejemplo, 100 fotogramas) o cuando el tiempo transcurrido desde la marca de tiempo ha pasado algún valor preestablecido (por ejemplo, 1 segundo), calcule los fps:

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

Luego, restablezca el contador a 0 y establezca la marca de tiempo en la hora actual.

Cuña
fuente
12

Incremente un contador cada vez que renderice una pantalla y borre ese contador durante algún intervalo de tiempo durante el cual desea medir la velocidad de fotogramas.

Es decir. Cada 3 segundos, obtenga el contador / 3 y luego borre el contador.

apandit
fuente
+1 Aunque esto solo le dará un nuevo valor en intervalos, es fácil de entender y no requiere matrices ni adivinar valores y es científicamente correcto.
opatut
10

Hay al menos dos formas de hacerlo:


El primero es el que otros han mencionado aquí antes que yo. Creo que es la forma más sencilla y preferida. Tu solo para hacer un seguimiento de

  • cn: contador de cuántos fotogramas has renderizado
  • time_start: el tiempo desde que comenzó a contar
  • time_now: la hora actual

Calcular los fps en este caso es tan simple como evaluar esta fórmula:

  • FPS = cn / (hora_ahora - hora_inicio).

Luego está la forma súper genial que le gustaría usar algún día:

Digamos que tiene marcos en 'i' para considerar. Usaré esta notación: f [0], f [1], ..., f [i-1] para describir cuánto tiempo tomó renderizar el cuadro 0, el cuadro 1, ..., cuadro (i-1 ) respectivamente.

Example where i = 3

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

Entonces, la definición matemática de fps después de i cuadros sería

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

Y la misma fórmula pero solo considerando marcos i-1.

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

Ahora, el truco aquí es modificar el lado derecho de la fórmula (1) de tal manera que contenga el lado derecho de la fórmula (2) y sustituirlo por su lado izquierdo.

Así (deberías verlo más claramente si lo escribes en un 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)

Entonces, de acuerdo con esta fórmula (aunque mi habilidad de derivación matemática está un poco oxidada), para calcular los nuevos fps necesita conocer los fps del cuadro anterior, la duración que tardó en renderizar el último cuadro y la cantidad de cuadros que ha prestados.

Peter Jankuliak
fuente
1
+1 para el segundo método. Me imagino que sería bueno para un cálculo súper preciso: 3
zeboidlund
5

Esto puede ser excesivo para la mayoría de las personas, por eso no lo había publicado cuando lo implementé. Pero es muy robusto y flexible.

Almacena una cola con los últimos tiempos de fotogramas, por lo que puede calcular con precisión un valor promedio de FPS mucho mejor que solo tener en cuenta el último fotograma.

También le permite ignorar un fotograma, si está haciendo algo que sabe que arruinará artificialmente el tiempo de ese fotograma.

También le permite cambiar la cantidad de fotogramas para almacenar en la cola mientras se ejecuta, para que pueda probar sobre la marcha cuál es el mejor valor para usted.

// 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);
}
Petrucio
fuente
2

Buenas respuestas aquí. La forma en que lo implemente depende de para qué lo necesite. Yo mismo prefiero el promedio corriente "tiempo = tiempo * 0.9 + last_frame * 0.1" por el tipo de arriba.

sin embargo, personalmente me gusta ponderar mi promedio más hacia los datos más nuevos porque en un juego los SPIKES son los más difíciles de aplastar y, por lo tanto, de mayor interés para mí. Entonces, usaría algo más como una división de .7 \ .3 que hará que un pico se muestre mucho más rápido (aunque su efecto también desaparecerá de la pantalla más rápido ... ver más abajo)

Si su enfoque está en el tiempo de RENDERIZACIÓN, entonces la división .9.1 funciona bastante bien porque tiende a ser más suave. Aunque los picos de jugabilidad / IA / física son mucho más preocupantes, ya que ESO suele ser lo que hace que tu juego se vea entrecortado (que a menudo es peor que una velocidad de fotogramas baja asumiendo que no estamos por debajo de 20 fps)

Entonces, lo que haría también es agregar algo como esto:

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

(Complete 3.0f con cualquier magnitud que encuentre que es un pico inaceptable) Esto le permitirá encontrar y, por lo tanto, resolver los problemas de FPS al final del cuadro en el que ocurren.

David Frenkel
fuente
Me gusta el time = time * 0.9 + last_frame * 0.1cálculo promedio que hace que la pantalla cambie sin problemas.
Fabien Quatravaux
2

Un sistema mucho mejor que usar una gran variedad de framerates antiguos es simplemente hacer algo como esto:

new_fps = old_fps * 0.99 + new_fps * 0.01

Este método utiliza mucha menos memoria, requiere mucho menos código y da más importancia a las tasas de fotogramas recientes que a las antiguas, al mismo tiempo que suaviza los efectos de los cambios repentinos de velocidad de fotogramas.

Barry Smith
fuente
1

Puede mantener un contador, incrementarlo después de que se procese cada fotograma y luego restablecer el contador cuando esté en un segundo nuevo (almacenando el valor anterior como el número de fotogramas renderizados del último segundo)

Mike Stone
fuente
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));
Ephellon Dantzler
fuente
1

Aquí hay un ejemplo completo, usando Python (pero fácilmente adaptado a cualquier idioma). Utiliza la ecuación de suavizado en la respuesta de Martin, por lo que casi no hay sobrecarga de memoria, y elegí valores que funcionaron para mí (siéntase libre de jugar con las constantes para adaptarse a su 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)
jd20
fuente
0

Ponga el contador a cero. Cada vez que dibuje un cuadro, incremente el contador. Después de cada segundo imprima el contador. hacer espuma, enjuagar, repetir. Si desea crédito adicional, mantenga un contador activo y divida por el número total de segundos para un promedio móvil.

Bryan Oakley
fuente
0

En pseudocódigo (como c ++), estos dos son los que usé en aplicaciones de procesamiento de imágenes industriales que tenían que procesar imágenes de un conjunto de cámaras activadas externamente. Las variaciones en la "velocidad de fotogramas" tuvieron una fuente diferente (producción más lenta o más rápida en la cinta) pero el problema es el mismo. (Supongo que tiene una llamada simple a timer.peek () que le da algo como el nr de msec (nsec?) Desde el inicio de la aplicación o la última llamada)

Solución 1: rápido pero no actualizado en cada fotograma

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

Solución 2: actualizado cada cuadro, requiere más memoria y 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
   }
} 
jilles de wit
fuente
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
fuente
0

¡Cómo lo hago!

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

En palabras, un reloj marca los pasos. Si es la primera vez, toma la hora actual y la pone en 'tickstart'. Después del primer tick, hace que la variable 'fps' sea igual al número de tics del reloj de tick dividido por el tiempo menos el tiempo del primer tick.

Fps es un número entero, por lo tanto, "(int)".

BottleFact
fuente
1
No lo recomendaría a nadie. Dividir el número total de tics por el número total de segundos hace que el FPS se acerque a algo así como un límite matemático, donde básicamente se establece en 2-3 valores después de un tiempo prolongado y muestra resultados inexactos.
Kartik Chugh
0

Así es como lo hago (en 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
}
aventureroOK
fuente
0

En TypeScript, uso este algoritmo para calcular los promedios de framerate y 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';

Consejo: si samples es 1, el resultado es la velocidad de fotogramas y el tiempo de fotogramas en tiempo real.

Donavan Carvalho
fuente
0

Esto se basa en la respuesta de KPexEA y proporciona el promedio móvil simple. Ordenado y convertido a TypeScript para copiar y pegar fácilmente:

Declaración de variable:

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

Función:

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 (puede variar en su aplicación):

this.fps = this.calculateFps(this.ticker.FPS)
danday74
fuente
-1

almacenar una hora de inicio e incrementar su contador de fotogramas una vez por ciclo? cada pocos segundos puede imprimir framecount / (ahora - hora de inicio) y luego reinicializarlos.

editar: oops. doble ninja

Palanqueta
fuente