Breakout.

A 2D retro Breakout in C with Raylib — paddle, ball, brick maps, swept-AABB collision, file-backed high scores. University coursework, group of four.

2021
Breakout

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 Game struct. 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 .ps1 ties 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 GetCharPressed straight into player.name with only a length cap; a \0 at the right index is the only thing keeping it well-formed. Trim, validate, and reject empty names before they reach scores.txt.

Repo

Source on GitHub. GPL.