Chapter 3  ·  Project

Bouncing Ball

In the last chapter we worked through int, float, bool, and the rest, but we did it all in the abstract. Boxes with labels on them. That's a fine way to learn what variables are, but it doesn't quite show you what they do. The point of this chapter is to do exactly that — to take those building blocks and make them produce something visible. By the end you'll have a bright ball bouncing around the screen, and every single thing about that ball — its position, its size, its speed, the color of the window — will be controlled by a variable you can change.

This is the first project where you'll feel that satisfying click of theory turning into pixels. It's deliberately small. The whole program is well under a hundred lines. But it's also the first thing you've built that genuinely behaves like a game: it moves, it reacts to its surroundings, and it keeps going until you stop it.

Project folder: SDL3 Projects/Bouncing Ball — the complete source for this chapter lives here.

In this chapter, we will:

Let's build it.

Setting Up the Project

Open Visual Studio and follow the same steps you used in Chapter 1. The whole sequence is:

  1. File > New > Project, pick Empty Project (C++), name it BouncingBall.
  2. Right-click Source Files, choose Add > New Item, and add a main.cpp.
  3. Right-click the project, choose Properties, and set:
    • C/C++ > General > Additional Include Directories to your SDL include folder.
    • Linker > General > Additional Library Directories to your SDL lib\x64 folder.
    • Linker > Input > Additional Dependencies to include SDL3.lib.
    • Linker > System > SubSystem to Windows.
  4. Copy SDL3.dll from SDL3\lib\x64 into the project's x64\Debug folder.

If any of that feels hazy, flip back to Chapter 1 and walk through it slowly. From this point on, every project chapter assumes you've got the setup down.

Coding the Game

We'll build the program up piece by piece, exactly as we did with the Controllable Square. Open main.cpp and follow along.

Includes and Constants

Start with the includes and a small block of constants:

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

const int   WINDOW_W    = 800;     // window width  in pixels
const int   WINDOW_H    = 600;     // window height in pixels
const float BALL_RADIUS = 20.0f;   // half the ball's width/height in pixels

In the preceding code, we bring in the two SDL headers we met in Chapter 1, then declare three constants. WINDOW_W and WINDOW_H are int values because window dimensions are always whole pixels. BALL_RADIUS is a float because positions and sizes in SDL's renderer are stored as floats, and mixing types causes the compiler to grumble at us.

The word const in front of each one means these values cannot be changed once they're set. If somewhere later in the code we wrote WINDOW_W = 1000; by accident, the compiler would catch it and refuse to build. That safety net is one of the cheapest wins in C++ — when something genuinely shouldn't change, mark it const and the compiler enforces it for you.

main and SDL Setup

Next, the main function and the SDL initialization, all of which should look familiar from Chapter 1:

int main(int argc, char* argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    SDL_Window* window = SDL_CreateWindow(
        "Bouncing Ball",
        WINDOW_W, WINDOW_H,
        0
    );
    if (!window)
    {
        SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
    if (!renderer)
    {
        SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }

In the preceding code, we start SDL, create an 800-by-600 window titled "Bouncing Ball", and create a renderer to draw into it. Each step is checked for failure, and if anything goes wrong we tidy up and exit. This same boilerplate appears at the top of every SDL project — copy and adapt rather than retyping it every time.

The Ball's Variables

Now to the part that's new for this chapter. We need variables to describe the ball: where it is, and how fast it's moving:

    float ballX    = 400.0f;
    float ballY    = 300.0f;
    float ballVelX = 300.0f;   // pixels per second, positive = moving right
    float ballVelY = 250.0f;   // pixels per second, positive = moving down

In the preceding code, we create four float variables. ballX and ballY are the ball's position — we start it in the middle of the window (since the window is 800 by 600, the center is at 400, 300). ballVelX and ballVelY are the ball's velocity — how many pixels it should move each second.

A positive ballVelX means the ball is moving to the right. If we made it negative — say, -300.0f — the ball would move left instead. The same applies vertically: positive ballVelY moves down, negative moves up. This is exactly the kind of thing variables are for. The behavior of the ball is wired directly to the value in these four boxes.

Notice the f on the end of every number. Without it, C++ would treat 300.0 as a double, not a float, and the compiler would warn about narrowing. We covered this in Chapter 2 — get into the habit now.

Timing and the Game Loop

Next we set up timing and start the game loop, just like in Chapter 1:

    Uint64 lastTime = SDL_GetTicks();

    bool running = true;
    SDL_Event event;

    while (running)
    {

In the preceding code, we capture the current time in lastTime so we can measure frame durations, then declare running as a bool that controls the main loop. The while (running) loop will repeat for as long as running stays true. The full story of while loops is coming in Chapter 6 — for now, treat it as "keep doing this until something sets running to false."

Inside the loop, the first job is to handle events:

        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_EVENT_QUIT)
                running = false;
        }

This is the same pattern from Chapter 1: drain the event queue, and if the user clicked the X to close the window, set running to false. We're not handling keyboard input in this project — the ball moves on its own — so this is all we need here.

Delta Time

Then we calculate the delta time — the number of seconds since the previous frame:

        Uint64 now       = SDL_GetTicks();
        float  deltaTime = (now - lastTime) / 1000.0f;
        lastTime         = now;

In the preceding code, we ask SDL for the current time in milliseconds, subtract the time of the previous frame, and divide by 1000.0f to convert to seconds. We then update lastTime so the next frame measures from this one.

Why all this work just to know how much time passed? Because we want the ball to move at a consistent speed regardless of how fast the computer is. A fast machine might run our game loop 200 times per second; a slow one might manage 60. If we just nudged the ball by a fixed amount every frame, it would move three times faster on the fast machine. By multiplying the velocity by deltaTime, the movement stays the same — the ball travels 300 pixels per second whether each frame takes a millisecond or sixteen.

Updating the Ball's Position

Now for the line that actually moves the ball:

        ballX += ballVelX * deltaTime;
        ballY += ballVelY * deltaTime;

In the preceding code, we use the compound assignment operator += that we met in Chapter 2. ballX += ballVelX * deltaTime; is shorthand for ballX = ballX + (ballVelX * deltaTime);. We take the velocity, multiply it by how many seconds have passed, and add the result onto the ball's position.

If ballVelX is 300.0f and deltaTime is 0.016f (a typical 16-millisecond frame), then we add 4.8 pixels to ballX this frame. That's the ball moving 300 pixels per second, expressed as the distance it should travel during this one frame. The exact same math runs for ballY.

Two lines. One ball, two axes, moving smoothly. Take a moment to appreciate that — the whole "moving thing on screen" idea, the foundation of every game in this book, fits in two lines of arithmetic on float variables.

Bouncing Off the Walls

If we ran the program as it stands, the ball would slide off the bottom-right of the screen and never come back. We need it to bounce when it hits a wall. That means checking whether the ball has gone past any edge, and if so, pushing it back inside and reversing its velocity in that direction:

        // Left wall
        if (ballX - BALL_RADIUS < 0)
        {
            ballX    = BALL_RADIUS;
            ballVelX = -ballVelX;
        }

        // Right wall
        if (ballX + BALL_RADIUS > WINDOW_W)
        {
            ballX    = WINDOW_W - BALL_RADIUS;
            ballVelX = -ballVelX;
        }

        // Top wall
        if (ballY - BALL_RADIUS < 0)
        {
            ballY    = BALL_RADIUS;
            ballVelY = -ballVelY;
        }

        // Bottom wall
        if (ballY + BALL_RADIUS > WINDOW_H)
        {
            ballY    = WINDOW_H - BALL_RADIUS;
            ballVelY = -ballVelY;
        }

In the preceding code, we use four if statements to check each wall. We'll cover if properly in Chapter 4, but the shape is intuitive: "if this condition is true, run the code inside the braces." The condition ballX - BALL_RADIUS < 0 is asking "has the left edge of the ball gone past the left edge of the window?" If yes, we set ballX to exactly BALL_RADIUS (parking the ball flush against the wall) and flip the horizontal velocity with -ballVelX.

That little minus sign on the front is doing the real work. If the ball was moving right at 300.0f pixels per second, -ballVelX is -300.0f — the same speed in the opposite direction. The ball is now moving left. The next frame, it'll move a bit further left, away from the wall. The other three walls work exactly the same way.

Notice how cleanly the constants pay off here. BALL_RADIUS and WINDOW_W make the math read like English: "if ballX plus the radius is past the window width, push it back to window width minus the radius." Without those constants we'd have a 20.0f and an 800 and a 780.0f scattered through the code, and changing the window size would mean hunting down every number that depended on it.

Drawing the Frame

Finally, draw what we've got:

        // Clear to black
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);

        // Draw the ball as a red square (close enough for now)
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        SDL_FRect ballRect = {
            ballX - BALL_RADIUS,
            ballY - BALL_RADIUS,
            BALL_RADIUS * 2.0f,
            BALL_RADIUS * 2.0f
        };
        SDL_RenderFillRect(renderer, &ballRect);

        SDL_RenderPresent(renderer);
    }

In the preceding code, we clear the window to black, switch the draw color to bright red, build an SDL_FRect to describe where to draw the ball, and fill it. The } on the last line closes the main while loop.

A small honesty note: SDL doesn't have a built-in "draw a filled circle" function, so we're drawing the ball as a filled square. From a distance it still reads as a bouncing object, which is all we need for this chapter. The arithmetic is interesting — we treat ballX and ballY as the center of the ball, but SDL_FRect expects the top-left corner, so we subtract the radius from both. The width and height are simply twice the radius.

Cleanup

When the loop ends, tidy up exactly as before:

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

That's the lot. Save the file.

The Complete Program

Here is the whole thing in one piece, ready to copy if you want to check against what you've typed:

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

const int   WINDOW_W    = 800;
const int   WINDOW_H    = 600;
const float BALL_RADIUS = 20.0f;

int main(int argc, char* argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    SDL_Window* window = SDL_CreateWindow(
        "Bouncing Ball",
        WINDOW_W, WINDOW_H,
        0
    );
    if (!window)
    {
        SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
    if (!renderer)
    {
        SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }

    float ballX    = 400.0f;
    float ballY    = 300.0f;
    float ballVelX = 300.0f;
    float ballVelY = 250.0f;

    Uint64 lastTime = SDL_GetTicks();

    bool running = true;
    SDL_Event event;

    while (running)
    {
        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_EVENT_QUIT)
                running = false;
        }

        Uint64 now       = SDL_GetTicks();
        float  deltaTime = (now - lastTime) / 1000.0f;
        lastTime         = now;

        ballX += ballVelX * deltaTime;
        ballY += ballVelY * deltaTime;

        if (ballX - BALL_RADIUS < 0)
        {
            ballX    = BALL_RADIUS;
            ballVelX = -ballVelX;
        }
        if (ballX + BALL_RADIUS > WINDOW_W)
        {
            ballX    = WINDOW_W - BALL_RADIUS;
            ballVelX = -ballVelX;
        }
        if (ballY - BALL_RADIUS < 0)
        {
            ballY    = BALL_RADIUS;
            ballVelY = -ballVelY;
        }
        if (ballY + BALL_RADIUS > WINDOW_H)
        {
            ballY    = WINDOW_H - BALL_RADIUS;
            ballVelY = -ballVelY;
        }

        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);

        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        SDL_FRect ballRect = {
            ballX - BALL_RADIUS,
            ballY - BALL_RADIUS,
            BALL_RADIUS * 2.0f,
            BALL_RADIUS * 2.0f
        };
        SDL_RenderFillRect(renderer, &ballRect);

        SDL_RenderPresent(renderer);
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

Playing the Game

Hit F5 to build and run. An 800-by-600 black window opens, with a red ball drifting away from the center. It hits a wall. It bounces. It keeps bouncing.

The Bouncing Ball program running, mid-bounce in the lower-right of the window.
The Bouncing Ball program running — it will bounce happily until you close the window.

This is your first program that does something on its own — no input required. It will quite happily bounce forever until you close the window. Sit and watch it for a minute. It's strangely satisfying.

Understanding the Code

Let's step back and see the shape of what we wrote.

The whole program is two layers. The outer layer is setup and teardown — start SDL, create a window and renderer at the top; destroy them and quit SDL at the bottom. The inner layer is the game loop, where the action happens, and it has three jobs that repeat every frame:

  1. Process events, in case the user wants to close the window.
  2. Update the ball's position based on its velocity and the time that has passed.
  3. Render the current state of the world to the screen.

That three-step rhythm — input, update, render — is the heart of every real-time game ever made, from Pong to the latest big-budget title. The only thing that changes from project to project is what gets updated and what gets drawn.

The variables themselves do almost all the heavy lifting. ballX and ballY hold the ball's position. ballVelX and ballVelY hold its velocity. Two lines of math move the ball. Four small if blocks reverse the velocity when a wall is hit. That's the entire physics simulation. Variables in, variables out, picture on screen.

This is the click. Variables aren't just abstract boxes — they're the controls of the game. Change a number, change a behavior.

Experimenting

Now that you've got it running, the best thing you can do is mess with it. Each of these is a one-line change. Try them one at a time and see what happens.

None of this changes the program in any fundamental way — the structure is exactly the same. But the result on screen is dramatically different. That's what variables give you. The structure stays put; the behavior flexes.

Common Errors and Fixes

The window opens but the ball doesn't move — Check that you're adding ballVelX * deltaTime to ballX, not just ballVelX (which would only move one pixel per frame), and not ballVelX + deltaTime (a common typo). The compound += and the multiplication both matter.

The ball flies off and never comes back — One of the if blocks for the walls is wrong. Most often it's a typo: ballX somewhere it should be ballY, or > instead of <. Re-read the block for the wall the ball is escaping through.

The ball gets stuck to a wall and jitters — You may be forgetting to set ballX (or ballY) back to the edge before reversing the velocity. Without that line, the ball can stay slightly past the wall while reversing, then move back past it, then reverse again, locked in place. The two-step "snap-to-edge then reverse" pattern fixes it.

Everything compiles but the program won't launch — Almost certainly SDL3.dll is missing from the x64\Debug folder. Copy it across from the SDL package.

AI Exercise (Optional)

If you'd like to take this project a little further with AI help, here's a low-stakes vibe-coding challenge. Skip the section entirely if you'd rather not — there's nothing here you'll need later.

Open your AI chatbot of choice and try a prompt like this:

"I have a small C++ SDL 3 program that bounces a single red square ball around an 800x600 window. The ball has four variables: ballX, ballY, ballVelX, ballVelY, and a BALL_RADIUS constant. I'm a beginner who has only just learned about variables — no loops, no functions, no arrays yet. Show me how to add a second ball of a different color and size, using only more variables of the same kinds I already have. Don't introduce any new C++ features. Paste your full code so I can compare it to mine."

Notice what that prompt does. It tells the AI exactly what you've got, what you know, and — crucially — what you don't yet know. The "no loops, no functions, no arrays" constraint is the magic bit. Without it, the AI will gleefully introduce a std::vector<Ball> and a for loop, which will give you a working program but won't teach you anything you can read yet.

When the AI comes back, read every line. Can you point to which variables belong to the new ball? Can you follow the new bouncing logic? If the AI smuggled in something you don't recognize, push back: "That uses a feature I haven't learned. Show me a version that only uses variables and if statements." Make the AI work to your level.

Summary

You've taken the variables we covered in the last chapter and turned them into a bouncing ball. The position is a variable. The velocity is a variable. The size is a constant. Tweaking any of them changes the game's behavior immediately. That tight loop between "change a value" and "see something different on screen" is the most important thing a beginner programmer can experience — it's the moment code stops feeling like a foreign language and starts feeling like a set of dials you're learning to turn.

In the next chapter we'll teach our code to make decisions: if, else, comparisons, and logical operators. That's the missing piece that turns "things on screen" into "things on screen that react." Then in Chapter 5 we'll come straight back to SDL and use those decisions to build something a lot more interactive than a ball minding its own business.