[English Version] | Version Française
- About This Project
- Quick Start
- How to Learn from this Project
- Understanding Raycasting
- Code Structure
- Going Further
- Additional Resources
- FAQ
⚠️ Note: I'm a student myself, so this project may contain errors or inaccuracies. If you spot any mistakes or have suggestions for improvements, please don't hesitate to open an issue or submit a pull request. Your feedback is greatly appreciated!
This project was initially a personal endeavor for me to deeply understand the mechanics of raycasting. I decided to share it in the hope that it might help other students on their own learning journey. Think of it as a student's notes, shared with the community.
This project is a didactic C implementation of a basic first-person shooter (FPS) game engine, rendered directly in the command line. It serves as an educational tool to understand the fundamental principles of raycasting, a rendering technique popularized by early 3D games like Wolfenstein 3D.
The engine is heavily inspired by the excellent work of @Javidx9 (OneLoneCoder) and his C++ project "Command Line First Person Shooter (FPS) Engine". This C implementation aims to replicate the basic concepts in a simplified, modular, and well-commented way, making it ideal for learning.
All explanations, in-code comments, and diagrams were prepared with the help of AI.
- ASCII 3D Rendering: Experience a pseudo-3D environment using standard terminal characters.
- Player Movement: Navigate the 3D world with forward/backward movement.
- Player Rotation: Look around the environment.
- Mini-map: A small 2D representation of the player's position and map layout.
- Basic Statistics: Display of player coordinates, angle, and estimated FPS.
- Terminal Interaction: Uses raw terminal mode for direct key input and screen manipulation.
- Modular Code: Organized in header and source files for clarity and maintainability.
- Educational Comments: Every function includes visual diagrams and detailed explanations.
To build and run this project, you will need a C compiler (like GCC).
- Navigate to the project directory:
cd /w3d-cli-raycaster - Compile the project using
make:This will compile allmake
.cfiles and link them into an executable namedw3d-cli-raycaster. - Run the game:
./w3d-cli-raycaster
- W / w (or Up Arrow): Move Forward
- S / s (or Down Arrow): Move Backward
- A / a (or Left Arrow): Rotate Left
- D / d (or Right Arrow): Rotate Right
- Q / q: Quit Game
The program is built around a simple, modular architecture that separates concerns, making it easier to understand and modify.
-
main.c: The entry point of the application. It orchestrates the main game loop, calling initialization, update, and cleanup functions. -
init.c: Handles all the setup required to run the game, such as initializing the player, loading the map (defined indefines.h), and setting up the terminal for rendering. -
loop.c: Contains the core game loop. It processes user input, updates the player's state, and triggers the rendering of a new frame. -
raycasting.c: The heart of the 3D engine. It takes the player's position and direction and calculates what the world looks like from that perspective using the raycasting algorithm. -
draw.c: Main rendering functions that orchestrate the raycasting process and coordinate the drawing of each frame. -
draw_utils.c: Utility functions for drawing specific elements: statistics display, minimap, screen buffer output, and player direction indicator. -
terminal_mode.c: Terminal configuration functions for raw mode, cursor visibility, and screen clearing. -
terminal_input.c: Keyboard input handling, including support for arrow keys via ANSI escape sequences. -
cleanup.c: Manages the clean shutdown of the application, restoring the terminal to its original state.
In this project, I've aimed to avoid "magic numbers." Important configuration constants, such as screen dimensions, player field of view (FOV), and rendering parameters, are centralized in include/defines.h. For pedagogical clarity, single-use constants are inlined directly where they are used, accompanied by explanatory comments. This approach balances centralized configuration with immediate context for learning.
I've tried to follow a specific documentation strategy to enhance learning:
-
Header Files (
.h): Contain "interface" comments (similar to JavaDoc). They explain WHAT a function does, its parameters, and what it returns. This provides a quick, high-level overview of the module's capabilities. -
Source Files (
.c): Contain detailed didactic comments. They explain HOW and WHY the code works. You will find step-by-step algorithmic explanations, diagrams, and clarifications on the purpose of complex logic.
This separation allows you to first understand the public API in the .h files and then dive into the implementation details in the .c files.
Raycasting is a technique that allows creating a 3D illusion in a 2D environment. Imagine yourself in a dark room with a flashlight:
TOP VIEW (Bird's eye)
╔════════════════════════════════════╗
║ Wall ║
║ ################ ║
║ # # ║
║ # P # ║
║ # ╱ ╲ # ║
║ # ╱ ╲ # ║
║ # ╱ ╲ # ║
║ # ╱ ╲ # ║
║ Wall # ╱ ╲ # Wall ║
║ # ╲ # ║
║ # ◄══════════► # ║
║ # Flashlight # ║
║ # Cone # ║
║ # # ║
║ ################ ║
║ ║
╚════════════════════════════════════╝
P = Player.
Each ray in the cone = one light beam
Each ray stops when it hits a wall (#)
How it works:
- The flashlight cone represents your field of view (FOV).
- We divide this cone into 120 light rays (one per screen column).
- Each ray travels forward until it hits a wall.
- We measure the distance to that wall.
- Farther walls appear smaller on screen.
Example:
Close wall (2 units away): Far wall (10 units away):
P ──→ # (hits quickly!) P ─────────────→ # (takes longer!)
Screen shows: Screen shows:
████ ██
████ ← Large wall ██ ← Small wall
████ ██
Raycasting does exactly that, but with 120 rays (one per screen column)!
┌─────────────────────────────────────────────────┐
│ For each COLUMN of the screen (x = 0 to 119) │
└─────────────────────────────────────────────────┘
↓
┌──────────────────────┐
│ 1. INIT_RAY │ Calculate ray angle
│ Which direction? │
└──────────────────────┘
↓
┌──────────────────────┐
│ 2. CAST_RAY │ Cast the ray
│ Advance until │ until hitting a wall
│ hitting wall │
└──────────────────────┘
↓
┌──────────────────────┐
│ 3. HEIGHT CALC │ Farther away,
│ Wall height = │ smaller it is
│ f(distance) │
└──────────────────────┘
↓
┌──────────────────────┐
│ 4. DRAW_COLUMN │ Draw:
│ Draw on screen │ - Ceiling (empty)
│ │ - Wall (#, X, x, .)
│ │ - Floor (_, :, ;, ,)
└──────────────────────┘
File: src/raycasting.c
FOV = 60° (π/3 radians)
/-------------
/ \
/ \
/ \
-30° Player (0°) +30°
θ
Column 0 (left) → angle = θ - 30°
Column 60 (center) → angle = θ
Column 119 (right) → angle = θ + 30°
angle = (player_angle - FOV/2) + (x / screen_width) * FOVConcrete example:
- Player looking North:
player_angle = 0° - FOV = 60° (π/3 radians)
- For column 0 (left):
angle = 0 - 30° = -30° - For column 60 (center):
angle = 0° - For column 119 (right):
angle = +30°
void init_ray(t_game *game, int x, t_ray *ray)
{
// 1. Calculate the ray's angle
// This determines which direction the ray points based on screen column
ray->angle = (game->player.a - game->player.fov / 2.0f)
+ ((float)x / (float)SCRN_WIDTH) * game->player.fov;
// 2. Initialize ray state
ray->distance_to_wall = 0.0f; // Start at player position
ray->hit_wall = 0; // Haven't hit a wall yet
// 3. Calculate direction vector (normalized)
ray->eye_x = sinf(ray->angle); // X component
ray->eye_y = cosf(ray->angle); // Y component
}Key points:
eye_xandeye_yform a direction vector.- This vector has a length of 1 (unit vector).
- It indicates which direction the ray travels.
File: src/raycasting.c
Player (P) ───→ ───→ ───→ ───→ X (Wall detected!)
| | | |
0.1 0.2 0.3 0.4 ← distance traveled
WHILE (no wall AND distance < max):
1. Advance by 0.1 unit
2. Calculate current position (x, y)
3. Check if it's a wall (#)
4. If yes: STOP
void cast_ray(t_game *game, t_ray *ray)
{
int test_x, test_y;
while (!ray->hit_wall && ray->distance_to_wall < game->depth)
{
// 1. Advance by one small step
ray->distance_to_wall += 0.1; // RAY_STEP_SIZE
// 2. Calculate current position
test_x = (int)(game->player.x + ray->eye_x * ray->distance_to_wall);
test_y = (int)(game->player.y + ray->eye_y * ray->distance_to_wall);
// 3. Check for collision
if (out_of_bounds(test_x, test_y)) {
ray->hit_wall = 1; // Map edge
}
else if (game->map[test_x][test_y] == '#') {
ray->hit_wall = 1; // Wall detected!
}
}
}Key points:
- We advance little by little (
RAY_STEP_SIZE= 0.1). - At each step, we check the map tile.
- We stop as soon as we hit a
#(wall).
There are two main techniques for calculating rays:
1. Small Steps Method (used here):
Player → → → → → → → → → Wall
0.1 0.2 0.3 0.4
At each step, advance 0.1 units and test for collision
- ✅ Easy to understand: "Advance until hitting a wall"
- ✅ Short and readable code: ~20 lines
- ✅ Ideal for learning basic concepts
⚠️ Average performance (but sufficient for this project)
2. Optimized DDA Method (alternative):
This method is significantly faster and more precise as it mathematically calculates the exact points where the ray intersects with the grid lines, instead of taking small steps. It jumps directly from one intersection to the next, drastically reducing the number of checks needed. However, the implementation is more complex.
- ✅ 5-10x faster: Jumps directly to grid intersections.
- ✅ Perfect precision: Uses exact mathematical calculations.
- ❌ Complex code: Harder to understand for beginners.
For those interested in the mathematical details,read the Digital Differential Analyzer (DDA) algorithm page on Wikipedia.
Why does this project use the simple method?
This project has a pedagogical goal. The small steps method allows you to:
- Mentally visualize the ray "advancing" through space
- Easily understand collision detection
- Focus on concepts rather than optimization
Both methods use sin/cos to calculate the ray direction. The real difference is in how we find the wall: regular small steps vs. direct calculation of grid intersections.
For a production project where performance is critical, the optimized DDA method would be preferable. For learning, the current method is perfect! 🎓
File: src/draw.c (function render)
CLOSE wall (distance = 2): FAR wall (distance = 10):
┌─────────┐ Screen ┌─────────┐ Screen
│ │ ← Ceiling │ │
│ │ │ │
│#########│ │ │
│#########│ ← LARGE wall │ ... │ ← small wall
│#########│ (occupies │ │ (occupies
│#########│ screen) │_________│ little)
│_________│ ← Floor │ │
└─────────┘ └─────────┘
wall_height = (screen_height / 2) / distance
Examples:
distance = 1.0→height = 40/2/1 = 20 lines(very large!)distance = 5.0→height = 40/2/5 = 4 lines(small)distance = 10.0→height = 40/2/10 = 2 lines(very small)
// Line where the wall starts (from top)
ceiling = (SCRN_HEIGHT / 2) - wall_height;
// Line where the wall ends (from bottom)
floor = SCRN_HEIGHT - ceiling;Visualization:
Line 0 → ┌─────────┐
│ Ceiling │ ← ceiling = 10
Line 10 → ├─────────┤
│#########│
│ Wall │
│#########│
Line 30 → ├─────────┤ ← floor = 30
│ Floor │
Line 40 → └─────────┘
If you use the direct distance of the ray, you'll notice that walls look curved, especially at the edges of the screen. This is called the fish-eye effect.
Player
| \
| \
| \ <-- Ray at the edge (longer)
| \
Perp. | \ Direct Distance
Dist. | \
+------- Wall
^
|
Center Ray (shorter)
The Cause: Rays cast to the sides of the player's view have to travel a longer distance to hit a flat wall compared to the ray in the center.
The Solution: We need the perpendicular distance, not the direct one. We can calculate it by multiplying the direct distance by the cosine of the angle difference between the player's view and the ray's angle.
Formula:
// Angle difference between player's look direction and the current ray
float angle_diff = ray_angle - player_angle;
// Correct the distance
float corrected_distance = direct_distance * cos(angle_diff);
// Use the corrected distance for wall height calculation
wall_height = (screen_height / 2) / corrected_distance;This simple correction makes straight walls appear straight, creating a more natural and less distorted image.
File: src/draw.c
For each line Y of the column:
IF Y <= ceiling THEN
Draw ' ' (empty ceiling)
ELSE IF Y <= floor THEN
Draw wall character (#, X, x, .)
ELSE
Draw floor character (_, :, ;, ,)
For walls (get_wall_shade in src/raycasting.c) :
Distance Character Meaning
-------- --------- -------------
0 - 4 # Very close, very dark
4 - 5 X Close
5 - 8 x Medium distance
8 - 16 . Far, light
> 16 (space) Invisible
For floor (get_floor_shade in src/raycasting.c) :
Brightness Character Meaning
---------- --------- -------------
0.0 - 0.25 _ Very close
0.25 - 0.5 :
0.5 - 0.75 ;
0.75 - 0.9 ,
> 0.9 (space) Very far
| Concept | Description | File |
|---|---|---|
| FOV | Field of view (viewing angle) | init.c |
| Ray | Ray cast from player | raycasting.c |
| DDA | Ray advancement algorithm | raycasting.c |
| Distance | Distance player → wall | raycasting.c |
| Height | Wall height = f(distance) | draw.c |
| Shading | Shading based on distance | raycasting.c |
| Fish-Eye Correction | Corrects perspective distortion | draw.c (function render) |
| Trigonometry | Angle → direction vector conversion | raycasting.c |
120 columns on screen
↓
120 rays cast
↓
120 distances measured
↓
120 wall heights calculated
Distance
↓
Wall height (geometry)
↓
Shading (aesthetics)
Small FOV (30°): Large FOV (90°):
Narrow vision Wide vision
"Zoom" effect "Fish-eye" effect
| \ | /
| \ | /
P \ | /
\ | /
\ | /
\|/
P
Large RAY_STEP_SIZE (1.0):
✅ Fast
❌ Imprecise (may miss walls)
Small RAY_STEP_SIZE (0.01):
✅ Very precise
❌ Slow (many calculations)
Medium RAY_STEP_SIZE (0.1):
✅ Good balance
✅ Acceptable precision
✅ Correct performance
- Textures : Add different patterns for different wall types.
- Colors : Use ANSI codes to colorize walls.
- Sprites : Add objects in the world (enemies, items).
- Doors : Add walls that can be opened.
- External Map Files : Load maps from external files passed as command-line arguments.
- Create a
.mapfile format (e.g.,level1.map). - Parse the file at startup :
./w3d-cli-raycaster maps/level1.map. - Allow dynamic level loading without recompilation.
- Example map file format :
16 16 ################ #..............# #..#####.......# #..............# ################
- Create a
❌ Cannot do :
- Sloped floors and ceilings.
- Multiple wall heights.
- True 3D (look up/down).
- Objects on multiple levels.
✅ Excellent for :
- Mazes.
- Dungeons.
- Wolfenstein 3D-style games.
- Learning 3D concepts.
Raycasting was a revolutionary first step, but the journey to the rich, immersive 3D worlds we see today involved several key innovations. Here's how graphics technology evolved.
As you've learned, raycasting creates a 3D perspective from a 2D map. It's fast and efficient but highly constrained.
- What it did well: Created convincing 3D-like environments on limited hardware (e.g., Wolfenstein 3D).
- Limitations:
- No true verticality: Floors and ceilings are always flat. You can't have bridges, rooms above rooms, or look up and down.
- Simple geometry: Walls are always at 90-degree angles to the floor and have a uniform height.
- Basic lighting: Shading is often based only on distance, not on light sources.
Ray Tracing is the logical evolution of raycasting. Instead of stopping at the first wall, a ray of light is "traced" as it bounces around the scene.
- What it added:
- Realistic lighting: Simulates how light reflects off surfaces (reflections), passes through objects (refractions), and gets blocked (shadows).
- Complex scenes: Can handle varied geometry and materials with stunning realism.
- The catch: For decades, Ray Tracing was incredibly slow and computationally expensive, limiting its use to pre-rendered media like movie CGI (Toy Story, Jurassic Park) and architectural visualization.
While Ray Tracing focused on realism, the gaming industry needed speed. Rasterization became the dominant technique for real-time 3D graphics, especially with the rise of dedicated Graphics Processing Units (GPUs).
- How it works: Instead of casting rays from the camera, rasterization projects 3D models (made of triangles/polygons) onto the 2D screen and "fills in" the pixels.
- Advantages:
- Extremely fast: GPUs are designed to process millions of triangles in parallel, enabling high frame rates.
- Complex geometry: Can handle detailed 3D models and environments.
- Disadvantages: It's not based on how light actually works. Realistic lighting effects like reflections and soft shadows are difficult to achieve and often require clever "hacks" and approximations.
Path Tracing is an advanced form of Ray Tracing that aims to simulate the "full" behavior of light (Global Illumination). It traces thousands of light paths for each pixel, capturing how light bounces from surface to surface, creating incredibly soft shadows, color bleeding, and ambient lighting. It's the gold standard for photorealism but is even more computationally intensive than traditional Ray Tracing.
Today, we've come full circle. Modern GPUs are now powerful enough to perform a limited amount of Ray Tracing in real-time.
- Hybrid Approach: Games now primarily use rasterization for its speed but enhance the final image with real-time Ray Tracing for specific effects like:
- Accurate reflections on shiny surfaces (water, mirrors).
- Physically correct soft shadows.
- Realistic global illumination.
This hybrid model combines the performance of rasterization with the visual fidelity of Ray Tracing, pushing the boundaries of what's possible in real-time 3D graphics.
- DDA vs Bresenham : Other raycasting algorithms.
- Floor Casting : Raycasting for the floor with textures.
- Binary Space Partitioning : Optimization for large maps.
- Wolfenstein 3D : https://github.com/id-Software/wolf3d The original game that popularized raycasting.
- Lode's Tutorial : lodev.org/cgtutor/raycasting.html
- Javidx9's Video : youtube.com/@javidx9
Q: Why advance in small steps instead of going directly to the wall? A: It's simpler to understand! A more advanced algorithm (optimized DDA) can go directly from one grid intersection to another, but it's more complex. For learning, the current method is perfect because it allows you to visualize the ray "advancing" through space.
Q: Why use sin() and cos()? A: To convert an angle into a direction vector (x, y). It's basic trigonometry!
Q: Can you make a real 3D game with this? A: Yes, but limited! Games like Wolfenstein 3D used this technique. For modern games, we use GPUs and complete 3D engines.
Raycasting is:
- 120 rays cast from the player.
- Each ray finds a wall at a distance.
- This distance determines the height and shading.
- We draw 120 columns to form the image.
- Repeat 30-60 times per second → Animation! 🎨
Result : A 3D illusion with only 2D! 🎨
The project is organized in the following main directories:
include/: Contains all header files (.h) with function declarations and structure definitions.src/: Contains all source files (.c) with implementations of functions declared in header files.
The codebase follows a modular architecture, grouping related functionalities into single files for clarity and maintainability.
| File | Purpose |
|---|---|
src/main.c |
Application entry point and orchestration. |
src/init.c |
Game initialization (player, map setup, common utilities). |
src/loop.c |
Main game loop (input processing, state updates, rendering). |
src/raycasting.c |
Core 3D rendering calculations (ray casting, shading). |
src/cleanup.c |
Application shutdown and resource cleanup. |
| File | Purpose |
|---|---|
src/draw.c |
Main rendering functions (orchestrates frame drawing). |
src/draw_utils.c |
Drawing utilities (stats, minimap, screen output). |
src/terminal_mode.c |
Terminal management (raw mode, cursor visibility, screen clearing). |
src/terminal_input.c |
Keyboard input handling (keys and ANSI escape sequences). |
src/utils_time.c |
Timing utilities and FPS estimation. |
src/utils_str_base.c |
String/base conversion helpers for display. |
src/utils_itoa.c |
Integer-to-string conversion helpers. |
| File | Purpose | Difficulty |
|---|---|---|
src/raycasting.c |
Basic raycasting implementation | ⭐⭐ Intermediate |
src/draw.c |
Main rendering functions | ⭐⭐ Intermediate |
src/draw_utils.c |
Drawing utilities | ⭐ Beginner |
include/defines.h |
Core game configuration constants | ⭐ Beginner |
src/init.c |
Game initialization and setup | ⭐ Beginner |
This project is a C language adaptation inspired by the fantastic work of @Javidx9 (OneLoneCoder) and his YouTube tutorials on creating a command-line FPS engine in C++. His clear explanations and creative approach to game development in the terminal have been invaluable.
- Original Project: OneLoneCoder/CommandLineFPS
This project is open-source and available under the MIT License. Feel free to use, modify, and distribute it for educational or personal purposes.
Now, compile and run the program to see all this in action! 🎮
