Separate fps and tick rate. How to do that correctly?

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?

#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
#include <SDL3/SDL.h>

const Uint64 FPS = 60;
// const Uint64 TICK_RATE = 35; ???
const Uint64 DELTA = 1e9/FPS;

SDL_Window* window;
SDL_Renderer* renderer;
Uint64 frame_duration, prev_time, accumulator;
double t;
Uint64 tick;

SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv)
{
	SDL_CreateWindowAndRenderer(NULL, 800, 600, SDL_WINDOW_RESIZABLE, &window, &renderer);
	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
	Uint64 current_time = SDL_GetTicksNS();
	frame_duration = current_time - prev_time;
	prev_time = current_time;
	accumulator += frame_duration;
	if (accumulator < DELTA)
	{
		Uint64 time_remaining = DELTA - accumulator;
		SDL_Event event;
		if (SDL_WaitEventTimeout(&event, time_remaining / 1e6))
		{
			SDL_PushEvent(&event);
		}
		return SDL_APP_CONTINUE;
	}
	int num_ticks = accumulator / DELTA;
	accumulator -= num_ticks * DELTA;

	for (int i = 0; i < num_ticks; ++i)
	{
		t += DELTA/1.0e9;
		++tick;
		SDL_Log("doing stuff t=%lf tick=%ld", t, tick);
	}

	SDL_RenderPresent(renderer);
	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
	switch (event->type)
	{
	case SDL_EVENT_QUIT:
		return SDL_APP_SUCCESS;
	case SDL_EVENT_WINDOW_EXPOSED:
		SDL_RenderPresent(renderer);
		break;
	}
	return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
}

I did it like this. I’m not sure if that’s correct way to do it.

#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
#include <SDL3/SDL.h>

const Uint64 FRAME_RATE = 5;
const Uint64 FRAME_TIME = 1e9/FRAME_RATE;
const Uint64 TICK_RATE = 60;
const Uint64 TICK_TIME = 1e9/TICK_RATE;

SDL_Window* window;
SDL_Renderer* renderer;
Uint64 tick_duration, frame_duration, frame_start, prev_time, accumulator;
double t;
Uint64 tick;

SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv)
{
	SDL_CreateWindowAndRenderer(NULL, 800, 600, SDL_WINDOW_RESIZABLE, &window, &renderer);
	return SDL_APP_CONTINUE;
}

bool WaitFrame()
{
	frame_duration = SDL_GetTicksNS() - frame_start;
	if (frame_duration <= FRAME_TIME)
	{
		Uint64 time_remaining = FRAME_TIME - frame_duration;
		SDL_Event event;
		if (SDL_WaitEventTimeout(&event, time_remaining / 1e6))
		{
			SDL_PushEvent(&event);
			return false;
		}
	}
	frame_start = SDL_GetTicksNS();
	return true;
}

int GetTickCount()
{
	Uint64 current_time = SDL_GetTicksNS();
	tick_duration = current_time - prev_time;
	prev_time = current_time;
	accumulator += tick_duration;
	int num_ticks = accumulator / TICK_TIME;
	accumulator -= num_ticks * TICK_TIME;
	return num_ticks;
}

void DrawStuff()
{
	SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
	SDL_RenderClear(renderer);
	SDL_RenderPresent(renderer);
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
	bool draw = WaitFrame();
	int num_ticks = GetTickCount();

	for (int i = 0; i < num_ticks; ++i)
	{
		t = SDL_GetTicksNS()/1.0e9;
		++tick;
		SDL_Log("doing stuff t=%lf tick=%ld", t, tick);
	}

	if (draw)
		DrawStuff();
	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
	switch (event->type)
	{
	case SDL_EVENT_QUIT:
		return SDL_APP_SUCCESS;
	case SDL_EVENT_WINDOW_EXPOSED:
		DrawStuff();
		break;
	}
	return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
}

personally i use a while loop for ticking:

while(accumulatedTime >= tickDuration){

     //do stuff
      
     accumulatedTime -= tickDuration;
}

i don’t know if i understood you correctly and this is my first commenting so i hope this could be of some help.

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