I’m trying to have tick rate and fps configurable via constants. Right now I have these two coupled together, so if I double the fps i would also have double the ticks per second. How do I have tick rate separate from frame rate?
Hi, The way I do it works well. I use a nanosecond-based frame rate limiting approach to ensure consistent smoothness. Other than measuring the time of a frame, my approach actively affects the execution time of a frame by slowing down the game loop. It measures how long the actual work took, and if that work took less time than the target duration, it uses SDL_DelayNS to pause the thread for the exact difference.
Here is an example in C++:
#ifndef VG_FRAME_TIMER_HPP
#define VG_FRAME_TIMER_HPP
#include <SDL3/SDL.h>
#include <algorithm>
namespace VoxelGame {
const double FPS_LOWER_LIMIT = 20.0;
const double FPS_UPPER_LIMIT = 200.0;
class FrameTimer {
public:
FrameTimer(double targetFPS = 60.0) {
SetFPSTarget(targetFPS);
Reset();
}
void Reset() {
frameCount_ = 0;
timeAccumulator_= 0;
fps_ = 0.0;
delta_ = 0.0;
}
// Définit la cible et recalcule la durée d'une frame en nanosecondes
void SetFPSTarget(double target) {
target = std::clamp(target, FPS_LOWER_LIMIT, FPS_UPPER_LIMIT);
fpsTarget_ = target;
// 1 seconde = 1e9 nanosecondes
targetFrameDurationNS_ = static_cast<uint64_t>(1e9 / fpsTarget_);
}
double GetFPSTarget() const { return fpsTarget_; }
// À appeler au TOUT DÉBUT de la boucle de jeu
void BeginFrame() {
uint64_t now = SDL_GetTicksNS();
// 1. Calcul du Delta Time (Temps écoulé depuis le début de la frame précédente)
// C'est ce delta qui doit être utilisé pour la physique/mouvement
uint64_t deltaNS = now - lastFrameStart_;
// Protection contre les deltas énormes (ex: fenêtre déplacée/bloquée)
if (deltaNS > 250000000) { // Max 0.25s (4 FPS)
deltaNS = 250000000;
}
lastFrameStart_ = now;
delta_ = static_cast<double>(deltaNS) / 1e9;
// 2. Calcul des FPS (Moyenne sur 1 seconde)
timeAccumulator_ += deltaNS;
frameCount_++;
if (timeAccumulator_ >= 1000000000) { // Chaque seconde
fps_ = static_cast<double>(frameCount_) * (1e9 / static_cast<double>(timeAccumulator_));
frameCount_ = 0;
timeAccumulator_ = 0;
}
}
// À appeler à la TOUTE FIN de la boucle de jeu, après le Rendu
void EndFrame() {
uint64_t now = SDL_GetTicksNS();
// Temps écoulé durant le TRAVAIL de cette frame (Update + Draw)
uint64_t frameWorkDuration = now - lastFrameStart_;
// Si on a été plus rapide que la cible, on attend
if (frameWorkDuration < targetFrameDurationNS_) {
uint64_t delay = targetFrameDurationNS_ - frameWorkDuration;
SDL_DelayNS(delay);
}
}
// Retourne le temps écoulé en secondes (ex: 0.0166)
double GetDelta() const { return delta_; }
// Retourne le nombre de frames par seconde calculé
double GetFPS() const { return fps_; }
private:
double fpsTarget_;
uint64_t targetFrameDurationNS_; // Durée cible en nanosecondes
uint64_t lastFrameStart_; // Timestamp du début de la frame précédente
// Variables pour le calcul du FPS moyen
uint64_t frameCount_;
uint64_t timeAccumulator_;
double fps_;
double delta_;
};
} // namespace VoxelGame
#endif // VG_FRAME_TIMER_HPP
In the main loop, we just need to do the following:
void Main::Loop() {
while (running_) {
// --- begin frame ---
frameTimer_.BeginFrame();
// --- handle event ---
inputManager_.poll();
InputState state = inputManager_.GetState();
if (state.application.IsEventActivated(ApplicationEvent::Quit) ||
state.IsKeyJustPressed(KeyboardKey::Escape)) {
running_ = false;
}
// --- update ---
// Something that takes time
// --- render ---
renderer_->SetDrawColor(0.1f, 0.1f, 0.1f);
renderer_->Clear();
// Something that takes time
renderer_->SetDrawColor(0.8f, 0.8f, 0.8f);
renderer_->SetCursor(0, 0);
renderer_->Print("VoxelGame\n");
renderer_->Print("FPS : {}\n", frameTimer_.GetFPS());
renderer_->Print("Delta: {}\n", frameTimer_.GetDelta());
renderer_->Present();
// --- end frame ---
frameTimer_.EndFrame();
}
}