Skip to content

rndM/w3d-cli-raycaster

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

w3d-cli-raycaster: Command-Line FPS Engine with Raycasting


📚 Navigation

[English Version] | Version Française


ScreenShot

Table of Contents

  1. About This Project
  2. Quick Start
  3. How to Learn from this Project
  4. Understanding Raycasting
  5. Code Structure
  6. Going Further
  7. Additional Resources
  8. FAQ

About This Project

Author

⚠️ 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.

Description

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.

Features

  • 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.

Quick Start

Building and Running

To build and run this project, you will need a C compiler (like GCC).

  1. Navigate to the project directory:
    cd /w3d-cli-raycaster
  2. Compile the project using make:
    make
    This will compile all .c files and link them into an executable named w3d-cli-raycaster.
  3. Run the game:
    ./w3d-cli-raycaster

Controls

  • 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

How to Learn from this Project

Program Architecture

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 in defines.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.

Code Commenting Philosophy

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.


1. Introduction: What is Raycasting?

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:

  1. The flashlight cone represents your field of view (FOV).
  2. We divide this cone into 120 light rays (one per screen column).
  3. Each ray travels forward until it hits a wall.
  4. We measure the distance to that wall.
  5. 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)!


2. The 4-Step Process (Overview)

┌─────────────────────────────────────────────────┐
│  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 (_, :, ;, ,)
         └──────────────────────┘

3. Detailed Explanation of the 4 Steps

3.1. Step 1: init_ray() - Calculate Ray Angle

File: src/raycasting.c

Visual Concept

           FOV = 60° (π/3 radians)
              /-------------
             /               \
            /                 \
           /                   \
        -30°    Player (0°)   +30°
                   θ

Column 0 (left)     → angle = θ - 30°
Column 60 (center)  → angle = θ
Column 119 (right)  → angle = θ + 30°

Mathematical Formula

angle = (player_angle - FOV/2) + (x / screen_width) * FOV

Concrete 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°

Simplified Code

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_x and eye_y form a direction vector.
  • This vector has a length of 1 (unit vector).
  • It indicates which direction the ray travels.

3.2. Step 2: cast_ray() - Cast the Ray (DDA Algorithm)

File: src/raycasting.c

Concept: DDA Algorithm (Digital Differential Analyzer)

Player (P) ───→ ───→ ───→ ───→ X (Wall detected!)
            |     |     |     |
         0.1   0.2   0.3   0.4  ← distance traveled

Simple Pseudo-code

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

Simplified Code

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).

📝 Note: Two Approaches to Raycasting

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! 🎓


3.3. Step 3: Wall Height Calculation

File: src/draw.c (function render)

Perspective Principle

CLOSE wall (distance = 2):       FAR wall (distance = 10):

┌─────────┐ Screen                 ┌─────────┐ Screen
│         │ ← Ceiling              │         │
│         │                        │         │
│#########│                        │         │
│#########│ ← LARGE wall           │   ...   │ ← small wall
│#########│   (occupies            │         │   (occupies
│#########│    screen)             │_________│    little)
│_________│ ← Floor                │         │
└─────────┘                        └─────────┘

Mathematical Formula

wall_height = (screen_height / 2) / distance

Examples:

  • distance = 1.0height = 40/2/1 = 20 lines (very large!)
  • distance = 5.0height = 40/2/5 = 4 lines (small)
  • distance = 10.0height = 40/2/10 = 2 lines (very small)

Ceiling and Floor Calculation

// 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 → └─────────┘

Correcting Fish-Eye Distortion

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.


3.4. Step 4: draw_column() - Draw the Column

File: src/draw.c

Algorithm

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 (_, :, ;, ,)

Shading Choice

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

4. Key Concepts Summary

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

1. One Ray = One Column

120 columns on screen
  ↓
120 rays cast
  ↓
120 distances measured
  ↓
120 wall heights calculated

2. Distance Determines Everything

Distance
   ↓
Wall height (geometry)
   ↓
Shading (aesthetics)

3. FOV Controls Vision

Small FOV (30°):         Large FOV (90°):
  Narrow vision            Wide vision
  "Zoom" effect            "Fish-eye" effect

    |                      \     |     /
    |                       \    |    /
    P                        \   |   /
                              \  |  /
                               \ | /
                                \|/
                                 P

4. RAY_STEP_SIZE is a Trade-off

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

6. Going Further

6.1. Possible Improvements

  1. Textures : Add different patterns for different wall types.
  2. Colors : Use ANSI codes to colorize walls.
  3. Sprites : Add objects in the world (enemies, items).
  4. Doors : Add walls that can be opened.
  5. External Map Files : Load maps from external files passed as command-line arguments.
    • Create a .map file 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
      ################
      #..............#
      #..#####.......#
      #..............#
      ################
      

6.2. Raycasting Limitations

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.

6.3. Evolution to Full 3D

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.

1. Raycasting (Early 1990s) - The "2.5D" Illusion

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.

2. Ray Tracing - Simulating Light with Physics

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.

3. Rasterization - The Real-Time Revolution

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.

4. Path Tracing - The Quest for Photorealism

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.

5. The Modern Era - Hybrid Rendering (The Best of Both Worlds)

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.

6.4. Advanced Concepts

  • DDA vs Bresenham : Other raycasting algorithms.
  • Floor Casting : Raycasting for the floor with textures.
  • Binary Space Partitioning : Optimization for large maps.

7. Additional Resources


8. Frequently Asked Questions

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.


9. Conclusion

Raycasting is:

  1. 120 rays cast from the player.
  2. Each ray finds a wall at a distance.
  3. This distance determines the height and shading.
  4. We draw 120 columns to form the image.
  5. Repeat 30-60 times per second → Animation! 🎨

Result : A 3D illusion with only 2D! 🎨


Code Structure

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.

File Organization

The codebase follows a modular architecture, grouping related functionalities into single files for clarity and maintainability.

Core Engine Files

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.

Rendering & Utilities Files

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.

Key Files for Learning

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

Inspiration and Credits

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.


License

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! 🎮

About

Command-Line FPS Engine with Raycasting

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors