C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
152: Setting Up a Basic Rendering Loop
I remember a junior developer I mentored a few years back who was building his first physics-based platformer. He had the movement logic working perfectly on his high-end gaming rig, but when he sent the build to the QA lead—who was using a decade-old company laptop—the character didn't just move slowly; it practically teleported across the screen. The problem wasn't the physics; it was that his loop was running "as fast as possible." On the gaming rig, it was hitting 500 FPS, but on the laptop, it was struggling at 30. Because his movement was tied to the frame count rather than actual time, the game speed was essentially tied to the CPU clock. It was a classic mistake, but a painful one to debug when you're staring at a screen where your character is vibrating out of existence.
The Anatomy of the Game Loop
At its core, a rendering loop is just a while loop that keeps your application alive until a specific exit condition is met. But you can't just throw a while(true) in there and call it a day. If you do, you'll peg one of your CPU cores to 100% and likely freeze your windowing system. A proper loop needs to synchronize three distinct phases: processing input, updating the simulation state, and drawing the results to the screen.
I usually structure this as a sequence of calls within the main loop. You check if the user hit the 'X' button or pressed a key, you calculate where your objects should be based on the time that has passed, and then you tell the GPU to clear the screen and redraw everything. If you skip any of these, or do them in the wrong order, you get "input lag" or "stuttering"—those tiny glitches that make a professional app feel amateur.
while (appRunning) {
processInput(); // Handle keyboard/mouse
updateGameState(); // Move players, check collisions
renderFrame(); // Push pixels to the screen
}
Taming the Frame Rate with Delta Time
To avoid the "teleporting character" problem I mentioned earlier, we use something called Delta Time (often written as dt). Delta time is simply the amount of time that elapsed since the last frame was rendered. Instead of saying "move the player 5 pixels per frame," you say "move the player 100 pixels per second."
To implement this in C++, you'll typically use std::chrono. By capturing the timestamp at the start of the loop and subtracting the timestamp from the previous frame, you get a fraction of a second. You then multiply every movement value by this dt. This ensures that whether your loop runs 30 times a second or 3,000, the object moves the same physical distance in real-world time. It's a simple multiplication that saves you from a world of synchronization headaches.
Handling the Buffer Swap
One thing that often trips people up is the "flicker." If you draw directly to the screen while the monitor is refreshing, the user will see the screen being cleared and redrawn in real-time, resulting in a jarring strobe effect. This is why we use double buffering.
In a standard rendering loop, you aren't drawing to the visible screen; you're drawing to a "back buffer"—a hidden piece of memory. Once the entire frame is finished, you "swap" the buffers. The back buffer becomes the front buffer, and the front becomes the back. This swap happens almost instantaneously, giving the user a smooth, seamless transition between frames. When you're setting up your loop, make sure your rendering call ends with a swap or a "present" command, otherwise, you're just painting on a canvas that no one can see.
📋 Practical Task
Exercise: Implementing a Framerate-Independent Position Tracker
Your task is to implement a basic C++ console-based loop that simulates a moving object. Instead of using a graphics library, you will print the object's position to the console, but the logic must be framerate-independent.
- Create a
whileloop that runs until a booleanisRunningbecomes false. - Use
std::chrono::steady_clockto calculate thedeltaTime(the time elapsed between the current frame and the previous frame). - Define a
positionvariable (float) and aspeedvariable (e.g., 10.0 units per second). - In each iteration, update the position using the formula:
position += speed * deltaTime. - Print the current position and the current
deltaTimeto the console. - Add a small
std::this_thread::sleep_forcall with a random duration (between 10ms and 50ms) inside the loop to simulate fluctuating hardware performance.
Verify that despite the random sleep intervals (which simulate FPS drops), the object's position increases linearly relative to the actual wall-clock time.
There are no comments for now.