Overview
A retro Breakout clone written from scratch in C against Raylib — paddle,
ball, brick grid, sound effects, a five-row high-score table persisted to
scores.txt, and a small state machine for menu, play, pause, game over,
win, credits. Built as the term project for Marmara University's
"Computer Programming I" course in a group of four. The whole game lives
in a single main.c — no engine, no scene graph, just structs, a fixed
60 FPS loop, and Raylib doing the windowing, drawing, audio, and input.
The reason for C and Raylib together is the reason for the project. The
course was the first programming course of the degree; the teaching
language was C. Raylib is the only library that lets you write idiomatic
C against a games-shaped API — a flat C header, no C++ glue, no engine
runtime, no scene tree, no asset pipeline. You get InitWindow,
BeginDrawing, LoadTexture, IsKeyDown. Anything more than that, you
write yourself. That's the assignment.
How the loop fits together
The shape is the textbook one. main opens a window, calls InitGame,
then runs while (!WindowShouldClose()) switching on a GAME_STATE enum.
The active state runs UpdateGame followed by DrawGame; other states
(menu, paused, game-over, credits, high-score) get their own
single-function handlers that draw and read input directly. There is no
ECS and no separate update list — the ball, the bat, and the brick array
are file-scope statics, and UpdateGame walks them in a fixed order:
read input, integrate velocities, resolve collisions, score, then return
to draw.
The interesting part is the ball-vs-brick collision. A naive AABB overlap check works for the bat (the bat is slow relative to the frame), but the ball moves fast enough that at 60 FPS it can pass through a brick between two frames — the classic tunnelling problem. The fix is swept collision: cast the ball's movement for the frame as a ray against each brick expanded by the ball's half-extents, and resolve the earliest contact. Multiple candidate collisions in one frame are sorted by contact time and resolved in order. The static AABB stays in the codebase for the bat-vs-bounds checks, where it's enough:
bool RectVsRectAABB(Rectangle a, Rectangle b)
{
return (a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y);
}
DynamicRectVsRect and ResolveDynamicRectVsRect do the swept work on
top of a RayVsRect primitive — same idea, applied per moving body
per frame.
Stack and shape
Raylib is the entire third-party surface. Everything else — input
handling, map parsing, score sorting (a tiny qsort-driven leaderboard
on a players[] array), text-entry for the player name, the state
machine — is plain C. Brick layouts are ASCII files in map/: * is a
two-hit red brick, + a four-hit purple, $ a five-hit gold; the
loader reads the file, allocates a blockstruct *block with calloc,
and stamps each cell with colour, hit count, and reward. Audio is three
WAVs in sfx/. Textures are PNGs in textures/ for the menu buttons
and the spacebar/enter prompts.
Build is the assignment-grade story. A PowerShell script
(pscompiler.ps1) drives gcc against the bundled static library
(lib/libraylib.a) and the headers under include/, producing
brkout.exe in the repo root. There is no makefile and no CMake; the
script is the build system. Distribution is "ship the folder" — the
exe expects textures/, sfx/, map/, and scores.txt next to it
and reads them with relative paths.
What I'd change
This is what I'd do differently picking it up now, not a feature wishlist.
- Decouple the simulation from the frame rate. The update step uses
GetFrameTime()directly, which is correct in spirit but lets the physics drift on a stalled frame. A fixed timestep with an accumulator would make the swept collision deterministic across hardware. - Lift the file-scope statics into a
Gamestruct. Everything lives at file scope right now (block,ball,bat,screen,GAME_STATE). Wrapping them in a single struct passed by pointer would make the state-handlers testable in isolation and the initialisation order explicit. - Replace the PowerShell build with a Makefile. The
.ps1ties the project to Windows for no reason; the rest of the code is portable C against a portable library. - Sanitize the high-score input. The name field is read with
GetCharPressedstraight intoplayer.namewith only a length cap; a\0at the right index is the only thing keeping it well-formed. Trim, validate, and reject empty names before they reachscores.txt.
Repo
Source on GitHub. GPL.