Game loop & timing
The game loop is the heartbeat of a game: read input, advance the simulation by the time that passed, draw, repeat.
The concept
Each iteration of the loop is a frame. Simulation code must be told how long the previous frame took (the delta time) so movement stays the same speed on a 30 Hz and a 144 Hz display. Engines differ on whether the simulation uses a variable step (one update per frame with the real delta) or a fixed step (update at a constant rate, possibly several times per frame); a variable step is simpler, a fixed step is more deterministic.
Anatomy of a frame
Every frame does the same four jobs in the same order: input (drain the OS event queue and update input state), update (advance the simulation), render (draw the result) and present (swap the finished image onto the screen). Doing input first means the update sees the freshest state; presenting last is what makes a frame visible at all.
Delta time
Without delta time, x += 5 each frame moves twice as far at 120 FPS as at 60 FPS. Multiplying by the
seconds since the last frame, x += speed * dt, makes motion depend on time and not on frame rate. A
very long frame (a breakpoint, a window drag) produces a huge delta that can push objects through walls, which is why
engines often clamp it.
Variable versus fixed step
A variable step is the simplest option and fine for most games. A fixed step runs the simulation at a constant rate (say 60 times a second) and accumulates leftover real time, so results are reproducible and physics stays stable; the cost is more machinery, and rendering then often needs to interpolate between two simulation states.
Pacing the frame
Without a limit a loop runs as fast as the machine allows, burning CPU and battery for no visible gain. Engines either wait for the display's vertical sync or sleep until the target frame time has passed. Time can also be scaled (slow motion) or paused, which is why a clock distinguishes scaled game time from unscaled real time: a pause menu still needs real time to animate.
How ASGE does it
Application
asge::Application<TGame> owns the video, input and audio systems and constructs your
TGame in place. Run() executes the loop below and stops on an OS quit event or when a game
state requests TransitionKind::Quit.
// asge::Application<TGame>::Run(), abridged
while ( m_Running )
{
GET_TIMESYSTEM.Tick(); // advance the clock
m_InputSys.NewFrame();
while ( SDL_PollEvent( &event ) ) // drain OS events
processEvent( &event );
m_Game->Update( time::DeltaTime(), m_InputSys.GetState() );
m_Game->Render( m_VideoSys.GetRenderer() );
m_VideoSys.GetRenderer().Present();
}
ASGE uses a variable timestep: one Update per frame, with the scaled delta time.
The target frame rate comes from ApplicationConfig::s_TargetFps.
One frame inside Game
Game<TStateId>::Update runs the systems in the only valid order:
- UI interaction
- the active state's own
Update()(your game logic) - states never call systems themselves - gravity, movement, path following
- UI layout, then one transform propagation
- collision detection and resolution, then trigger events
- audio
Render then resolves pending assets, clears with the visible state's colour, draws the scene through
the render pipeline, and finally lets the states draw on top.
Game states
Screens are IGameState objects on a GameStateStack. A state's Update
may return a Transition to Push, Pop, Replace or Quit.
Your Game subclass implements CreateState() to build a state from its id.
Time
The asge::time namespace exposes the clock: DeltaTime() (scaled and pause-aware),
UnscaledDeltaTime(), ElapsedTime(), FrameIndex(), Scale(),
Paused(); Scale(float), Pause(bool) and TargetFPS(fps) are the setters. Pausing makes the scaled delta zero while the unscaled one keeps running,
which is what you want for a pause menu.
Profiling
PROFILE_SCOPE("name") times the enclosing scope with a ScopedTimer and records it into
a named TimingBucket held by the TimingProfiler singleton.
asge::time::Scale( 0.5f ); // slow motion
asge::time::Paused(); // true while paused
float dt = asge::time::DeltaTime(); // scaled, zero while paused
float raw = asge::time::UnscaledDeltaTime();
void Update()
{
PROFILE_SCOPE( "Update" ); // times this scope into a named bucket
// ...
}
See also
Source: Application/Application.hpp, Game/Game.hpp, Core/Time/Time.hpp.
Scoped profiler example · Moving box