Berechnung von Bildern pro Sekunde in einem Spiel


111

Was ist ein guter Algorithmus zur Berechnung von Bildern pro Sekunde in einem Spiel? Ich möchte es als Zahl in der Ecke des Bildschirms anzeigen. Wenn ich mir nur ansehe, wie lange es gedauert hat, den letzten Frame zu rendern, ändert sich die Zahl zu schnell.

Bonuspunkte, wenn Ihre Antwort jedes Bild aktualisiert und nicht anders konvergiert, wenn die Bildrate steigt oder sinkt.

Antworten:


101

Sie benötigen einen geglätteten Durchschnitt. Am einfachsten ist es, die aktuelle Antwort (die Zeit zum Zeichnen des letzten Frames) mit der vorherigen Antwort zu kombinieren.

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

Durch Anpassen des Verhältnisses 0,9 / 0,1 können Sie die 'Zeitkonstante' ändern - so schnell reagiert die Zahl auf Änderungen. Ein größerer Bruchteil zugunsten der alten Antwort führt zu einer langsameren, reibungsloseren Änderung, ein großer Bruchteil zugunsten der neuen Antwort zu einem schnelleren Wertwechsel. Offensichtlich müssen die beiden Faktoren zu einem addieren!


14
Dann würden Sie für Narrensicherheit und Ordnung wahrscheinlich so etwas wie float weightRatio = 0.1 wollen; und Zeit = Zeit * (1.0 - weightRatio) + last_frame * weightRatio
korona

2
Klingt im Prinzip gut und einfach, aber in Wirklichkeit ist die Glättung dieses Ansatzes kaum spürbar. Nicht gut.
Petrucio

1
@Petrucio Wenn die Glättung zu niedrig ist, drehen Sie einfach die Zeitkonstante auf (weightRatio = 0,05, 0,02, 0,01 ...)
John Dvorak

8
@Petrucio: last_framebedeutet nicht (oder sollte zumindest nicht bedeuten) die Dauer des vorherigen Frames; Es sollte den Wert bedeuten, den timeSie für den letzten Frame berechnet haben. Auf diese Weise werden alle vorherigen Frames eingeschlossen, wobei die neuesten Frames am stärksten gewichtet werden.
j_random_hacker

1
Der Variablenname "last_frame" ist irreführend. "current_frame" wäre aussagekräftiger. Es ist auch wichtig zu wissen, dass die Variable "Zeit" im Beispiel global sein muss (dh ihren Wert über alle Frames hinweg beibehalten muss) und im Idealfall eine Gleitkommazahl sein muss. Es enthält den Durchschnitts- / Gesamtwert und wird in jedem Frame aktualisiert. Je höher das Verhältnis, desto länger dauert es, bis die Variable "Zeit" auf einen Wert eingestellt ist (oder von diesem weggedrängt wird).
Jox

52

Das habe ich in vielen Spielen verwendet.

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

Ich mag diesen Ansatz sehr. Gibt es einen bestimmten Grund, warum Sie MAXSAMPLES auf 100 setzen?
Zolomon

1
MAXSAMPLES ist hier die Anzahl der Werte, die gemittelt werden, um einen Wert für fps zu erhalten.
Cory Gross

8
Es ist einfacher gleitender Durchschnitt (SMA)
KindDragon

Perfekt danke! Ich habe es in meinem Spiel so angepasst, dass die Tick-Funktion ungültig ist und eine andere Funktion die FPS zurückgibt. Dann kann ich die Hauptfunktion für jeden Tick ausführen, auch wenn im Render-Code keine FPS angezeigt werden.
TheJosh

2
Bitte verwenden Sie Modulo und kein Wenn. tickindex = (tickindex + 1) % MAXSAMPLES;
Felix K.

25

Na klar

frames / sec = 1 / (sec / frame)

Wie Sie bereits betont haben, gibt es große Unterschiede in der Zeit, die zum Rendern eines einzelnen Frames benötigt wird, und aus Sicht der Benutzeroberfläche kann die Aktualisierung des fps-Werts mit der Framerate überhaupt nicht verwendet werden (es sei denn, die Anzahl ist sehr stabil).

Was Sie wollen, ist wahrscheinlich ein gleitender Durchschnitt oder eine Art Binning / Reset-Zähler.

Sie können beispielsweise eine Warteschlangendatenstruktur verwalten, die die Renderzeiten für jeden der letzten 30, 60, 100 oder What-Have-You-Frames enthält (Sie können sie sogar so gestalten, dass das Limit zur Laufzeit einstellbar ist). Um eine anständige fps-Näherung zu ermitteln, können Sie die durchschnittlichen fps aus allen Renderzeiten in der Warteschlange ermitteln:

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

Wenn Sie mit dem Rendern eines neuen Frames fertig sind, stellen Sie eine neue Renderzeit in die Warteschlange und eine alte Renderzeit aus der Warteschlange. Alternativ können Sie die Warteschlange nur dann deaktivieren, wenn die Summe der Renderzeiten einen voreingestellten Wert (z. B. 1 Sek.) Überschreitet. Sie können den "letzten fps-Wert" und einen zuletzt aktualisierten Zeitstempel beibehalten, um auf Wunsch auszulösen, wann die fps-Zahl aktualisiert werden soll. Obwohl bei einem gleitenden Durchschnitt, wenn Sie eine konsistente Formatierung haben, das Drucken der "momentanen Durchschnitts" -Fps auf jedem Frame wahrscheinlich in Ordnung wäre.

Eine andere Methode wäre ein Rücksetzzähler. Behalten Sie einen genauen Zeitstempel (Millisekunden), einen Bildzähler und einen fps-Wert bei. Wenn Sie mit dem Rendern eines Frames fertig sind, erhöhen Sie den Zähler. Wenn der Zähler ein voreingestelltes Limit erreicht (z. B. 100 Frames) oder wenn die Zeit seit dem Überschreiten eines voreingestellten Werts des Zeitstempels (z. B. 1 Sek.) Die fps berechnet hat:

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

Setzen Sie dann den Zähler auf 0 zurück und setzen Sie den Zeitstempel auf die aktuelle Zeit.


12

Erhöhen Sie einen Zähler jedes Mal, wenn Sie einen Bildschirm rendern, und löschen Sie diesen Zähler für ein Zeitintervall, über das Sie die Bildrate messen möchten.

Dh. Holen Sie sich alle 3 Sekunden Zähler / 3 und löschen Sie den Zähler.


+1 Obwohl dies nur in Intervallen einen neuen Wert ergibt, ist dies leicht zu verstehen und erfordert weder Arrays noch Schätzwerte und ist wissenschaftlich korrekt.
Opatut

10

Es gibt mindestens zwei Möglichkeiten:


Das erste ist das, was andere hier vor mir erwähnt haben. Ich denke, es ist der einfachste und bevorzugte Weg. Sie nur um den Überblick zu behalten

  • cn: Zähler für die Anzahl der gerenderten Frames
  • time_start: Die Zeit, seit Sie mit dem Zählen begonnen haben
  • time_now: die aktuelle Zeit

Die Berechnung der fps ist in diesem Fall so einfach wie die Auswertung dieser Formel:

  • FPS = cn / (time_now - time_start).

Dann gibt es die überaus coole Art, die Sie eines Tages nutzen möchten:

Angenommen, Sie müssen 'i'-Frames berücksichtigen. Ich werde diese Notation verwenden: f [0], f [1], ..., f [i-1], um zu beschreiben, wie lange es gedauert hat, Frame 0, Frame 1, ..., Frame (i-1) zu rendern ) beziehungsweise.

Example where i = 3

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

Dann wäre die mathematische Definition von fps nach i Frames

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

Und die gleiche Formel, aber nur unter Berücksichtigung von i-1-Frames.

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

Der Trick hier besteht nun darin, die rechte Seite der Formel (1) so zu ändern, dass sie die rechte Seite der Formel (2) enthält, und sie durch die linke Seite zu ersetzen.

So (Sie sollten es klarer sehen, wenn Sie es auf Papier schreiben):

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)

Nach dieser Formel (meine mathematischen Fähigkeiten sind allerdings etwas verrostet) müssen Sie zur Berechnung der neuen fps die fps aus dem vorherigen Frame, die Dauer des Renderns des letzten Frames und die Anzahl der Frames kennen gerendert.


1
+1 für die zweite Methode. Ich stelle mir vor, es wäre gut für eine überpräzise Berechnung: 3
Zeboidlund

5

Dies könnte für die meisten Leute übertrieben sein, deshalb hatte ich es bei der Implementierung nicht gepostet. Aber es ist sehr robust und flexibel.

Es speichert eine Warteschlange mit den letzten Frame-Zeiten, sodass ein durchschnittlicher FPS-Wert viel besser genau berechnet werden kann als nur der letzte Frame.

Sie können damit auch einen Frame ignorieren, wenn Sie etwas tun, von dem Sie wissen, dass es die Zeit dieses Frames künstlich verkürzt.

Außerdem können Sie die Anzahl der Frames ändern, die während der Ausführung in der Warteschlange gespeichert werden sollen, damit Sie im Handumdrehen testen können, welcher Wert für Sie am besten geeignet ist.

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

Gute Antworten hier. Wie Sie es implementieren, hängt davon ab, wofür Sie es benötigen. Ich bevorzuge den laufenden Durchschnitt selbst "time = time * 0.9 + last_frame * 0.1" von dem Typ oben.

Ich persönlich mag es jedoch, meinen Durchschnitt stärker auf neuere Daten auszurichten, da es in einem Spiel SPIKES sind, die am schwersten zu quetschen sind und daher für mich am interessantesten sind. Ich würde also eher einen .7 \ .3-Split verwenden, bei dem ein Spike viel schneller angezeigt wird (obwohl der Effekt auch schneller vom Bildschirm abfällt. Siehe unten).

Wenn Sie sich auf die RENDERING-Zeit konzentrieren, funktioniert die .9.1-Aufteilung ziemlich gut, da sie in der Regel flüssiger ist. Für Gameplay / KI / Physik sind Spikes jedoch viel besorgniserregender, da dies normalerweise dazu führt, dass Ihr Spiel abgehackt aussieht (was oft schlechter ist als eine niedrige Bildrate, vorausgesetzt, wir fallen nicht unter 20 fps).

Also würde ich auch so etwas hinzufügen:

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

(Füllen Sie 3.0f mit der Größe aus, die Sie für eine inakzeptable Spitze halten.) Auf diese Weise können Sie FPS-Probleme am Ende des Rahmens finden und lösen .


Ich mag die time = time * 0.9 + last_frame * 0.1durchschnittliche Berechnung, mit der sich die Anzeige reibungslos ändert.
Fabien Quatravaux

2

Ein viel besseres System als die Verwendung einer großen Anzahl alter Frameraten besteht darin, einfach Folgendes zu tun:

new_fps = old_fps * 0.99 + new_fps * 0.01

Diese Methode benötigt viel weniger Speicher, benötigt viel weniger Code und legt mehr Wert auf aktuelle Frameraten als auf alte Frameraten, während die Auswirkungen plötzlicher Frameratenänderungen immer noch geglättet werden.


1

Sie können einen Zähler beibehalten, ihn nach dem Rendern jedes Frames erhöhen und dann den Zähler zurücksetzen, wenn Sie sich in einer neuen Sekunde befinden (wobei der vorherige Wert als Anzahl der gerenderten Frames der letzten Sekunde gespeichert wird).


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

Hier ist ein vollständiges Beispiel mit Python (das jedoch leicht an jede Sprache angepasst werden kann). Es verwendet die Glättungsgleichung in Martins Antwort, also fast keinen Speicheraufwand, und ich habe Werte ausgewählt, die für mich funktionieren (Sie können gerne mit den Konstanten herumspielen, um sie an Ihren Anwendungsfall anzupassen).

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

Zähler auf Null setzen. Jedes Mal, wenn Sie einen Frame zeichnen, erhöhen Sie den Zähler. Drucken Sie nach jeder Sekunde den Zähler aus. einschäumen, ausspülen, wiederholen. Wenn Sie zusätzliches Guthaben wünschen, behalten Sie einen laufenden Zähler und dividieren Sie durch die Gesamtzahl der Sekunden für einen laufenden Durchschnitt.


0

In (c ++ like) Pseudocode habe ich diese beiden in industriellen Bildverarbeitungsanwendungen verwendet, die Bilder von einer Reihe von extern ausgelösten Kameras verarbeiten mussten. Variationen in der "Bildrate" hatten eine andere Quelle (langsamere oder schnellere Produktion am Band), aber das Problem ist das gleiche. (Ich gehe davon aus, dass Sie einen einfachen Aufruf von timer.peek () haben, der Ihnen seit dem Start der Anwendung oder dem letzten Aufruf so etwas wie die Nr. Von ms (ns?) Gibt.)

Lösung 1: schnell, aber nicht bei jedem Frame aktualisiert

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

Lösung 2: Aktualisiert jeden Frame, benötigt mehr Speicher und 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

Wie ich es mache!

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

In Worten, eine Tick-Uhr verfolgt Ticks. Wenn es das erste Mal ist, nimmt es die aktuelle Zeit und setzt sie in "Tickstart". Nach dem ersten Tick entspricht die Variable 'fps' der Anzahl der Ticks der Tick-Uhr geteilt durch die Zeit minus der Zeit des ersten Ticks.

Fps ist eine ganze Zahl, daher "(int)".


1
Würde es niemandem empfehlen. Durch Teilen der Gesamtzahl der Ticks durch die Gesamtzahl der Sekunden nähert sich der FPS einer mathematischen Grenze, bei der er sich nach langer Zeit auf 2-3 Werte einstellt und ungenaue Ergebnisse anzeigt.
Kartik Chugh

0

So mache ich das (in 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

In Typescript verwende ich diesen Algorithmus, um Framerate- und Frametime-Durchschnittswerte zu berechnen:

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

Verwendung:

statsUpdate();

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

Tipp: Wenn Samples 1 ist, ist das Ergebnis Echtzeit-Framerate und Frametime.


-1

eine Startzeit speichern und Ihren Framecounter einmal pro Schleife erhöhen? Alle paar Sekunden können Sie einfach Framecount / (Jetzt - Startzeit) drucken und dann neu initialisieren.

edit: oops. Doppel-Ninja'ed

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.