This project has been created as part of the 42 curriculum by moabed, melshata.
A dynamic 3D raycasting engine written in C using MiniLibX, inspired by the seminal 1992 game Wolfenstein 3D. This project explores graphical rendering, linear algebra, DDA (Digital Differential Analysis), event-driven programming, and rigorous map parsing.
Cub3D renders a 3D first-person perspective inside a maze-like grid defined by a .cub configuration file. The engine casts rays across a 2D grid representation to calculate the distances to surfaces, determining the height and texture alignment of walls projected onto a 2D pixel buffer.
- Fast DDA Raycasting: Ray marching algorithm with wall boundary calculation.
- Directional Texture Mapping: Distinct textures for North, South, East, and West wall faces.
- Half-Plane Background Rendering: Smooth ceiling and floor coloring with customizable RGB values.
- Axis-Independent Wall Collision: Player slides along walls rather than stopping abruptly upon collision.
- Strict Error Handling: Validates closed borders, space leakage, unique player position, and texture paths.
On Linux (Ubuntu/Debian), ensure X11 development headers and MinilibX dependencies are installed:
sudo apt-get update
sudo apt-get install -y gcc make xorg libx11-dev libxext-dev libbsd-devThe project includes a Makefile that compiles all sources into an obj/ directory using strict compiler flags (-Wall -Wextra -Werror):
# Compile cub3D executable
make
# Clean object files
make clean
# Full clean (removes objects and executable)
make fclean
# Recompile from scratch
make rePass a valid .cub scene file as the sole argument:
./cub3D tests/maps/valid/standard_box.cub| Key / Action | Function |
|---|---|
| W / ▲ | Move forward in direction of view |
| S / ▼ | Move backward |
| A | Strafe left (orthogonal to view direction) |
| D | Strafe right (orthogonal to view direction) |
| ◀ | Rotate camera left |
| ▶ | Rotate camera right |
| ESC | Exit program cleanly |
Window Close (X) |
Destroy display and exit cleanly |
The program execution is divided into distinct lifecycle phases:
┌────────────────────────┐
│ main() │
└───────────┬────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
[Invalid Input / Map] [Valid Map File]
• Print "Error\n<msg>" • Extract Textures & RGB
• Free allocated memory • Validate 4-Way Wall Enclosure
• exit(1) • Initialize MLX Display & Textures
• Enter mlx_loop() Raycasting Engine
│
▼
[Player Inputs (WASD / Rotate)]
• Update position with wall sliding
• Re-project rays (DDA) & draw frame
│
▼
[ESC or Window Close Button]
• Destroy images, window & display
• Free all structures
• exit(0)
- Scenario A (Valid invocation):
ac == 2andav[1]ends with.cub. The file descriptor is opened viaopen(av[1], O_RDONLY). - Scenario B (Invalid invocation):
- If
ac != 2, triggershandle_exit(cub, ERR_ARG_COUNT, 1)-> PrintsError\nUsage: ./cub3D map.cub. - If extension !=
.cub, triggersERR_FILE_EXT-> PrintsError\nInvalid file extension (.cub required). - If file does not exist or has no read permissions,
read_filetriggersERR_FILE_OPEN-> PrintsError\nError opening map file.
- If
The configuration file is read line-by-line via get_next_line:
- Texture Identifiers (
NO,SO,WE,EA):- The path following each identifier is stored.
- If duplicate identifiers are encountered,
ERR_DUPLICATE_ELEMENTis raised. - In
mini_parse(), each texture path is opened viaopen()to ensure existence before launching graphics.
- Colors (
F,C):- Comma-separated strings are split into R, G, and B components.
- Each component is verified to be numeric and within range
[0, 255]. - Out-of-range or malformed values raise
ERR_COLOR_RANGE.
Once all 6 scene elements are acquired (data_count == 6), lines are treated as the map grid:
- Character Validation: Ensures only
'0','1',' ', and player characters ('N','S','E','W') are present. - Player Count: Exactly one player must exist. 0 or >1 players raises
ERR_PLAYER_COUNT("wanna party or play?"). Player coordinate(x, y)and direction orientation are stored. - Boundary Enclosure (4-Way Perimeter Check):
- For every
'0'or player cell, adjacent cells (North, South, East, West) must not be empty or out of bounds. - Spaces (
' ') must never be adjacent to walkable floor cells ('0') or the player. - Any gap raises
ERR_MAP_OPEN("Error, map is open").
- For every
When map validation succeeds, init_player_vectors() sets up 2D Cartesian vectors:
- Position Vector:
pos = (int_position.x + 0.5, int_position.y + 0.5)(centered in starting tile). - Direction (
dir) & Camera Plane (plane) Vectors:- North (
N):dir = (0.0, -1.0),plane = (0.66, 0.0) - South (
S):dir = (0.0, 1.0),plane = (-0.66, 0.0) - West (
W):dir = (-1.0, 0.0),plane = (0.0, -0.66) - East (
E):dir = (1.0, 0.0),plane = (0.0, 0.66)
- North (
mlx_init()initializes the connection to the X-Window server.mlx_new_window()opens a1920x1080window.mlx_new_image()creates the primary frame buffer.load_images()loads all four wall textures usingmlx_xpm_file_to_image(), storing dimensions and data addresses:walls[0]= North (NO)walls[1]= South (SO)walls[2]= West (WE)walls[3]= East (EA)
- Event hooks (
DestroyNotify,KeyPress,mlx_loop_hook) are registered, andmlx_loop()is entered.
On every frame tick (game_loop):
-
Background:
render_background()fills top half of the buffer with Ceiling color (C) and bottom half with Floor color (F). -
Ray Generation: For each screen column
$x \in [0, \text{WIDTH})$ :- Camera X:
$\text{camera_x} = 2x / \text{WIDTH} - 1$ . - Ray Direction:
$\vec{ray_dir} = \vec{dir} + \vec{plane} \times \text{camera_x}$ . - Delta Distance:
$\Delta_x = |1 / \text{ray_dir}_x|$ ,$\Delta_y = |1 / \text{ray_dir}_y|$ .
- Camera X:
-
DDA Stepping: Advances through the map grid tile-by-tile until hitting a wall cell (
'1'), tracking whether a vertical (side = 0) or horizontal (side = 1) face was struck. - Perpendicular Wall Distance: $$\text{perp_wall_dist} = \begin{cases} \text{side_dist}_x - \Delta_x & \text{if } side = 0 \ \text{side_dist}_y - \Delta_y & \text{if } side = 1 \end{cases}$$
-
Wall Projection & Texture Mapping:
- Projected line height:
$\text{line_height} = \text{HEIGHT} / \text{perp_wall_dist}$ . - Exact wall hit coordinate:
$\text{wall_x}$ determines column in the texture ($\text{tex_x}$ ). - The selected texture column is scaled and painted to the screen buffer via
draw_wall_stripe().
- Projected line height:
-
mlx_put_image_to_window()pushes the complete rendered frame to the display.
When a movement key is pressed (handle_keypress):
-
Forward/Backward: Moves along
$\pm \vec{dir} \times \text{MOVE_SPEED}$ . -
Strafe Left/Right: Moves along
$\pm (\text{dir}_y, -\text{dir}_x) \times \text{MOVE_SPEED}$ . -
Wall Sliding: Collision is evaluated separately on the X and Y axes using a collision margin (
$0.15$ ). If moving diagonally into a corner or wall, the player slides along the unblocked axis. -
Rotation: Applies 2D rotation matrix to
$\vec{dir}$ and$\vec{plane}$ : $$\begin{bmatrix} x' \ y' \end{bmatrix} = \begin{bmatrix} \cos(\theta) & -\sin(\theta) \ \sin(\theta) & \cos(\theta) \end{bmatrix} \begin{bmatrix} x \ y \end{bmatrix}$$
When exiting via ESC or the window close button:
clean_mlx()destroys all 4 texture images, the frame buffer image, the window, and closes the X11 display connection viamlx_destroy_display().free_cub()deallocates texture paths, floor/ceiling strings, and 2D arrays (splitted_lines).exit(0)terminates the process cleanly with zero memory leaks.
An automated test suite is located in tests/ and can be run with:
./tests/run_tests.sh- CLI & Arguments (8 tests): Missing arguments, extra arguments, non-existent files, directories, empty files, malformed file extensions.
- Textures (6 tests): Missing textures, duplicate textures, invalid identifiers, unreadable file paths.
- Colors (7 tests): Out of range (>255, negative), non-numeric characters, missing/extra RGB components, duplicate colors.
- Player (4 tests): 0 players, multiple players, player placed out-of-bounds, player adjacent to void.
- Borders (6 tests): Open North, South, East, and West walls, holes in perimeter, floor touching space.
- Characters (3 tests): Invalid characters in map matrix.
- Valid Maps (12 tests): Minimal 3x3, standard rooms, all 4 orientations (N/S/E/W), irregular shapes, boundary RGB values, mazes.
- Lode's Computer Graphics Tutorial: Raycasting Tutorial — Foundation for 2D DDA raycasting, camera plane mathematics, and vertical texture scaling.
- 42 School Subject:
cub3D.pdfspecifications. - MiniLibX Documentation: Harm-Smits MiniLibX Guide — Event hooks, image buffers, and X11 color encoding.
Artificial Intelligence (Antigravity Assistant) was used during this project for the following specific tasks:
-
Architectural Review & Refactoring: Audited execution flow to eliminate redundant render cycles, isolate dead legacy code, and modularize functions to adhere strictly to the 42 Norminette limit (maximum 5 functions per
.cfile and$\le 25$ lines per function). -
Automated Test Suite Creation: Designed and generated 42+ automated test maps (
tests/maps/) and an ANSI color-coded test runner (tests/run_tests.sh) to validate all edge cases. -
Build Configuration: Configured
Makefileto isolate object files into anobj/directory and generated language server configurations (compile_flags.txtand.vscode/c_cpp_properties.json). -
Documentation: Assisted in structuring this comprehensive
README.mdand flow breakdown.