Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 22 – C++ for game development

raylib 6.0 in C++: a window, drawing and input

Build a raylib 6.0 game in C++ with CMake: open a window, draw shapes and text, read the keyboard, and test the game rules without any window.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: CMake downloads raylib 6.0 and checks it before building · One loop opens the window, reads the keys and draws each frame · Rules that never call raylib can be tested without a window · Practise: a score and a pause key. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Build a raylib 6.0 program with CMake from a pinned, hash-checked download
  • Draw shapes and text, and read the keyboard, in a loop of fixed steps
  • Separate the game rules from raylib and test them without a window
  • Test drawn frames on a computer that has no display
  • Find and fix the frame-time and input faults of a window loop

Before you start

What you’ll learn · Before you start

What you will learn

  • Build a raylib 6.0 program with CMake from a pinned, hash-checked download
  • Draw shapes and text, and read the keyboard, in a loop of fixed steps
  • Separate the game rules from raylib and test them without a window
  • Test drawn frames on a computer that has no display
  • Find and fix the frame-time and input faults of a window loop

Before you start

On this page

The paddle game of this lesson draws a new picture 60 times a second, and rules that can only be tested by playing the game are tested slowly, by hand. raylib, a small C library for games, handles the window: from C++ you include raylib.h, call InitWindow, and write one loop that reads the keys, updates the game and draws it between BeginDrawing and EndDrawing, until WindowShouldClose turns true. It also draws textures, plays sound and reads the mouse and gamepads. IsKeyDown reports a key on every frame while it is held, and IsKeyPressed only on the frame it goes down.

For the testing, one design rule pays off at once: keep the game’s rules in plain C++ that never calls raylib. You will build a paddle-and-ball game in that shape, with a file of rules, a file of drawing and a window program, let CMake download raylib 6.0 and build it with the game, and test the rules with Apple clang and GCC 16 without raylib.

CMake downloads raylib 6.0 and checks it before building

raylib is written in C99 under the zlib/libpng licence, which allows its use in commercial games, and its source archive contains everything it depends on, including GLFW for windows and input. CMake’s FetchContent module downloads a pinned release while it configures, checks the archive against the A hash function that turns a file of any size into a 256-bit value. A changed file practically always gives another value, so a matching SHA-256 shows that a download is the file expected. in URL_HASH, and adds raylib’s own targets to your build, so linking takes one line:

CMakeLists.txt Text · paddle/CMakeLists.txt
# Paddle and ball: the game rules (plain C++), the drawing code (raylib) and the programs that use them.
cmake_minimum_required(VERSION 3.25)
project(paddle LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# raylib 6.0 from its release archive, checked against the archive's SHA-256 before it is used.
include(FetchContent)
set(BUILD_EXAMPLES OFF CACHE BOOL "Build raylib's examples" FORCE)
FetchContent_Declare(raylib
  URL https://github.com/raysan5/raylib/archive/refs/tags/6.0.tar.gz
  URL_HASH SHA256=2b3ee1e2120c7a0796b33062c7e9a694dd8a8caa56a96319ac8c8ecf54a90d0b
  DOWNLOAD_EXTRACT_TIMESTAMP OFF)
FetchContent_MakeAvailable(raylib)

# The rules know nothing about raylib, so their tests need no window.
add_library(game STATIC src/game.cpp)
target_include_directories(game PUBLIC src)
target_compile_options(game PRIVATE -Wall -Wextra)

add_executable(game_tests tests/game_tests.cpp)
target_link_libraries(game_tests PRIVATE game)

# The drawing code is the only part that calls raylib.
add_library(draw STATIC src/draw.cpp)
target_link_libraries(draw PUBLIC game raylib)

if(PLATFORM STREQUAL "Memory")
  add_executable(capture src/capture.cpp)  # draws into memory with the software renderer: no window
  target_link_libraries(capture PRIVATE draw)
else()
  add_executable(paddle src/main.cpp)      # the game in a window
  target_link_libraries(paddle PRIVATE draw)
endif()

enable_testing()
add_test(NAME game_rules COMMAND game_tests)
Configure, build and test Python · build_session.py
from session import run

# This session needs the network: CMake downloads raylib 6.0's source archive (about 50 MB) and checks its SHA-256.
# The configure step shows only its main lines. The build runs 4 jobs at once, so Ninja's progress lines come in a
# different order on every run: only their count is shown.
run("""
cd paddle
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
""", keep=r"^-- (Using raylib's GLFW|Building raylib static library|Configuring done|Build files)|^--   (PLATFORM|GRAPHICS)=")
run("""
cmake --build build --parallel 4
""", keep=r"^$")
run("""
ctest --test-dir build
ls build/paddle build/game_tests
""")

Output

$ cd paddle
$ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
-- Using raylib's GLFW
-- Building raylib static library
--   PLATFORM=PLATFORM_DESKTOP
--   GRAPHICS=GRAPHICS_API_OPENGL_33
-- Configuring done (…s)
-- Build files have been written to: build
[31 lines not shown]
$ cmake --build build --parallel 4
[41 lines not shown]
$ ctest --test-dir build
Test project build
    Start 1: game_rules
1/1 Test #1: game_rules .......................   Passed    … sec

100% tests passed out of 1

Total Test time (real) =   … sec
$ ls build/paddle build/game_tests
build/game_tests
build/paddle

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py

Step through the session’s three commands, from the download to the test run:

Interactive · step by step From CMakeLists.txt to a passing test, with raylib fetched Step through the three commands of the session above. Each step says what CMake did and what the session printed.
From CMakeLists.txt to a passing test, with raylib fetched, step 7 of 7. Showing CMakeLists.txt, raylib 6.0 archive, Configure (Configuring done), SHA-256 check (matches URL_HASH), Build (only what changed), raylib's targets (PLATFORM_DESKTOP, GL 3.3), Test, game_tests (1 of 1 test passed). In focus: Build.readdownloadedcomparescompilesrunsCMakeLists.txtraylib 6.0 and its SHA-256raylib 6.0 archiveat its release addressConfigureConfiguring doneSHA-256 checkmatches URL_HASHBuildonly what changedraylib's targetsPLATFORM_DESKTOP, GL 3.3Testctest --test-dir buildgame_tests1 of 1 test passed

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Read CMakeLists.txt. cmake -S . -B build reads CMakeLists.txt. FetchContent_Declare names raylib's 6.0 release archive and the SHA-256 the archive must have.
  2. Download the archive. While it configures, FetchContent downloads the archive from its release address. Nothing uses it yet.
  3. Check the hash. CMake computes the archive's SHA-256 and compares it with URL_HASH. They match, so the configure goes on. If the file behind the address ever changed, CMake would stop here, before building anything.
  4. raylib's own targets. FetchContent_MakeAvailable adds raylib's CMake targets to the build. raylib chose its desktop platform, PLATFORM_DESKTOP, with OpenGL 3.3, and uses its own copy of GLFW for the window and the input.
  5. The first build. cmake --build compiles raylib and its GLFW once, then the game's four targets, game, draw, game_tests and paddle. Linking raylib took one line, target_link_libraries(draw PUBLIC game raylib).
  6. The rules' tests. ctest runs game_tests, which links only the rules library, so it needs neither raylib nor a window. It printed 100% tests passed out of 1.
  7. Later builds. The next cmake --build compiles only the files that changed since the last one, so the long first build of raylib happens once per build folder.

raylib chose its desktop platform with OpenGL 3.3, and later builds compile only what changed. The hash protects the build: if the file behind the address ever changes, CMake stops instead of building something else.

Checkpoint

Check: raylib from CMake, pinned by its hash

  1. Question 1 of 2 What does URL_HASH SHA256=… in FetchContent_Declare give you?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • CMake checks the downloaded archive and stops if its SHA-256 differs
    • Every build folder that downloads the archive gets exactly the same raylib source

    The hash pins the exact bytes of the archive. If the file behind the address ever changed, the configure step would fail instead of building different code. It does nothing for speed and never picks another release.

  2. Question 2 of 2 A teammate changes the URL in FetchContent_Declare to a newer raylib release and leaves URL_HASH as it was. What does the next configure do?

    Choose one answer.

    A hint

    A newer release is a different file.

    Show the answer: Question 2 of 2

    Answer: It stops, because the new archive's SHA-256 does not match URL_HASH, before anything is built

    URL_HASH is the expected SHA-256 of one exact archive. A newer release has another hash, so the check fails before raylib is built, and the teammate updates the URL and the hash together.

One loop opens the window, reads the keys and draws each frame

The window program is one loop:

main.cpp C++ · paddle/src/main.cpp
// The game in a window: raylib opens it, reads the keyboard, and the game advances in steps of 1/60 s.
#include <algorithm>

#include "draw.hpp"
#include "game.hpp"
#include "raylib.h"

int main() {
    InitWindow(int(paddle::court_width), int(paddle::court_height), "Paddle and ball");
    SetTargetFPS(60);

    paddle::Game game;
    constexpr float step = 1.0f / 60;
    float unsimulated = 0.0f;  // time the game has not caught up with yet, in seconds

    while (!WindowShouldClose()) {  // Esc or the window's close button ends the loop
        const paddle::Input input{
            .up = IsKeyDown(KEY_UP) || IsKeyDown(KEY_W),
            .down = IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S),
        };
        // Whatever the frame took, the rules always run in the same fixed steps (at most a quarter second's worth).
        unsimulated = std::min(unsimulated + GetFrameTime(), 0.25f);
        while (unsimulated >= step) {
            paddle::update(game, input, step);
            unsimulated -= step;
        }

        BeginDrawing();
        paddle::draw(game);
        EndDrawing();
    }
    CloseWindow();
}
  • InitWindow(800, 450, title) opens the window with its The state OpenGL keeps for one window. Every drawing call works in it, so the window and its context must exist before anything is drawn., and CloseWindow() closes both.
  • SetTargetFPS(60) caps the loop at 60 frames a second; GetFrameTime() says how long the last frame took.
  • WindowShouldClose() becomes true when the player presses Esc or clicks the window’s close button.
  • IsKeyDown(KEY_UP) is true on every frame while the key is held, which suits movement.
  • IsKeyPressed(KEY_P) is true only on the frame the key went down, which suits a toggle such as pause.

Whatever a frame took, the rules advance in fixed steps of 1/60 s, so the window, the tests and a replay all do the same arithmetic. Step through one pass of the loop with the down arrow held:

Interactive · step by step One frame of main.cpp, with the down arrow held Step through one pass of the loop, from the keys to the picture. The game starts where game.hpp puts the paddle and the ball.
One frame of main.cpp, with the down arrow held, step 8 of 8. Showing The player, Input {up, down} (down = true, up = false), Time not simulated (0 ms owed), update() (ran once), The window (the new picture), Game (paddle at y = 191). In focus: The player, Input {up, down}.IsKeyDownwhole stepsdraw(game)The playerholds the down arrowInput {up, down}down = true, up = falseTime not simulated0 ms owedupdate()ran onceThe windowthe new pictureGamepaddle at y = 191held: IsKeyDown; once: IsKeyPressed

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The loop goes on. WindowShouldClose() is false, because the player has neither pressed Esc nor closed the window, so main.cpp runs another pass of its loop.
  2. Read the keys. The player holds the down arrow. IsKeyDown(KEY_DOWN) is true on this frame, so the loop fills an Input with down = true and up = false.
  3. Add the frame's time. GetFrameTime() says how long the last frame took; say 1/60 s, about 16.7 ms. The loop adds it to the time the game has not simulated yet, and never lets that pile up past 0.25 s.
  4. One fixed step. 16.7 ms is one whole step of 1/60 s, so update() runs once with the Input and the step is taken off the time owed. Whatever a frame took, the rules only ever see steps of 1/60 s.
  5. The rules move things. In that step the paddle moves 360 pixels a second × 1/60 s = 6 pixels down, from y = 185 to y = 191, and the ball moves 5 pixels left and 3 down, from (400, 225) to (395, 228).
  6. Draw the frame. Between BeginDrawing() and EndDrawing(), draw(game) clears the background and draws the middle line, the paddle, the ball and the misses text. EndDrawing() shows the new picture.
  7. Wait for the next frame. With SetTargetFPS(60), EndDrawing() waits out what is left of the frame's 1/60 s, then reads the new keyboard events. The loop starts again from WindowShouldClose().
  8. Pressed or held. Had the player pressed P in this frame, IsKeyPressed(KEY_P) would be true now and false on the next frame, with the key still held. IsKeyDown(KEY_DOWN) stays true on every frame the key is held.

The drawing code is the only other file that calls raylib:

draw.cpp C++ · paddle/src/draw.cpp
#include "draw.hpp"

#include "raylib.h"

namespace paddle {

namespace {
constexpr Color background{20, 24, 36, 255};
constexpr Color court_line{70, 80, 110, 255};
constexpr Color paddle_colour{240, 200, 80, 255};
constexpr Color ball_colour{120, 220, 255, 255};
constexpr Color text_colour{235, 235, 235, 255};
}  // namespace

void draw(const Game& game) {
    ClearBackground(background);
    DrawLine(int(court_width / 2), 0, int(court_width / 2), int(court_height), court_line);
    DrawRectangleRec(Rectangle{game.paddle.x, game.paddle.y, game.paddle.w, game.paddle.h}, paddle_colour);
    DrawCircleV(Vector2{game.ball.x, game.ball.y}, game.ball.r, ball_colour);
    DrawText(TextFormat("misses: %d", game.misses), 660, 16, 20, text_colour);
}

}  // namespace paddle

ClearBackground paints the whole frame, and every later call draws over it: a line down the middle, the paddle as a rectangle, the ball as a circle and the text in raylib’s built-in font. A Color holds red, green, blue and alpha from 0 to 255. TextFormat works like printf but returns one of four buffers that raylib reuses, so draw its result at once and never keep the pointer.

Checkpoint

Check: The window loop and the keyboard

  1. Question 1 of 2 A key should switch the game between running and paused. Which raylib function should the window layer read for it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: IsKeyPressed(KEY_P), true only on the frame the key went down

    A toggle must change once per press. IsKeyDown stays true for every frame the key is held, so a half-second press would flip the toggle about 30 times at 60 frames a second. IsKeyPressed reports the press once.

  2. Question 2 of 2 The ball moves at 300 pixels a second, and the rules advance in fixed steps of 1/60 s. How many pixels does it move in one step?

    Type a number.

    A hint

    Multiply the speed by the length of one step.

    Show the answer: Question 2 of 2

    Answer: 5 pixels

    300 × 1/60 = 5 pixels per step, as the stepped frame showed: the ball went from x = 400 to x = 395. Every update moves it by exactly that much, whatever a frame took, so the window, the tests and a replay all agree.

Rules that never call raylib can be tested without a window

Follow the arrows from the window and from the tests to the same update():

main.cpp and the tests both fill Input; update() turns it into the Game state, which draw() shows and the tests check.main.cpp:IsKeyDown(KEY_UP),IsKeyDown(KEY_DOWN)game_tests.cpp:a scripted InputInput {up, down}update(game, input, 1/60 s)game.cpp, plain C++Game {paddle, ball, misses}draw.cpp: draw(game)between BeginDrawing()and EndDrawing()game_tests.cpp:checks on Gamefillsfillsraylibno raylib

The rules know nothing about raylib; the window and the tests both drive them

Text description of the diagram

The diagram is a chain from top to bottom, with two entries at the top and two exits at the bottom.

  1. At the top, main.cpp reads the keyboard with IsKeyDown(KEY_UP) and IsKeyDown(KEY_DOWN), and game_tests.cpp makes up a scripted Input. Both fill the same Input {up, down}.
  2. update(game, input, 1/60 s), in game.cpp, which is plain C++, turns that input into the next Game {paddle, ball, misses}.
  3. At the bottom, the Game goes two ways: to draw(game) in draw.cpp, which uses raylib between BeginDrawing() and EndDrawing(), and to the checks of game_tests.cpp, which use no raylib.

The rules sit between Input and Game and call nothing of raylib, so the tests build and run them without a window.

game.hpp C++ header · paddle/src/game.hpp
// The paddle-and-ball game as plain C++: what is on the court and how one step of time changes it.
// Nothing here includes raylib, so the tests build and run without a window, on any compiler.
#pragma once

namespace paddle {

inline constexpr float court_width = 800.0f;   // pixels, the size of the window
inline constexpr float court_height = 450.0f;
inline constexpr float paddle_speed = 360.0f;  // pixels per second
inline constexpr float serve_speed_x = 300.0f;
inline constexpr float serve_speed_y = 180.0f;

// What the player asked for during this step. The window layer fills it from the keyboard; a test writes it.
struct Input {
    bool up = false;
    bool down = false;
};

struct Paddle {
    float x, y;  // top-left corner
    float w, h;
};

struct Ball {
    float x, y;    // centre
    float vx, vy;  // pixels per second
    float r;
};

struct Game {
    Paddle paddle{24.0f, 185.0f, 12.0f, 80.0f};
    Ball ball{court_width / 2, court_height / 2, -serve_speed_x, serve_speed_y, 8.0f};
    int misses = 0;
};

// Advances the game by dt seconds: moves the paddle, moves the ball, bounces it, and counts a miss.
void update(Game& game, const Input& input, float dt);

}  // namespace paddle

update takes the game, what the player asked for and a time step. The tests build game.cpp without raylib, create situations that would take minutes to produce by hand, such as a ball 24 pixels from the paddle, and check the state afterwards:

The tests

paddle/tests/game_tests.cpp

// The game rules, tested without a window: no raylib, just the state and update().
#include <cstdio>
#include <string_view>

#include "../src/game.hpp"

namespace {

int checks = 0;
int failures = 0;

void check(bool ok, std::string_view what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::printf("FAIL %.*s\n", int(what.size()), what.data());
    }
}

// Runs the game for a number of 1/60 s steps with the same input, as 60 frames a second would.
void run(paddle::Game& game, paddle::Input input, int steps) {
    for (int i = 0; i < steps; ++i) paddle::update(game, input, 1.0f / 60);
}

}  // namespace

int main() {
    using namespace paddle;
    {
        Game g;
        run(g, {.up = true}, 120);
        check(g.paddle.y == 0.0f, "the paddle stops at the top of the court");
        run(g, {.down = true}, 120);
        check(g.paddle.y == court_height - g.paddle.h, "the paddle stops at the bottom of the court");
    }
    {
        Game g;
        g.ball = {400.0f, 12.0f, 0.0f, -300.0f, 8.0f};
        run(g, {}, 3);
        check(g.ball.vy > 0.0f && g.ball.y >= g.ball.r, "the top wall sends the ball back down");
    }
    {
        Game g;
        g.ball = {60.0f, 225.0f, -300.0f, 0.0f, 8.0f};  // straight at the middle of the paddle
        run(g, {}, 10);
        check(g.ball.vx > 0.0f, "the paddle returns a ball that hits it");
        check(g.misses == 0, "a returned ball is not a miss");
    }
    {
        Game g;
        g.paddle.y = 300.0f;  // low on the court, so the ball passes above it
        g.ball = {60.0f, 100.0f, -300.0f, 0.0f, 8.0f};
        run(g, {}, 20);
        check(g.misses == 1, "a ball that passes the paddle counts one miss");
        check(g.ball.vx > 0.0f && g.ball.x > court_width / 2, "the next serve starts at the centre, towards the right wall");
    }
    {
        // A replay: the same inputs from the same start give the same game, step for step.
        Game a, b;
        for (int i = 0; i < 600; ++i) {
            const Input in{.up = i % 90 < 30, .down = i % 90 >= 60};
            update(a, in, 1.0f / 60);
            update(b, in, 1.0f / 60);
        }
        check(a.ball.x == b.ball.x && a.ball.y == b.ball.y && a.misses == b.misses, "the same inputs replay to the same game");
        std::printf("after 10 s of play: ball at (%.0f, %.0f), %d miss%s\n", a.ball.x, a.ball.y, a.misses, a.misses == 1 ? "" : "es");
    }
    std::printf("%d of %d checks passed\n", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}

paddle/src/game.cpp

#include "game.hpp"

#include <algorithm>
#include <cmath>

namespace paddle {

void update(Game& game, const Input& input, float dt) {
    Paddle& p = game.paddle;
    Ball& b = game.ball;

    // The paddle moves while a key is held, and stays on the court.
    const float direction = (input.down ? 1.0f : 0.0f) - (input.up ? 1.0f : 0.0f);
    p.y = std::clamp(p.y + direction * paddle_speed * dt, 0.0f, court_height - p.h);

    b.x += b.vx * dt;
    b.y += b.vy * dt;

    // The top, bottom and right walls reflect the ball.
    if (b.y - b.r < 0.0f) {
        b.y = b.r;
        b.vy = std::abs(b.vy);
    }
    if (b.y + b.r > court_height) {
        b.y = court_height - b.r;
        b.vy = -std::abs(b.vy);
    }
    if (b.x + b.r > court_width) {
        b.x = court_width - b.r;
        b.vx = -std::abs(b.vx);
    }

    // The paddle returns a ball that reaches its front edge, level with it, while moving left.
    const bool level = b.y + b.r >= p.y && b.y - b.r <= p.y + p.h;
    const bool at_front = b.x - b.r <= p.x + p.w && b.x >= p.x;
    if (b.vx < 0.0f && level && at_front) {
        b.x = p.x + p.w + b.r;
        b.vx = -b.vx;
        // Where it hits sets the new angle: straight back from the middle, steeply from the ends.
        const float offset = (b.y - (p.y + p.h / 2)) / (p.h / 2);
        b.vy = 270.0f * offset;
    }

    // A ball that leaves the court on the left is a miss: serve again from the centre, towards the right wall.
    if (b.x + b.r < 0.0f) {
        ++game.misses;
        b = Ball{court_width / 2, court_height / 2, serve_speed_x, serve_speed_y, b.r};
    }
}

}  // namespace paddle
The rules' tests, with each compiler Python · tests_session.py
from session import run

# The game rules and their tests, with no raylib at all: built with each compiler and the sanitizers switched on.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined paddle/tests/game_tests.cpp paddle/src/game.cpp -o game_tests && ./game_tests
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined paddle/tests/game_tests.cpp paddle/src/game.cpp -o game_tests-gcc && ./game_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined paddle/tests/game_tests.cpp paddle/src/game.cpp -o game_tests && ./game_tests
after 10 s of play: ball at (97, 105), 1 miss
8 of 8 checks passed
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined paddle/tests/game_tests.cpp paddle/src/game.cpp -o game_tests-gcc && ./game_tests-gcc
after 10 s of play: ball at (97, 105), 1 miss
8 of 8 checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tests_session.py

Both compilers ran all eight checks with the sanitizer: A compiler option, such as -fsanitize=address or -fsanitize=undefined, that builds extra checks into a program, so that memory errors and undefined behaviour are reported, with their source line, while it runs. More in “Finding memory bugs with AddressSanitizer and UBSan” on, and both ended the 600 scripted steps with the ball at (97, 105) after one miss. Equal inputs gave an equal game, which replays, networked games and bug reports depend on.

Key idea

The window fills an Input from the keys, a test fills it from a script, and the rules cannot tell the two apart.

Other libraries can play raylib’s part. SDL3 is a C library under the zlib licence for windows, input, audio and graphics, with a 2D renderer and a GPU API. GLFW opens windows and reads input but leaves all drawing to OpenGL or Vulkan.

Checkpoint

Check: Rules tested without a window

  1. Question 1 of 2 Why do the game's tests build and run on a computer with no display at all?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: game.cpp never includes raylib, and the tests drive update() with an Input of their own

    The rules depend only on standard C++. The window layer and the tests both talk to them through Input and Game, so the tests need neither raylib nor a window, and they build with any compiler.

  2. Question 2 of 2 Which of these can game_tests.cpp check without raylib?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • That a ball which passes the paddle counts one miss
    • Where the ball is after 600 scripted steps

    The tests see what update() changes: positions, velocities and counts in Game. Colours and the keyboard belong to the window layer, which is the part they leave out on purpose.

Practise: a score and a pause key

The starter code in exercise/ is the same game with two empty places:

  1. score counts one point each time the paddle returns the ball, and a miss leaves it unchanged.
  2. Input::pause is true in the one step in which the pause key went down. A press switches paused, and while the game is paused, nothing on the court moves.
The starter code and its sample tests

C++ header · exercise/game.hpp

// The exercise: the paddle-and-ball rules with two things to add, a score and a pause state.
#pragma once

namespace paddle {

inline constexpr float court_width = 800.0f;
inline constexpr float court_height = 450.0f;
inline constexpr float paddle_speed = 360.0f;
inline constexpr float serve_speed_x = 300.0f;
inline constexpr float serve_speed_y = 180.0f;

struct Input {
    bool up = false;
    bool down = false;
    bool pause = false;  // true in the one step in which the pause key went down (raylib: IsKeyPressed(KEY_P))
};

struct Paddle {
    float x, y;
    float w, h;
};

struct Ball {
    float x, y;
    float vx, vy;
    float r;
};

struct Game {
    Paddle paddle{24.0f, 185.0f, 12.0f, 80.0f};
    Ball ball{court_width / 2, court_height / 2, -serve_speed_x, serve_speed_y, 8.0f};
    int misses = 0;
    int score = 0;        // TODO: one point each time the paddle returns the ball
    bool paused = false;  // TODO: a pause press switches it; while paused, nothing on the court moves
};

void update(Game& game, const Input& input, float dt);

}  // namespace paddle

C++ (game.cpp) · exercise/game.cpp

#include "game.hpp"

#include <algorithm>
#include <cmath>

namespace paddle {

void update(Game& game, const Input& input, float dt) {
    // TODO: handle input.pause and game.paused here.

    Paddle& p = game.paddle;
    Ball& b = game.ball;

    const float direction = (input.down ? 1.0f : 0.0f) - (input.up ? 1.0f : 0.0f);
    p.y = std::clamp(p.y + direction * paddle_speed * dt, 0.0f, court_height - p.h);

    b.x += b.vx * dt;
    b.y += b.vy * dt;

    if (b.y - b.r < 0.0f) {
        b.y = b.r;
        b.vy = std::abs(b.vy);
    }
    if (b.y + b.r > court_height) {
        b.y = court_height - b.r;
        b.vy = -std::abs(b.vy);
    }
    if (b.x + b.r > court_width) {
        b.x = court_width - b.r;
        b.vx = -std::abs(b.vx);
    }

    const bool level = b.y + b.r >= p.y && b.y - b.r <= p.y + p.h;
    const bool at_front = b.x - b.r <= p.x + p.w && b.x >= p.x;
    if (b.vx < 0.0f && level && at_front) {
        b.x = p.x + p.w + b.r;
        b.vx = -b.vx;
        const float offset = (b.y - (p.y + p.h / 2)) / (p.h / 2);
        b.vy = 270.0f * offset;
        // TODO: the paddle returned the ball.
    }

    if (b.x + b.r < 0.0f) {
        ++game.misses;
        b = Ball{court_width / 2, court_height / 2, serve_speed_x, serve_speed_y, b.r};
    }
}

}  // namespace paddle

C++ (score_pause_tests.cpp) · exercise/score_pause_tests.cpp

// Sample tests of the exercise: build them with game.cpp and run them. All of them pass once the TODOs are done.
#include <cstdio>
#include <string_view>

#include "game.hpp"

namespace {

int checks = 0;
int failures = 0;

void check(bool ok, std::string_view what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::printf("FAIL %.*s\n", int(what.size()), what.data());
    }
}

void run(paddle::Game& game, paddle::Input input, int steps) {
    for (int i = 0; i < steps; ++i) paddle::update(game, input, 1.0f / 60);
}

}  // namespace

int main() {
    using namespace paddle;
    {
        Game g;
        g.ball = {60.0f, 225.0f, -300.0f, 0.0f, 8.0f};
        run(g, {}, 10);
        check(g.score == 1, "a returned ball scores one point");
    }
    {
        Game g;
        g.paddle.y = 300.0f;
        g.ball = {60.0f, 100.0f, -300.0f, 0.0f, 8.0f};
        run(g, {}, 20);
        check(g.score == 0 && g.misses == 1, "a miss scores nothing");
    }
    {
        Game g;
        g.ball = {60.0f, 225.0f, -300.0f, 0.0f, 8.0f};
        run(g, {}, 10);
        g.paddle.y = 0.0f;
        g.ball = {60.0f, 300.0f, -300.0f, 0.0f, 8.0f};
        run(g, {}, 20);
        check(g.score == 1 && g.misses == 1, "the score survives a miss");
    }
    {
        Game g;
        const Ball before = g.ball;
        const float paddle_y = g.paddle.y;
        run(g, {.pause = true}, 1);
        run(g, {.down = true}, 30);
        check(g.paused, "a pause press pauses the game");
        check(g.ball.x == before.x && g.ball.y == before.y && g.paddle.y == paddle_y, "nothing moves while the game is paused");
        run(g, {.pause = true}, 1);
        run(g, {}, 1);
        check(!g.paused && g.ball.x != before.x, "a second pause press lets the game go on");
    }
    std::printf("%d of %d checks passed\n", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}
The sample tests against the starter code Python · exercise_session.py
from session import run

# The sample tests against the starter code, before any TODO is done, with the sanitizers switched on.
run("""
cd exercise
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined score_pause_tests.cpp game.cpp -o tests && ./tests
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined score_pause_tests.cpp game.cpp -o tests-gcc && ./tests-gcc
""")

Output

$ cd exercise
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined score_pause_tests.cpp game.cpp -o tests && ./tests
FAIL a returned ball scores one point
FAIL the score survives a miss
FAIL a pause press pauses the game
FAIL nothing moves while the game is paused
2 of 6 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined score_pause_tests.cpp game.cpp -o tests-gcc && ./tests-gcc
FAIL a returned ball scores one point
FAIL the score survives a miss
FAIL a pause press pauses the game
FAIL nothing moves while the game is paused
2 of 6 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

You are done when both compilers print 6 of 6 checks passed. First hint: handle the pause before anything moves, so that one early return freezes the paddle and the ball together. To play it, read the key with IsKeyPressed(KEY_P) in main.cpp, keep the press until one fixed step has used it, and draw the score in draw.cpp.

Then practise the build, the window loop and the split between rules and window.

Practice that adapts to you

Practise this lesson

8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does FetchContent_MakeAvailable(raylib) add to the paddle game's build?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: raylib's own CMake targets, so linking it takes one line in target_link_libraries

    FetchContent downloads the pinned archive while CMake configures, and FetchContent_MakeAvailable adds raylib's own targets. The first build compiles raylib and its GLFW once; later builds compile only what changed.

  2. Question 2 of 8 A colleague wants to download raylib from an address that always serves the newest release, so that the game gets every fix. What does the pinned URL with URL_HASH give that this would lose?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: The same raylib source on every machine and in every build, until someone changes the pin on purpose

    A pinned release with its SHA-256 means every machine builds the same raylib, and a changed file stops the configure instead of building something else. An upgrade becomes a deliberate change of two lines.

  3. Question 3 of 8 Which call should main.cpp use to move the paddle up while the player holds the up arrow?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: IsKeyDown(KEY_UP)

    Movement is held input, and IsKeyDown is true on every frame of the hold. IsKeyPressed is for something that happens once per press, such as a pause toggle.

  4. Question 4 of 8 What ends the while loop of main.cpp?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: WindowShouldClose() turning true when the player presses Esc or clicks the window's close button

    while (!WindowShouldClose()) runs one frame per pass until Esc or the close button; CloseWindow() then closes the window and its context.

  5. Question 5 of 8 Nothing was owed, and then one frame takes 1/30 s. How many fixed steps of 1/60 s does main.cpp run in that frame?

    Type a number.

    A hint

    The loop runs whole steps out of the time owed.

    Show the answer: Question 5 of 8

    Answer: 2 steps

    1/30 s is two steps of 1/60 s, so update() runs twice and nothing is left over. A frame of 1/60 s runs one step, as in the stepped frame; the length of a step never changes.

  6. Question 6 of 8 draw() saves the pointer that TextFormat("misses: %d", …) returned and draws it again on later frames. What can go wrong?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: raylib reuses its four buffers, so the saved pointer may show other text by then

    TextFormat works like printf but writes into one of four buffers that raylib reuses. Draw its result at once and never keep the pointer; copy the text into a std::string if a later frame needs it.

  7. Question 7 of 8 Which files of the window build of the paddle game include raylib.h?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: main.cpp and draw.cpp

    Only main.cpp, which opens the window and reads the keys, and draw.cpp, which draws the state, call raylib. game.hpp, game.cpp and the tests are plain C++.

  8. Question 8 of 8 A player reports a miss they think was unfair, and the game records the Input of every step. What does keeping the rules apart from raylib let you do with that recording?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: Replay the recorded inputs through update() in a test, step for step, and reach the same state

    The tests ended 600 scripted steps at (97, 105) with one miss on both compilers: the same inputs give the same game. Replays, networked games and bug reports depend on exactly that.

Screenshots without a window, with raylib's memory platform Pro: opens with a Pro, Premium or Ultimate pass

A machine with no display can still draw the game: raylib 6.0 can render into memory with its software renderer. This part builds that version from the same CMakeLists.txt, reproduces two surprises in reading the frame back, and turns frames into tests that catch a change of one frame of input.

In this part:

  • Building for the memory platform
  • The readback, reproduced: upside down, with red and blue swapped
  • Pixel tests that catch a one-frame change, with numbers
  • What to compare when a GPU draws the frame

2 runnable examples (C++, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Frame time and input pitfalls, reproduced Pro: opens with a Pro, Premium or Ultimate pass

Most bugs of a first raylib game come from how the window loop calls the rules, not from the rules. This part runs four faulty loops against the real update() and the loop’s own arithmetic, and measures what each one does.

In this part:

  • A 150 ms frame: the ball passes through the paddle
  • The quarter-second limit, with numbers
  • IsKeyDown or IsKeyPressed: a toggle that flips 30 times
  • A press must reach exactly one fixed step
  • raylib, SDL3 or GLFW: the trade-offs

1 diagram · 2 runnable examples (C++, Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Why should a game's rules live in code that never calls raylib or any other window library?

    Code that calls raylib needs a window, a graphics context and a person at the keyboard to run, so it can only be tested by playing. Rules written in plain C++, which take the player's input as a small struct and a time step and change a game state, can be run by a test in milliseconds, with any compiler and on a build machine with no display. A test can then set up situations that are slow to reach by hand, such as a ball a few pixels from the paddle, and check exactly what happens. The same split gives replays and networked games their determinism: equal inputs give an equal game. The window layer shrinks to reading keys into the input struct and drawing the state, and it can be swapped for SDL3 or another library without touching the rules.

  2. Fresher to mid

    Players report that after they drag the game window, the ball sometimes flies straight through the paddle. The loop calls update(game, input, GetFrameTime()) once per frame. Explain the bug with numbers, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Your build machines have no display. How would you test a raylib game's drawing code automatically, and what can make a pixel comparison fail, or pass when it should not?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    In a raylib game, the pause key sometimes works and sometimes seems to do nothing, yet every test of the pause rule passes. What is the likely bug, and how do you structure input so that tests would catch it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • raylib 6.0 is a C99 library under the zlib/libpng licence; FetchContent with URL_HASH downloads a pinned release, checks its SHA-256 and adds raylib’s targets to your build.
  • A raylib program opens its window with InitWindow, then reads input, updates and draws between BeginDrawing and EndDrawing until WindowShouldClose is true.
  • IsKeyDown suits movement and IsKeyPressed suits toggles, and the rules advance in fixed steps of 1/60 s.
  • Rules in plain C++ that never call raylib can be tested with any compiler and without a window.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.