Skip to Content
Course content

175: Building a Small Game with a Game Loop

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably already written plenty of programs that wait for user input, process it, and then stop. But games don't work like that. A game is essentially a high-speed loop that keeps running whether the player is pressing a key or not. The world keeps turning, the enemies keep moving, and the timer keeps ticking.

When I first started writing game loops, I made a mistake that almost every beginner makes. I thought "once per frame" was a reliable unit of measurement. Take a look at this snippet from a project I worked on years ago:

while (gameRunning) {
    handleInput();
    
    // Move the player to the right
    player.x += 5; 
    
    updateWorld();
    render();
}

The Teleporting Player Problem

On my old laptop, this code worked fine. The player moved at a reasonable pace. But when I ran it on a newer machine with a faster CPU, the player didn't just move—they teleported. They flew off the screen in a fraction of a second. Conversely, if the computer lagged, the player slowed down to a crawl.

The problem is that player.x += 5 is tied to the frame rate. If your computer can push 500 frames per second, you're moving 5 pixels 500 times. If it can only push 30, you're moving 150 pixels per second. Your game's physics are currently held hostage by the hardware's speed, which is a nightmare for gameplay balance.

Scaling Movement by Delta Time

To fix this, we need to stop thinking in "pixels per frame" and start thinking in "pixels per second." We do this using something called Delta Time (dt)—the amount of time that actually elapsed since the last frame was processed.

Here is how we fix the logic using the <chrono> library to track real-world time:

#include <chrono>

auto lastTime = std::chrono::high_resolution_clock::now();
float playerSpeed = 300.0f; // 300 pixels per second

while (gameRunning) {
    auto currentTime = std::chrono::high_resolution_clock::now();
    std::chrono::duration<float> elapsed = currentTime - lastTime;
    float dt = elapsed.count(); 
    lastTime = currentTime;

    handleInput();
    
    // Now movement is independent of frame rate
    player.x += playerSpeed * dt; 
    
    updateWorld(dt);
    render();
}

By multiplying the speed by dt, we've normalized the movement. If the frame takes 0.016 seconds (roughly 60 FPS), the player moves a small amount. If the frame takes 0.1 seconds (a massive lag spike), the player moves a larger chunk to "catch up." The result? The player crosses the screen in exactly one second, regardless of whether the computer is a toaster or a supercomputer.

Structuring the Heartbeat of Your Game

Now that we have timing sorted, let's look at the overall architecture. A professional game loop is usually split into three distinct phases. I find that if you mix these, your code becomes a spaghetti mess very quickly.

  • Process Input: Capture keyboard, mouse, or controller states. Do not put game logic here; just record what the player wants to do.
  • Update: This is where the "simulation" happens. Move characters, check for collisions, and update timers. This phase must accept dt as a parameter so everything stays synchronized.
  • Render: Draw everything to the screen. This phase should be "read-only"—it shouldn't change the player's position or health; it just displays the current state.

Handling Input Without Blocking

One last thing: if you use std::cin, your game loop will freeze every time it waits for the user to press Enter. That's a deal-breaker for a game. In a real-world scenario, you'd use a library like SDL, SFML, or GLFW to poll the keyboard state. For our purposes in C++, you need to ensure your input method is non-blocking, meaning the code checks "is this key down right now?" and immediately moves on if the answer is no.




📋 Practical Task

Exercise: Implementing a Frame-Independent Enemy Patrol

You are building a simple survival game. You have an enemy that needs to patrol back and forth between two points on the X-axis (leftBound = 100 and rightBound = 500).

Currently, the enemy movement is broken because it's tied to the frame rate. Your task is to rewrite the update logic to use Delta Time.

Requirements:

  • Create a float enemyX = 100.0f; and a float enemySpeed = 200.0f; (pixels per second).
  • Create a bool movingRight = true; to track direction.
  • Implement a loop simulation where you calculate dt using std::chrono.
  • Update enemyX based on enemySpeed and dt.
  • Add logic to flip movingRight when the enemy hits either 100 or 500.
  • Print the enemyX position to the console each frame to verify it's moving smoothly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.