A raycasting 3D game engine in the style of Wolfenstein 3D, built with C and MLX42.
![]() d3bvstack |
![]() gamorcil |
- Textured walls with directional mapping (NO / SO / EA / WE)
- Configurable floor and ceiling colors (RGB)
- Real-time first-person movement (WASD + arrow keys)
- Collision detection against walls
- Minimap overlay showing player position and orientation
- Slow-walk modifier (Shift)
- DDA (Digital Differential Analyzer) raycasting algorithm
- Fisheye correction via perpendicular distance
- Comprehensive
.cubmap validation (enclosure, characters, format)
| Dependency | Debian / Ubuntu | macOS (Homebrew) |
|---|---|---|
| C compiler | build-essential |
Xcode CLT |
make |
make |
included |
cmake |
cmake |
cmake |
| GLFW | libglfw3-dev |
glfw |
Fresh clone:
git clone --recurse-submodules https://github.com/d3bvstack/cub5d.git
cd cub5d
makeAlready cloned (update submodules):
git submodule update --init --recursive
makeThe cub3D binary is created at the project root.
The project vendors three libraries as git submodules under libs/:
| Submodule | Repository | Description |
|---|---|---|
| libft | d3bvstack/libft | Custom C standard-library replacement built during the 42 cursus provides ft_split, ft_strdup, ft_calloc, get_next_line, ft_printf, and dozens of other utility functions used throughout cub3D. |
| MLX42 | codam-coding-college/MLX42 | The simple graphics library used for windowing, input handling, image creation, and pixel manipulation. |
| ft_printf | d3bvstack/ft_printf | A custom printf implementation used for formatted error messages. |
libft and ft_printf are previous 42 school projects where the student builds their own versions of standard C functions from scratch a core part of the curriculum's pedagogy.
./cub3D map.cubAn example map (map.cub) and textures (textures/) are included.
A .cub file defines textures, colors, and the map grid. Lines can appear in any order before the map begins, and blank lines are allowed.
NO ./textures/north.png
SO ./textures/south.png
EA ./textures/east.png
WE ./textures/west.png
Each path is validated: extension must be .png, file must exist and be readable, and the header must match the PNG magic bytes (\x89PNG\r\n\x1a\n).
F 77,110,59
C 107,197,250
RGB values 0–255. Format: letter (F or C) followed by comma-separated RGB.
A rectangular grid fully enclosed by walls (1). The map begins when a line contains only valid map characters and no config identifier.
| Char | Meaning |
|---|---|
1 |
Wall |
0 |
Empty floor |
N / S / E / W |
Player start position + facing direction (exactly one required) |
|
Outside / void (triggers "not closed" error if reachable) |
NO ./textures/north1.png
SO ./textures/south1.png
WE ./textures/west1.png
EA ./textures/east1.png
F 77,110,59
C 107,197,250
111111111111111111111
100000000000000011001
100111100110000111001
1001 1001 1000000001
11100111100110000111001
110000000000000011110011111111111
111101111111111111000000100010001
111101111 11010100100000001
100000111111111110000000000111101
110000000000000000000000000010001
10000011110101011111010010W011111
1011101 1010101 1010010001
1000001 1000001 1011010001
1111111 1111111 1111111111
| Key | Action |
|---|---|
| W / A / S / D | Move forward / strafe left / backward / strafe right |
| ← / → | Rotate view |
| Shift | Slow walk (40% speed) |
| ESC | Exit |
The program runs in four sequential phases, then enters a frame-based game loop.
%% Diagram 1: Top-level program flow 4 sequential phases then game loop
flowchart TD
classDef entry fill:#1e88e5,stroke:#1565c0,color:#fff
classDef phase1 fill:#43a047,stroke:#2e7d32,color:#fff
classDef phase2 fill:#00897b,stroke:#00695c,color:#fff
classDef phase3 fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef phase4 fill:#8e24aa,stroke:#6a1b9a,color:#fff
classDef loop fill:#6d4c41,stroke:#4e342e,color:#fff
classDef exit fill:#e53935,stroke:#c62828,color:#fff
A["./cub3D map.cub"] --> B[Phase 1: Parse .cub file]
B --> C[Phase 2: Assemble scene]
C --> D[Phase 3: Init MLX42 & resources]
D --> E[Phase 4: Game loop]
subgraph Game Loop
F[Movement & rotation] --> G[Collision detection]
G --> H[Draw minimap]
H --> I[Raycast & render]
I -->|next frame| F
end
E --> F
E -->|ESC| J[Cleanup & exit]
class A entry
class B phase1
class C phase2
class D phase3
class E,F,G,H,I loop
class J exit
Entry: parse_scene_file(argc, argv) in src/parse/parse.c
The parser validates CLI arguments, checks the file extension and accessibility, reads every line from the file into a t_lines struct (a dynamic array of trimmed strings with a position cursor), and returns the result.
%% Diagram 2: Phase 1 Parse .cub file validation pipeline
flowchart LR
classDef entry fill:#1e88e5,stroke:#1565c0,color:#fff
classDef decision fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef process fill:#43a047,stroke:#2e7d32,color:#fff
classDef error fill:#e53935,stroke:#c62828,color:#fff
A["argc, argv"] --> B{argc < 2?}
B -->|Yes| C["error_argc()\nreturn NULL"]
B -->|No| D["validate_extension()\n.cub check"]
D --> E{Extension .cub?}
E -->|No| F["error_extension()\nreturn NULL"]
E -->|Yes| G{"validate_fileaccess()\nfile readable?"}
G -->|No| H["perror_generic()\nreturn NULL"]
G -->|Yes| I["retrieve_lines()\nGNL → t_lines"]
I --> J[t_lines struct]
class A entry
class B,E,G decision
class D process
class I process
class C,F,H error
click D "src/parse/validate_file_helpers.c#L20-L32"
click I "src/parse/retrieve_lines.c#L88-L101"
Key functions:
| File | Function | Purpose |
|---|---|---|
parse.c |
parse_scene_file |
Orchestrates validation and reading |
validate_file.c |
validate_file |
Checks .cub extension + file access |
validate_file_helpers.c |
validate_extension |
Checks suffix matches .cub |
validate_file_helpers.c |
validate_fileaccess |
Ensures file is not a directory and is readable |
validate_file_helpers.c |
validate_png |
Validates PNG magic bytes |
file_helpers.c |
open_file_fd |
Opens file descriptor |
retrieve_lines.c |
retrieve_lines |
Reads all lines via GNL into t_lines |
file_helpers.c |
cleanup_fd |
Closes FD, resets GNL |
Entry: assemble_scene(&lines) in src/assemble/assemble.c
The assembly phase creates a t_scene struct, then processes config lines (texture paths and colors) followed by the map grid. After reading the map, it locates the player, normalizes the grid to a rectangle, checks size limits, and runs flood-fill to verify the map is fully enclosed.
%% Diagram 3: Phase 2 Assemble scene from parsed lines
flowchart TD
classDef entry fill:#1e88e5,stroke:#1565c0,color:#fff
classDef process fill:#43a047,stroke:#2e7d32,color:#fff
classDef decision fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef config fill:#00897b,stroke:#00695c,color:#fff
classDef map fill:#6d4c41,stroke:#4e342e,color:#fff
classDef validate fill:#8e24aa,stroke:#6a1b9a,color:#fff
classDef error fill:#e53935,stroke:#c62828,color:#fff
A[t_lines] --> B[create_t_scene]
subgraph Config Parsing
C{set_config}
C --> D["Parse NO/SO/EA/WE\nvalidate & store paths"]
C --> E["Parse F/C\nRGB → uint32"]
D --> F{All 4 textures +\n2 colors present?}
E --> F
F -->|Missing| G[missing_config_element]
end
subgraph Map Setup
F -->|OK| H{set_map}
H --> I["Read remaining lines\nas map rows"]
I --> J["set_player()\nfind N/S/E/W, calc dir"]
J --> K["normalize_map()\npad to rectangle"]
end
subgraph Validation
K --> L["map_size()\nlimit 200×200"]
L --> M["flood_map()\nenclosure check"]
M -->|Leak detected| N[not_closed_map]
M -->|OK| O[t_scene ready]
end
G --> P["free & return NULL"]
N --> P
class A entry
class B process
class C,D,E config
class F,H decision
class I,J,K map
class L,M validate
class G,N error
click D "src/assemble/set_config.c#L116-L138"
click J "src/assemble/set_player.c#L43-L70"
click M "src/assemble/flood_map.c#L67-L77"
Key sub-steps:
2a. Config parsing (set_config in src/assemble/set_config.c):
- Lines before the map grid are classified by their first token (
NO/SO/EA/WE/F/C) - Textures: path validated for
.pngextension, file access, and PNG magic bytes - Colors: RGB string parsed via
ft_spliton commas, each value validated 0–255 - Duplicate config entries are rejected
2b. Map reading (set_map in src/assemble/set_map.c):
- Lines are dynamically appended to
map->matrixviarealloc_array - Each line is validated: only characters
1,0,N,S,E,W, and space are allowed - Lines containing config keywords are rejected (ensures config ends before map begins)
2c. Player setup (set_player in src/assemble/set_player.c):
- Scans entire grid for
N/S/E/W - Exactly one player start required (no duplicates, no missing)
- Player position set to center of tile (
col + 0.5,row + 0.5) - Direction vector computed from orientation angle using
cos/sin:- N: 270° →
dir = (0, -1) - E: 0° →
dir = (1, 0) - S: 90° →
dir = (0, 1) - W: 180° →
dir = (-1, 0)
- N: 270° →
2d. Map normalization (normalize_map in src/assemble/map_normalize.c):
- Finds the widest row, pads all others to that width with spaces
2e. Map size check (map_size in src/assemble/map_size.c):
- Rejects maps with height or width ≥ 200
2f. Flood-fill validation (flood_map in src/assemble/flood_map.c):
- Starts from player position, recursively marks reachable empty tiles (
0→_, player char →P) - If a space character (void) is reached, the map is reported as not fully enclosed
- If the edge of the allocated matrix is reached, the map is also reported as open
Entry: game(scene) → init_game(&game, scene) in src/game/game_start.c
Creates the MLX42 window, loads all resources, and registers hook callbacks.
%% Diagram 4: Phase 3 Initialize MLX42, load resources, register hooks
flowchart LR
classDef entry fill:#1e88e5,stroke:#1565c0,color:#fff
classDef mlx fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef asset fill:#43a047,stroke:#2e7d32,color:#fff
classDef hook fill:#8e24aa,stroke:#6a1b9a,color:#fff
A[t_scene] --> B["init_game()\nmlx_init(800, 600, #quot;cub3D#quot;, false)"]
B --> C["init_game_resources()\nmlx_load_png ×4"]
C --> D["init_game_background()\nfloor / ceiling split (solid fill)"]
D --> E["mlx_new_image()\nwalls canvas"]
E --> F["mlx_new_image()\nminimap overlay"]
F --> G["set_loop_hooks()\nmlx_loop_hook ×5 + mlx_key_hook"]
class A entry
class B,C mlx
class D,E,F asset
class G hook
click B "src/game/init_game.c#L41-L54"
click C "src/game/init_game_resources.c#L30-L39"
click G "src/game/set_hooks.c#L26-L35"
Key initialization:
| Step | File | Detail |
|---|---|---|
| Window | init_game.c:19 |
mlx_init(800, 600, "cub3D", false) |
| Textures | init_game_resources.c:18 |
Loads 4 PNGs into mlx_texture_t array |
| Background | init_game_resources.c:39 |
Fills MLX image: top half = ceiling, bottom half = floor color |
| Walls | init_game_resources.c:101 |
Blank image drawn on top of background, cleared and redrawn each frame |
| Minimap | init_game_resources.c:80 |
Quarter-size overlay with configurable tile size |
Hook registration (set_hooks.c:26):
%% Diagram 5: Hook registration per-frame loop hooks + ESC key hook
flowchart TD
classDef hook_func fill:#1e88e5,stroke:#1565c0,color:#fff
classDef per_frame fill:#43a047,stroke:#2e7d32,color:#fff
classDef event fill:#fb8c00,stroke:#ef6c00,color:#fff
subgraph "mlx_loop_hook (every frame, in order)"
direction LR
L1["movement()\nWASD → next_x/y"]
L2["rotation()\nArrow keys → orientation"]
L3["execute_move()\ncollision check + apply"]
L4["minimap()\ndraw overlay"]
L5["render()\nraycast & paint"]
end
subgraph "mlx_key_hook (on key event)"
K["exit_key()\nESC → mlx_close_window"]
end
L1 --> L2 --> L3 --> L4 --> L5
A["set_loop_hooks()"] --> L1
A --> K
class A hook_func
class L1,L2,L3,L4,L5 per_frame
class K event
click A "src/game/set_hooks.c#L26-L35"
Each frame, MLX42 fires the loop hooks in order: movement → rotation → execute move → minimap → render.
%% Diagram 6: Per-frame game loop (hooks fire in registration order)
flowchart TD
classDef loop fill:#6d4c41,stroke:#4e342e,color:#fff
classDef timing fill:#00897b,stroke:#00695c,color:#fff
classDef exit fill:#e53935,stroke:#c62828,color:#fff
subgraph Per-Frame Loop
A["delta_time update\nΔt between frames"] --> B["movement()\nWASD → next_x/y"]
B --> C["rotation()\nArrow → orientation"]
C --> D["execute_move()\ncollision + apply"]
D --> E["minimap()\ndraw overlay"]
E --> F["render()\nraycast & paint"]
end
F -->|"next iteration"| A
G[ESC key] --> H["mlx_close_window()"]
H --> I["cleanup_game()"]
I --> J["mlx_terminate()"]
class A,B,C,D,E,F loop
class G timing
class H,I,J exit
click I "src/game/game_cleanup.c#L51-L56"
4a. Movement (hook_move_rotation.c:46):
- Reads WASD key states
- Computes next position (
next_x,next_y) using direction vector × speed - Speed =
5 × delta_time(or5 × 0.4 × delta_timewhen Shift held) - Strafe: A/D move perpendicular to direction vector
4b. Rotation (hook_move_rotation.c:69):
- Left/Right arrows change
player->orientationby180 × delta_timedegrees - Orientation wrapped to
[0, 360)viafmod - Direction vector recalculated:
dir_x = cos(orientation),dir_y = sin(orientation)
4c. Collision detection (hook_move_rotation.c:88):
- Checks if
next_x/next_ywith a 0.1-unit margin lands on a walkable tile (_orP) - X and Y axes checked independently for smooth wall sliding
4d. Rendering DDA Raycasting (render.c:76):
%% Diagram 7: DDA Raycasting per-frame rendering pipeline
flowchart TD
classDef frame fill:#1e88e5,stroke:#1565c0,color:#fff
classDef loop fill:#43a047,stroke:#2e7d32,color:#fff
classDef dda fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef draw fill:#8e24aa,stroke:#6a1b9a,color:#fff
classDef decision fill:#e53935,stroke:#c62828,color:#fff
A["render()\nentry point"] --> B["init_plane()\ncamera plane ⊥ direction"]
B --> C["clean_walls()\nclear previous frame"]
subgraph "For each column x = 0..799"
D["x = 0"] --> E["ray_dir_calc()\ncamera_x + ray direction"]
E --> F["setup_dda()\ndeltaDist, step, sideDist"]
F --> G["dda()\nstep grid → wall hit"]
G --> H["fix_eye()\nperpWallDist (fisheye fix)"]
H --> I["wall_size = WIN_H / perpWallDist"]
I --> J["paint_pixels()\nsample texture → draw column"]
J --> K["x++"]
K --> L{x < WIN_WIDTH?}
L -->|Yes| E
end
class A frame
class B,C frame
class D,E loop
class F,G dda
class H,I draw
class J draw
class L decision
click A "src/render/render.c#L76-L95"
click G "src/render/dda.c#L54-L78"
click H "src/render/raycast.c#L40-L45"
Raycasting detail:
%% Diagram 8: DDA raycasting math per-column algorithm detail
flowchart TD
classDef calc fill:#1e88e5,stroke:#1565c0,color:#fff
classDef step fill:#43a047,stroke:#2e7d32,color:#fff
classDef decision fill:#fb8c00,stroke:#ef6c00,color:#fff
classDef output fill:#8e24aa,stroke:#6a1b9a,color:#fff
A["camera_x = 2x / WIN_WIDTH - 1"] --> B["ray_dir = dir + plane × camera_x"]
B --> C["deltaDist_x = |1 / ray_dir_x|"]
B --> D["deltaDist_y = |1 / ray_dir_y|"]
C --> E{sideDist_x < sideDist_y?}
D --> E
E -->|Yes| F["step x\nsideDist_x += deltaDist_x\nmap_x += step_x"]
E -->|No| G["step y\nsideDist_y += deltaDist_y\nmap_y += step_y"]
F --> H{Hit wall '1'?}
G --> H
H -->|No| E
H -->|Yes| I["perpWallDist\n= sideDist − deltaDist"]
I --> J["wall_height\n= WIN_HEIGHT / perpWallDist"]
J --> K["draw_start = −wall_h/2 + WIN_H/2"]
J --> L["draw_end = +wall_h/2 + WIN_H/2"]
K --> M["Sample texture\n@ (tex_x, tex_y) → paint column"]
class A calc
class B,C,D calc
class E decision
class F,G step
class H decision
class I,J output
class K,L output
class M output
Key rendering functions:
| Function | File | Purpose |
|---|---|---|
init_plane |
raycast.c:16 |
Computes camera plane perpendicular to direction (FOV ≈ 62°) |
ray_dir_calc |
raycast.c:27 |
Calculates ray direction for column x |
setup_dda |
dda.c:33 |
Initializes DDA variables: deltaDist, step, sideDist |
dda |
dda.c:54 |
Steps through grid cells until a wall (1) is hit |
fix_eye |
raycast.c:40 |
Returns perpendicular wall distance to correct fisheye |
get_current_texture |
textures.c:27 |
Selects wall-facing texture based on side + ray direction |
calculate_tex_x |
textures.c:50 |
Computes x-coordinate within texture, mirror-corrected |
paint_pixels |
render.c:29 |
Draws the textured vertical strip for the column |
4e. Minimap (hook_minimap.c:106):
- Clears the minimap image each frame
- Draws tiles within a 5-tile radius of the player
- Color coding: wall = semi-transparent white, floor = solid white, void = semi-transparent white (walls color)
- Player drawn as a small blue square with a blue direction line
Entry: cleanup_game(&game) in src/game/game_cleanup.c
%% Diagram 9: Cleanup tear down MLX resources and free scene
flowchart LR
classDef exit fill:#e53935,stroke:#c62828,color:#fff
classDef cleanup fill:#43a047,stroke:#2e7d32,color:#fff
classDef resource fill:#fb8c00,stroke:#ef6c00,color:#fff
A["mlx_loop ends"] --> B["cleanup_game()"]
B --> C["mlx_delete_texture() ×4"]
B --> D["mlx_delete_image()\nbackground"]
B --> E["mlx_delete_image()\nwalls"]
B --> F["mlx_delete_image()\nminimap"]
C & D & E & F --> G["mlx_terminate()"]
G --> H["free_t_scene()\nfrom main"]
class A exit
class B cleanup
class C,D,E,F resource
class G cleanup
class H cleanup
click B "src/game/game_cleanup.c#L51-L56"
| Command | Effect |
|---|---|
make |
Build cub3D |
make clean |
Remove object files |
make fclean |
Remove objects, libraries, and binary |
make re |
Full rebuild |
MLX42 (the graphics library) reports memory allocations on exit; these are not leaks from cub3D.

