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.
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:
# 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) 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 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.
- Download the archive. While it configures, FetchContent downloads the archive from its release address. Nothing uses it yet.
- 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.
- 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.
- 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).
- 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.
- 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
One loop opens the window, reads the keys and draws each frame
The window program is one loop:
// 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., andCloseWindow()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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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().
- 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:
#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
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():
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.
- 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}.
- update(game, input, 1/60 s), in game.cpp, which is plain C++, turns that input into the next Game {paddle, ball, misses}.
- 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.
// 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:
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 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
Practise: a score and a pause key
The starter code in exercise/ is the same game with two empty places:
scorecounts one point each time the paddle returns the ball, and a miss leaves it unchanged.Input::pauseis true in the one step in which the pause key went down. A press switchespaused, and while the game is paused, nothing on the court moves.
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;
} 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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.
-
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?
-
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?
Key takeaways
- raylib 6.0 is a C99 library under the zlib/libpng licence; FetchContent with
URL_HASHdownloads 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 betweenBeginDrawingandEndDrawinguntilWindowShouldCloseis true. IsKeyDownsuits movement andIsKeyPressedsuits 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
- raylib, a simple and easy-to-use library to enjoy videogames programming (raylib project)
- raylib cheatsheet (raylib project)
- raylib source repository (README, LICENSE, src/raylib.h) (raylib project)
- raylib v6.0 release (raylib project)
- raylib 6.0 source: src/rcore.c (SetTargetFPS, EndDrawing) (raylib project)
- raylib 6.0 source: rtext.c (TextFormat), rlgl.h (rlReadScreenPixels), external/rlsw.h (readback) (raylib project)
- FetchContent (CMake documentation) (Kitware)
- SDL3 wiki (front page, CategoryRender, CategoryGPU) (SDL project)
- GLFW, an OpenGL library (GLFW project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress