Timer Klasse



  • Hi,

    Ich habe für mein Spiel eine Timerklasse geschrieben und bitte um ein Review, bzw. Tipps.
    Die Klasse ist für SDL2 ausgelegt (benutzt SDL_GetPerformanceCounter) aber wäre leicht auf std::chrono zu ändern... Ich hoffe das hält niemanden davon ab, sich den Code anzusehen 🙂

    Zur Verwendung: Ich wollte ein Timer Objekt haben, dass sowohl Zeit, die vergangen ist zählen kann (Stopp-Uhr), als auch einen Timer, der nach einer festgelegten Zeit Callback funktionen ausführen kann. (Default einmal, auf Wunsch auch immer wieder von vorne).
    Anstatt ein std::function zu verwenden, nehme ich meine Signal klasse um Funktionen auszuführen. Der Vorteil hier ist, dass ich einfach beliebig viele Funktionen registrieren (und auch de-registrieren) kann.

    Für die Countdown-Timer Funktion verwende ich std::thread. Da ist mir aufgefallen, dass ein Thread mit einer while(running) schleife sofort eine menge CPU verbraucht, deshalb hab ich eine sleep eingebaut, was den CPU verbrauch gesenkt hat.

    Mich würde interessieren, was ihr von meiner Implementation haltet, bzw. was ich daran besser machen kann.

    Die Namen der Variablen sind teilweise nicht so besonders...

    Hier der Code. Ich hab die ganzen Doxygen Kommentare drinngen gelassen.. Meine Signal klasse hab ich hier auskommentiert.

    Timer.hxx

    #ifndef TIMER_HXX_
    #define TIMER_HXX_
    
    #include <ctime>
    #include <chrono>
    #include <thread>
    #include <future>
    
    #include "SDL.h"
    // #include "../basics/log.hxx"
    // #include "../basics/signal.hxx"
    
    class Timer
    {
    public:
      /** \brief Timer object
      * creates a Timer object, that can measure past time in milliseconds or set a 
      * countdown timer. It's also possible to invoke a callback function when the timer is triggered
      */
      Timer() = default;
      ~Timer();
    
      /** \brief get elapsed time
      * Returns the time in milliseconds, that has passed since the timer was started.
      * @returns the time in milliseconds, that has passed since the timer was started
      */
      int getElapsedTime();
    
      /** \brief get elapsed time since last timeOut
      * Returns the time in milliseconds, that has passed since the last TimeOut has happend.
      * @returns the time in milliseconds, that has passed since the last timeOut
      * @note Only applicable, if a timer has been set
      * @see Timer#setTimer()
      */
      int getElapsedTimeSinceLastTimeOut();
    
      /** \brief Start / Reset the timer
      * Starts or restarts the timer.
      * If the timer is already running, it will be stopped and restarted.
      */
      void start();
    
      /** Stop the timer
      * Stops the timer and set it back to 0 
      */
      void stop();
    
      /** Pauses the timer
       */
      void pause();
    
      /** Resume the timer
      * Resumed the paused timer. If the timer is not paused, it will behave as start()
      */
      void resume();
    
      /** \brief Checks if the timer is active.
      * Returns true if the timer is running (pending); otherwise returns false.
      */
      bool isActive() { return _isActive; };
    
      /** \brief Checks if the time is done.
      * Returns true, if the timer has reached 0. 
      * @note Only applicable, if a timer has been set
      * @see Timer#setTimer()
      */
      bool isTimedOut() { return _timeOut; };
    
      /** \brief Sets a timer
       * Sets a timer. When a timer is set, a signal is emitted and the registered callback function 
       * is called. Also a bool variable will be set to true.
       * @see Timer#egisterCallbackFunction
       * @see Timer#isTimedOut()
      */
      void setTimer(int timeInMs);
    
      /** \brief Register callback function for this timer
      * Register one or more callback functions that should be exececuted, when the Timer is triggered.
      * @note Only applicable, if a timer has been set
      * @see setTimer()
      * @see Signal#Signal
      * @see Signal#slot
      */
    //  void registerCallbackFunction(std::function<void()> const &cb) { _timeOutSignal.connect(cb); };
    
      /** Loop Timer
      * Specify if the timerout callback is called once or everytime the counter hits zero.
      */
      void loopTimer(bool loop) { _loopTimer = loop; };
    
    private:
      void startThread();
    
    //  Signal::Signal<void()> _timeOutSignal;
    
      std::thread _timerThread;
    
      bool _loopTimer = false;
      bool _timeOut = false;
      bool _isActive = false;
      bool _threadRunning = false;
    
      int _timeUntilTimeOut = 0;
      int _timeSinceLastTimeOut = 0;
      int _elapsedTime = 0;
    
      Uint64 _startTime = 0;
      Uint64 _endTime = 0;
    
      Uint64 _lastTimeOutTime = 0;
    
      void timeOut();
    };
    
    #endif1
    

    Timer.cxx

    #include "timer.hxx"
    
    Timer::~Timer()
    {
      _threadRunning = false;
      if (_timerThread.joinable())
      {
        _timerThread.join();
      }
    }
    
    void Timer::start()
    {
      _startTime = SDL_GetPerformanceCounter();
      _lastTimeOutTime = SDL_GetPerformanceCounter();
      _endTime = _startTime;
      _elapsedTime = 0;
      _isActive = true;
      _timeOut = false;
    
      startThread();
    }
    
    void Timer::stop()
    {
      _elapsedTime = (int)((SDL_GetPerformanceCounter() - _startTime) * 1000 / SDL_GetPerformanceFrequency());
      _isActive = false;
      _timeOut = false;
      _threadRunning = false;
      if (_timerThread.joinable())
      {
        _timerThread.join();
      }
    }
    
    void Timer::pause()
    {
      if (_isActive)
      {
        _elapsedTime = (int)((SDL_GetPerformanceCounter() - _startTime) * 1000 / SDL_GetPerformanceFrequency());
        _timeSinceLastTimeOut = (int)((SDL_GetPerformanceCounter() - _lastTimeOutTime) * 1000 / SDL_GetPerformanceFrequency());
        _endTime = _startTime;
        _startTime = SDL_GetPerformanceCounter();
        _isActive = false;
      }
    }
    
    void Timer::resume()
    {
      if (!_isActive)
      {
        _startTime = SDL_GetPerformanceCounter() - (_startTime - _endTime);
        _lastTimeOutTime = SDL_GetPerformanceCounter() - _lastTimeOutTime;
        _isActive = true;
      }
    }
    
    void Timer::setTimer(int timeInMs)
    {
      _timeUntilTimeOut = timeInMs;
      _timeOut = false;
    }
    
    int Timer::getElapsedTime()
    {
      if (_isActive)
      {
        _elapsedTime = (int)((SDL_GetPerformanceCounter() - _startTime) * 1000 / SDL_GetPerformanceFrequency());
      }
      return _elapsedTime;
    }
    
    int Timer::getElapsedTimeSinceLastTimeOut()
    {
      if (_isActive)
      {
        _timeSinceLastTimeOut = (int)((SDL_GetPerformanceCounter() - _lastTimeOutTime) * 1000 / SDL_GetPerformanceFrequency());
      }
      return _timeSinceLastTimeOut;
    }
    
    void Timer::timeOut()
    {
      _timeSinceLastTimeOut = 0;
      _timeOut = true;
    //  _timeOutSignal.emit();
      _lastTimeOutTime = SDL_GetPerformanceCounter();
    }
    
    void Timer::startThread()
    {
      // if thread is running, abort and join it before starting a new one
      if (_timerThread.joinable())
      {
        _threadRunning = false;
        _timerThread.join();
      }
      else if (_threadRunning == true)
      {
        return;
      }
    
      _threadRunning = true;
      if (_timeUntilTimeOut != 0)
      {
        _timerThread = std::thread([=]() {
          while (_threadRunning)
          {
            if (getElapsedTimeSinceLastTimeOut() >= _timeUntilTimeOut)
            {
              timeOut();
              if (!_loopTimer)
              {
                _threadRunning = false;
              }
            }
    
            // helps with cpu usage...
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
          }
          return;
        });
      }
    }```


  • Ich würde dafür keine Threads verwenden. Du kannst einfach dem Timer Objekt die Framezeit/delta-time mitgeben, und die wird aufaddiert. Sobald t >= countdown ist, wird die Funktion aufgerufen. Ich würde die Timer Klasse selbst und das Ausführen eines Tasks nach einer bestimmten Zeit trennen. Heißt natürlich nicht, dass man Ersteres nicht dazu nehmen kann, um den Countdown zu implementieren. Aber ich würde es eben nicht in dieselbe Klasse packen. Das sind ziemlich viele Membervariablen für einen Timer.

    Ich würde Dir hier besonders raten, nicht gleich zu versuchen den perfekten alles-könnenden Timer zu implementieren, sondern mit einer minimalen Version anzufangen, die genau das macht, was Du brauchst. Zum Beispiel: Brauchst Du wirklich einen Timer, den man pausieren kann?



  • @harteware

    Danke! Ein Bekannter hat mir auch bereits dazu geraten, den Timer im Konstruktor am Event Handler (der die ganzen Maus / Tastatur Befehle auswertet) anzumelden und dort dann die Timer zu prüfen und das TimeOut event auszulösen, damit ich keine Threads verwenden muss. Ich werde das so umbauen.

    Ich denke, ich werde im Event Handler einen vector<Timer*> hinzufügen und beim erstellen des Timers einen pointer dort hineingeben und im Eventhandler zusätzlich immer alle aktiven Timer überprüfen / auslösen.

    (Ich denke, daraus ergibt sich der Nachteil, dass ich keine Timer im EventHandler verwenden kann, wegen den header includes, aber vorerst brauche ich dort ohnehin keine Timer).

    Die Funktion, die misst, wieviel Zeit seit dem start() verstrichen ist, brauche ich z.b. für Benchmarks. Wie lange die Engine zum starten / initialisieren der objekte usw benötigt. Das Ausführen einer Funktion nach Zeit t brauche ich z.b. für die Anzeige der Tooltips über einem UI Element, wenn der Mauszeiger länger darüber verharrt.
    Ich dachte, dass es Sinn macht alles in einer Klasse zusammenzuführen, aber ja, das Interface ist vielleicht etwas unübersichtlich geworden.
    Denkst du, ich sollte hier zwei Klassen machen? CountdownTimer und Timer?
    Sollten dann beide Klassen eine Parentklasse bzw. ein Interace haben?

    Und ja, die Timer müssen pausiert werden können, da das ganze Spiel auch pausiert werden können soll. z.b. beim Öffnen eines menüs. Da mein Spiel ein City Builds (alá SimCity 2000) wird, wird es auch mehrere Spiel-Geschwindigkeiten geben, an die sich manche (nicht alle!) Timer anpassen müssen. D.h. die Klasse(n) wir noch eher komplexer als einfacher.



  • Das Pausieren löst man i. d. R. dann so, dass man im "EventHandler" (also in deiner GameLoop) einfach nicht mehr den Countdown updatet. Deshalb ist es hier geschickt, wenn man nur ein einziges TimerObjekt hat, dass immer die Framezeit misst (einfach wäre ein fixed Timestep mit z. B. 60FPS) und diese so genannte delta-time wird allen GameObjekten in der update-Funktion übergeben. Du willst ja das Pausieren an einer einzigen Stelle handhaben, und nicht überall eine pause() Funktion einfügen müssen.

    Eine einfache Timer Klasse merkt sich einfach einen Start-Zeitpunkt (z. B. std::chrono::steady_clock::now()) und wenn Du wissen willst, wie viel Zeit seither vergangen ist, holst Du dir einfach wieder den aktuellen Zeitpunkt und subtrahierst davon den Start-Zeitpunkt. Als Beispiel könnte auch z. B. sf::Clock dienen.

    Das Verlangsamen oder schneller Laufen der Zeit könnte man mit einem Faktor lösen, der mit der Framezeit multipliziert wird. Ich hatte mal Zeitlupe implementiert, und habe es so gemacht, wenn ich mich richtig erinnere (z.B. mit Faktor 0,1 multiplizieren).


Anmelden zum Antworten