Calcul des images par seconde dans un jeu


110

Qu'est-ce qu'un bon algorithme pour calculer les images par seconde dans un jeu? Je veux l'afficher sous forme de nombre dans le coin de l'écran. Si je regarde simplement combien de temps il a fallu pour rendre la dernière image, le nombre change trop rapidement.

Des points bonus si votre réponse met à jour chaque image et ne converge pas différemment lorsque la fréquence d'images augmente ou diminue.

Réponses:


100

Vous avez besoin d'une moyenne lissée, le moyen le plus simple est de prendre la réponse actuelle (le temps de dessiner la dernière image) et de la combiner avec la réponse précédente.

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

En ajustant le rapport 0,9 / 0,1, vous pouvez modifier la «constante de temps», c'est-à-dire à quelle vitesse le nombre répond aux changements. Une plus grande fraction en faveur de l'ancienne réponse donne un changement plus lent et plus fluide, une grande fraction en faveur de la nouvelle réponse donne une valeur qui change plus rapidement. Évidemment, les deux facteurs doivent s'ajouter à un!


14
Ensuite, pour une sécurité et une propreté irréprochables, vous voudrez probablement quelque chose comme float weightRatio = 0.1; et time = time * (1.0 - weightRatio) + last_frame * weightRatio
korona

2
Cela semble bon et simple en principe, mais en réalité, le lissage de cette approche est à peine perceptible. Pas bien.
Petrucio

1
@Petrucio si le lissage est trop faible, il suffit d'augmenter la constante de temps (weightRatio = 0,05, 0,02, 0,01 ...)
John Dvorak

8
@Petrucio: last_framene signifie pas (ou du moins ne devrait pas signifier) ​​la durée de l'image précédente; cela devrait signifier la valeur timeque vous avez calculée pour la dernière image. De cette façon, toutes les images précédentes seront incluses, les images les plus récentes étant les plus pondérées.
j_random_hacker

1
Le nom de variable "last_frame" est trompeur. "current_frame" serait plus descriptif. Il est également important de savoir que la variable «temps» dans l'exemple doit être globale (c'est-à-dire conserver sa valeur sur toutes les images) et idéalement un nombre à virgule flottante. Il contient la valeur moyenne / agrégée et est mis à jour sur chaque image. Plus le ratio est élevé, plus il faudra de temps pour régler la variable «temps» vers une valeur (ou la dériver de celle-ci).
jox

52

C'est ce que j'ai utilisé dans de nombreux jeux.

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

J'aime beaucoup cette approche. Une raison spécifique pour laquelle vous définissez MAXSAMPLES sur 100?
Zolomon

1
MAXSAMPLES ici est le nombre de valeurs qui sont moyennées afin de trouver une valeur pour les fps.
Cory Gross

8
C'est une moyenne mobile simple (SMA)
KindDragon

Parfait merci! Je l'ai modifié dans mon jeu pour que la fonction tick soit vide et qu'une autre fonction renvoie le FPS, puis je peux exécuter la principale à chaque tick, même si le code de rendu n'a pas de FPS affiché.
TheJosh

2
Veuillez utiliser modulo et non un if. tickindex = (tickindex + 1) % MAXSAMPLES;
Felix K.

25

Eh bien, certainement

frames / sec = 1 / (sec / frame)

Mais, comme vous le faites remarquer, il y a beaucoup de variations dans le temps nécessaire pour rendre une seule image, et du point de vue de l'interface utilisateur, la mise à jour de la valeur fps à la fréquence d'images n'est pas du tout utilisable (à moins que le nombre ne soit très stable).

Ce que vous voulez, c'est probablement une moyenne mobile ou une sorte de compteur de regroupement / réinitialisation.

Par exemple, vous pouvez conserver une structure de données de file d'attente qui contient les temps de rendu pour chacune des 30, 60, 100 dernières images (vous pouvez même la concevoir de manière à ce que la limite soit ajustable au moment de l'exécution). Pour déterminer une approximation de fps décente, vous pouvez déterminer le fps moyen à partir de tous les temps de rendu dans la file d'attente:

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

Lorsque vous avez terminé le rendu d'une nouvelle image, vous mettez en file d'attente une nouvelle heure de rendu et retirez une ancienne heure de rendu de la file d'attente. Alternativement, vous ne pouvez sortir de la file d'attente que lorsque le total des temps de rendu dépasse une certaine valeur prédéfinie (par exemple 1 s). Vous pouvez conserver la "dernière valeur fps" et un horodatage de la dernière mise à jour afin de pouvoir déclencher le moment de la mise à jour du chiffre fps, si vous le souhaitez. Bien qu'avec une moyenne mobile si vous avez un formatage cohérent, l'impression de la "moyenne instantanée" fps sur chaque image serait probablement acceptable.

Une autre méthode serait d'avoir un compteur de réinitialisation. Conservez un horodatage précis (en millisecondes), un compteur d'images et une valeur fps. Lorsque vous avez terminé le rendu d'une image, incrémentez le compteur. Lorsque le compteur atteint une limite prédéfinie (par exemple 100 images) ou lorsque le temps écoulé depuis l'horodatage a dépassé une valeur prédéfinie (par exemple 1 seconde), calculez le fps:

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

Ensuite, remettez le compteur à 0 et réglez l'horodatage sur l'heure actuelle.


12

Incrémentez un compteur chaque fois que vous effectuez le rendu d'un écran et effacez ce compteur pendant un certain intervalle de temps sur lequel vous souhaitez mesurer la fréquence d'images.

C'est à dire. Toutes les 3 secondes, obtenez le compteur / 3, puis effacez le compteur.


+1 Bien que cela ne vous donne une nouvelle valeur que dans les intervalles, cela est facile à comprendre et ne nécessite ni tableaux ni valeurs supposées et est scientifiquement correct.
opatut

10

Il existe au moins deux façons de procéder:


Le premier est celui que d'autres ont mentionné ici avant moi. Je pense que c'est le moyen le plus simple et préféré. Vous juste pour garder une trace de

  • cn: compteur du nombre d'images que vous avez rendues
  • time_start: le temps depuis que vous avez commencé à compter
  • time_now: l'heure actuelle

Calculer les fps dans ce cas est aussi simple que d'évaluer cette formule:

  • FPS = cn / (time_now - time_start).

Ensuite, il y a la manière super cool que vous aimeriez utiliser un jour:

Disons que vous avez des cadres «i» à considérer. J'utiliserai cette notation: f [0], f [1], ..., f [i-1] pour décrire combien de temps il a fallu pour rendre l'image 0, l'image 1, ..., l'image (i-1 ) respectivement.

Example where i = 3

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

Ensuite, la définition mathématique de fps après i images serait

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

Et la même formule mais en ne considérant que les cadres i-1.

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

Maintenant, l'astuce ici est de modifier le côté droit de la formule (1) de telle sorte qu'il contienne le côté droit de la formule (2) et de le remplacer par le côté gauche.

Comme ça (vous devriez le voir plus clairement si vous l'écrivez sur un papier):

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)

Donc, selon cette formule (mes compétences en calcul mathématique sont un peu rouillées cependant), pour calculer les nouveaux fps, vous devez connaître les fps de l'image précédente, la durée qu'il a fallu pour rendre la dernière image et le nombre d'images que vous avez rendu.


1
+1 pour la deuxième méthode. J'imagine que ce serait bon pour un calcul
ultra

5

Cela pourrait être exagéré pour la plupart des gens, c'est pourquoi je ne l'avais pas publié lorsque je l'ai implémenté. Mais c'est très robuste et flexible.

Il stocke une file d'attente avec les temps de la dernière image, de sorte qu'il peut calculer avec précision une valeur FPS moyenne bien mieux que de simplement prendre la dernière image en considération.

Cela vous permet également d'ignorer une image, si vous faites quelque chose dont vous savez qu'elle va artificiellement gâcher le temps de cette image.

Il vous permet également de modifier le nombre d'images à stocker dans la file d'attente lors de son exécution, afin que vous puissiez la tester à la volée quelle est la meilleure valeur pour vous.

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

Bonnes réponses ici. La façon dont vous le mettez en œuvre dépend de ce dont vous en avez besoin. Je préfère la moyenne courante moi-même "time = time * 0.9 + last_frame * 0.1" par le gars ci-dessus.

Cependant, j'aime personnellement pondérer davantage ma moyenne en fonction de données plus récentes car dans un jeu, ce sont les SPIKES qui sont les plus difficiles à écraser et donc qui m'intéressent le plus. Donc, j'utiliserais quelque chose de plus comme une division .7 \ .3 fera apparaître un pic beaucoup plus rapidement (bien que son effet disparaisse également plus rapidement hors de l'écran ... voir ci-dessous)

Si vous vous concentrez sur le temps de rendu, alors la division .9.1 fonctionne plutôt bien car elle a tendance à être plus fluide. Les pics de gameplay / IA / physique sont beaucoup plus préoccupants car CELA sera généralement ce qui rend votre jeu saccadé (ce qui est souvent pire qu'une faible fréquence d'images en supposant que nous ne descendons pas en dessous de 20 ips)

Donc, ce que je ferais, c'est aussi ajouter quelque chose comme ceci:

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

(Remplissez 3.0f avec la grandeur que vous trouvez être un pic inacceptable) Cela vous permettra de trouver et donc de résoudre les problèmes de FPS à la fin de l'image où ils se produisent.


J'aime le time = time * 0.9 + last_frame * 0.1calcul moyen qui fait que l'affichage change en douceur.
Fabien Quatravaux

2

Un système bien meilleur que d'utiliser un large éventail d'anciens framerates consiste simplement à faire quelque chose comme ceci:

new_fps = old_fps * 0.99 + new_fps * 0.01

Cette méthode utilise beaucoup moins de mémoire, nécessite beaucoup moins de code et accorde plus d'importance aux fréquences d'images récentes qu'aux anciennes fréquences d'images tout en lissant les effets des changements soudains de fréquence d'images.


1

Vous pouvez conserver un compteur, l'incrémenter après le rendu de chaque image, puis réinitialiser le compteur lorsque vous êtes sur une nouvelle seconde (en stockant la valeur précédente comme le nombre d'images de la dernière seconde rendus)


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

Voici un exemple complet, utilisant Python (mais facilement adapté à n'importe quel langage). Il utilise l'équation de lissage dans la réponse de Martin, donc presque pas de surcharge de mémoire, et j'ai choisi des valeurs qui ont fonctionné pour moi (n'hésitez pas à jouer avec les constantes pour vous adapter à votre cas d'utilisation).

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

Mettez le compteur à zéro. Chaque fois que vous dessinez une image, augmentez le compteur. Après chaque seconde, imprimez le compteur. faire mousser, rincer, répéter. Si vous voulez un crédit supplémentaire, gardez un compteur en cours et divisez par le nombre total de secondes pour une moyenne en cours.


0

Dans le pseudocode (de type C ++), ces deux éléments sont ce que j'ai utilisé dans les applications de traitement d'images industrielles qui devaient traiter les images d'un ensemble de caméras déclenchées de l'extérieur. Les variations de "frame rate" avaient une source différente (production plus lente ou plus rapide sur la bande) mais le problème est le même. (Je suppose que vous avez un simple appel timer.peek () qui vous donne quelque chose comme le nr de msec (nsec?) Depuis le démarrage de l'application ou le dernier appel)

Solution 1: rapide mais pas mis à jour à chaque image

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

Solution 2: mise à jour à chaque image, nécessite plus de mémoire et de processeur

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

Comment je le fais!

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 mots, une horloge de ticks suit les ticks. Si c'est la première fois, il prend l'heure actuelle et la met dans 'tickstart'. Après le premier tick, cela rend la variable «fps» égale au nombre de ticks de l'horloge tick divisé par le temps moins le temps du premier tick.

Fps est un entier, d'où "(int)".


1
Je ne recommanderais à personne. En divisant le nombre total de graduations par le nombre total de secondes, le FPS approche quelque chose comme une limite mathématique, où il se fixe essentiellement sur 2-3 valeurs après une longue période et affiche des résultats inexacts.
Kartik Chugh

0

Voici comment je fais (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
}

0

Dans Typescript, j'utilise cet algorithme pour calculer les moyennes du framerate et du 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);
}

usage:

statsUpdate();

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

Conseil: si samples vaut 1, le résultat est la fréquence d'images et la durée d'images en temps réel.


0

Ceci est basé sur la réponse de KPexEA et donne la moyenne mobile simple. Rangé et converti en TypeScript pour un copier-coller facile:

Déclaration de variable:

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

Fonction:

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

Utilisation (peut varier dans votre application):

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

-1

stocker une heure de début et incrémenter votre compteur d'images une fois par boucle? toutes les quelques secondes, vous pouvez simplement imprimer framecount / (Now - starttime) puis les réinitialiser.

modifier: oups. double ninja'ed

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.