Obliczanie klatek na sekundę w grze


110

Jaki jest dobry algorytm obliczania liczby klatek na sekundę w grze? Chcę pokazać to jako liczbę w rogu ekranu. Jeśli spojrzę tylko na to, ile czasu zajęło renderowanie ostatniej klatki, liczba zmienia się zbyt szybko.

Dodatkowe punkty, jeśli twoja odpowiedź aktualizuje każdą klatkę i nie zbiegają się inaczej, gdy liczba klatek rośnie, a spada.

Odpowiedzi:


100

Potrzebujesz wygładzonej średniej, najprościej jest wziąć bieżącą odpowiedź (czas na narysowanie ostatniej klatki) i połączyć ją z poprzednią odpowiedzią.

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

Dostosowując stosunek 0,9 / 0,1 można zmienić „stałą czasową” - czyli jak szybko liczba reaguje na zmiany. Większa część na korzyść starej odpowiedzi daje wolniejszą, płynniejszą zmianę, a duża część na korzyść nowej odpowiedzi daje szybciej zmieniającą się wartość. Oczywiście te dwa czynniki muszą dodać do jednego!


14
W takim razie, aby zachować bezpieczeństwo i porządek, prawdopodobnie chciałbyś czegoś takiego jak float weightRatio = 0,1; and time = time * (1.0 - weightRatio) + last_frame * weightRatio
korona

2
W zasadzie brzmi dobrze i prosto, ale w rzeczywistości wygładzanie tego podejścia jest ledwo zauważalne. Nie dobrze.
Petrucio,

1
@Petrucio, jeśli wygładzanie jest zbyt niskie, po prostu podkręć stałą czasową (weightRatio = 0,05, 0,02, 0,01 ...)
John Dvorak

8
@Petrucio: last_framenie oznacza (a przynajmniej nie powinno oznaczać) czasu trwania poprzedniej klatki; Powinien oznaczać wartość time, którą obliczyłeś dla ostatniej klatki. W ten sposób zostaną uwzględnione wszystkie poprzednie klatki, przy czym najnowsze klatki mają największą wagę.
j_random_hacker

1
Nazwa zmiennej „last_frame” jest myląca. Bardziej opisowy byłby termin „current_frame”. Ważne jest również, aby wiedzieć, że zmienna „czas” w przykładzie musi być globalna (tj. Utrzymywać jej wartość we wszystkich ramkach), a najlepiej, aby była liczbą zmiennoprzecinkową. Zawiera średnią / zagregowaną wartość i aktualizuje dane w każdej ramce. Im wyższy współczynnik, tym dłużej zajmie ustalenie zmiennej „czas” w kierunku wartości (lub odejście od niej).
jox

52

Tego właśnie używałem w wielu grach.

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

Bardzo podoba mi się to podejście. Czy jest jakiś konkretny powód, dla którego ustawiłeś MAXSAMPLES na 100?
Zolomon

1
MAXSAMPLES to liczba wartości, które są uśredniane w celu uzyskania wartości fps.
Cory Gross,

8
To prosta średnia krocząca (SMA)
KindDragon

Perfekcyjnie, dzięki! Poprawiłem go w mojej grze, aby funkcja zaznaczania była nieważna, a inna funkcja zwraca FPS, a następnie mogę uruchomić główny z każdym tikiem, nawet jeśli kod renderowania nie ma pokazanego FPS.
TheJosh

2
Proszę użyć modulo, a nie if. tickindex = (tickindex + 1) % MAXSAMPLES;
Felix K.

25

Cóż, na pewno

frames / sec = 1 / (sec / frame)

Ale, jak zauważyłeś, czas potrzebny do wyrenderowania pojedynczej klatki jest bardzo zróżnicowany, az perspektywy interfejsu użytkownika aktualizacja wartości klatek na sekundę w ogóle nie nadaje się do użytku (chyba że liczba jest bardzo stabilna).

To, czego chcesz, to prawdopodobnie średnia ruchoma lub jakiś rodzaj binningu / resetowania licznika.

Na przykład, możesz zachować strukturę danych kolejki, która zawiera czasy renderowania dla każdej z ostatnich 30, 60, 100 lub ramek typu „co masz” (możesz nawet zaprojektować ją tak, aby limit był regulowany w czasie wykonywania). Aby określić przyzwoite przybliżenie liczby klatek na sekundę, możesz określić średnią liczbę klatek na sekundę ze wszystkich czasów renderowania w kolejce:

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

Po zakończeniu renderowania nowej klatki kolejkujesz nowy czas renderowania i usuwasz z kolejki stary czas renderowania. Alternatywnie, możesz usunąć z kolejki tylko wtedy, gdy suma czasów renderowania przekroczyła określoną wartość (np. 1 sek.). Możesz zachować "ostatnią wartość fps" i ostatnią aktualizowaną sygnaturę czasową, aby móc wyzwolić, kiedy zaktualizować liczbę fps, jeśli chcesz. Chociaż przy średniej ruchomej, jeśli masz spójne formatowanie, drukowanie „chwilowej średniej” fps dla każdej klatki prawdopodobnie byłoby w porządku.

Inną metodą byłoby posiadanie resetującego licznika. Utrzymuj dokładny (milisekundowy) znacznik czasu, licznik klatek i wartość fps. Po zakończeniu renderowania klatki zwiększ licznik. Kiedy licznik osiągnie ustalony limit (np. 100 klatek) lub gdy czas od znacznika czasu przekroczył określoną wartość (np. 1 sek.), Oblicz fps:

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

Następnie zresetuj licznik do 0 i ustaw znacznik czasu na bieżący czas.


12

Zwiększaj licznik za każdym razem, gdy renderujesz ekran i czyść go przez pewien przedział czasu, w którym chcesz zmierzyć liczbę klatek na sekundę.

To znaczy. Co 3 sekundy zdobądź licznik / 3, a następnie wyczyść licznik.


+1 Chociaż da ci to nową wartość tylko w interwałach, jest to łatwe do zrozumienia i nie wymaga ani tablic, ani zgadywania wartości i jest naukowo poprawne.
opatut

10

Można to zrobić na co najmniej dwa sposoby:


Pierwsza to ta, o której inni wspomnieli wcześniej. Myślę, że to najprostszy i preferowany sposób. Musisz tylko śledzić

  • cn: licznik liczby wyrenderowanych klatek
  • time_start: czas od rozpoczęcia liczenia
  • time_now: aktualny czas

Obliczenie fps w tym przypadku jest tak proste, jak oszacowanie tego wzoru:

  • FPS = cn / (time_now - time_start).

Jest też super fajny sposób, z którego możesz skorzystać pewnego dnia:

Powiedzmy, że masz do rozważenia ramki typu „i”. Użyję tego zapisu: f [0], f [1], ..., f [i-1], aby opisać, jak długo trwało renderowanie klatki 0, klatki 1, ..., klatki (i-1 ) odpowiednio.

Example where i = 3

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

Wtedy matematyczna definicja fps po i klatkach byłaby

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

I ta sama formuła, ale biorąc pod uwagę tylko klatki i-1.

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

Teraz sztuczka polega na zmodyfikowaniu prawej strony wzoru (1) w taki sposób, aby zawierała prawą stronę wzoru (2) i zastąpiła ją lewą stroną.

Tak (powinieneś zobaczyć to wyraźniej, jeśli napiszesz to na papierze):

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)

Więc zgodnie z tym wzorem (chociaż moje umiejętności matematyczne są trochę zardzewiałe), aby obliczyć nowe fps, musisz znać fps z poprzedniej klatki, czas trwania renderowania ostatniej klatki i liczbę klatek, które renderowane.


1
+1 za drugą metodę. Wyobrażam sobie, że byłoby dobrze do precyzyjnych obliczeń
ubera

5

To może być przesada dla większości ludzi, dlatego nie opublikowałem tego, kiedy go wdrażałem. Ale jest bardzo wytrzymały i elastyczny.

Przechowuje kolejkę z czasami ostatniej klatki, dzięki czemu może dokładnie obliczyć średnią wartość FPS znacznie lepiej niż tylko biorąc pod uwagę ostatnią klatkę.

Pozwala także zignorować jedną klatkę, jeśli robisz coś, o czym wiesz, że sztucznie zepsuje czas tej klatki.

Umożliwia także zmianę liczby ramek przechowywanych w kolejce w trakcie jej działania, dzięki czemu można przetestować w locie, jaka jest dla Ciebie najlepsza wartość.

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

Dobre odpowiedzi tutaj. Sposób wdrożenia zależy od tego, do czego jest potrzebny. Sam wolę średnią działającą „time = time * 0.9 + last_frame * 0,1” przez gościa powyżej.

jednak osobiście lubię mocniej ważyć moją średnią w kierunku nowszych danych, ponieważ w grze to KOLCE są najtrudniejsze do zgniatania i dlatego najbardziej mnie interesują. Więc użyłbym czegoś bardziej jak podział .7 \ .3 sprawi, że skok pojawi się znacznie szybciej (chociaż jego efekt również szybciej zniknie z ekranu ... patrz poniżej)

Jeśli skupiasz się na czasie RENDEROWANIA, to podział .9.1 działa całkiem nieźle b / c, zwykle jest bardziej płynny. Chociaż w przypadku gry / sztucznej inteligencji / fizyki skoki są dużo większym problemem, ponieważ to zwykle powoduje, że twoja gra wygląda na niestabilną (co często jest gorsze niż niska liczba klatek na sekundę, zakładając, że nie spadamy poniżej 20 fps)

Więc dodałbym też coś takiego:

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

(uzupełnij 3,0f dowolną wielkością, którą uznasz za niedopuszczalną wartość szczytową). Pozwoli ci to znaleźć, a tym samym rozwiązać problemy z liczbą klatek na sekundę, na końcu klatki, w której wystąpiły.


Podoba mi się time = time * 0.9 + last_frame * 0.1średnia kalkulacja, która sprawia, że ​​wyświetlacz zmienia się płynnie.
Fabien Quatravaux,

2

O wiele lepszym systemem niż używanie dużej liczby starych klatek na sekundę jest zrobienie czegoś takiego:

new_fps = old_fps * 0.99 + new_fps * 0.01

Ta metoda zużywa znacznie mniej pamięci, wymaga znacznie mniej kodu i przywiązuje większą wagę do ostatnich liczby klatek na sekundę niż stare, a jednocześnie wygładza skutki nagłych zmian liczby klatek na sekundę.


1

Możesz zachować licznik, zwiększać go po wyrenderowaniu każdej klatki, a następnie resetować licznik, gdy jesteś na nowej sekundzie (zapisując poprzednią wartość jako # wyrenderowanych klatek z ostatniej sekundy)


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

Oto kompletny przykład użycia Pythona (ale łatwo dostosowany do dowolnego języka). Używa równania wygładzającego w odpowiedzi Martina, więc prawie nie ma narzutu pamięci, a ja wybrałem wartości, które działały w moim przypadku (możesz swobodnie bawić się stałymi, aby dostosować się do twojego przypadku użycia).

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

Ustaw licznik na zero. Za każdym razem, gdy rysujesz ramkę, zwiększaj licznik. Po każdej sekundzie wydrukuj licznik. spienić, spłukać, powtórzyć. Jeśli chcesz uzyskać dodatkowy kredyt, zachowaj bieżący licznik i podziel przez całkowitą liczbę sekund, aby uzyskać średnią kroczącą.


0

W (podobnym do C ++) pseudokodzie te dwa są tym, czego użyłem w przemysłowych aplikacjach przetwarzania obrazu, które musiały przetwarzać obrazy z zestawu kamer wyzwalanych zewnętrznie. Różnice w „liczbie klatek na sekundę” miały inne źródło (wolniejsza lub szybsza produkcja na pasku), ale problem jest ten sam. (Zakładam, że masz proste wywołanie timera.peek (), które daje mniej więcej liczbę msec (nsec?) Od uruchomienia aplikacji lub ostatniego wywołania)

Rozwiązanie 1: szybkie, ale nie aktualizowane w każdej klatce

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

Rozwiązanie 2: aktualizacja każdej klatki, wymaga więcej pamięci i procesora

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

Jak to robię!

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

Innymi słowy, zegar tikowy śledzi tiki. Jeśli jest to pierwszy raz, bierze aktualny czas i umieszcza go w „tickstart”. Po pierwszym tiku powoduje, że zmienna „fps” jest równa liczbie taktów zegara podzielonej przez czas minus czas pierwszego tiku.

Fps to liczba całkowita, stąd „(int)”.


1
Nie polecałbym nikomu. Dzielenie całkowitej liczby taktów przez całkowitą liczbę sekund sprawia, że ​​FPS zbliża się do matematycznego limitu, w którym w zasadzie po długim czasie ustala się na 2-3 wartościach i wyświetla niedokładne wyniki.
Kartik Chugh

0

Oto jak to robię (w Javie):

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

W Typescript używam tego algorytmu do obliczania średniej szybkości klatek i czasu klatek:

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

stosowanie:

statsUpdate();

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

Wskazówka: jeśli próbki to 1, wynikiem jest liczba klatek na sekundę i czas klatek w czasie rzeczywistym.


0

Jest to oparte na odpowiedzi KPexEA i podaje prostą średnią kroczącą. Uporządkowane i przekonwertowane na TypeScript dla łatwego kopiowania i wklejania:

Deklaracja zmiennej:

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

Funkcjonować:

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

Użycie (może się różnić w Twojej aplikacji):

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

-1

zapisać czas rozpoczęcia i zwiększyć licznik ramek raz na pętlę? co kilka sekund możesz po prostu wydrukować liczbę ramek / (Teraz - czas rozpoczęcia), a następnie ponownie je zainicjować.

edycja: oops. podwójny ninja

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.