Skip to content
/ cub3D Public
forked from Mouad-kimdil/cub3D

A 3D maze exploration game built with C and miniLibX, inspired by the legendary Wolfenstein 3D. This project implements raycasting algorithms to create a first-person perspective view inside a maze environment.

Notifications You must be signed in to change notification settings

hel-asli/cub3D

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cub3D - 3D Game Engine

A 3D game engine built using raycasting techniques, inspired by classic games like Wolfenstein 3D.

Table of Contents

Overview

cub3D is a first-person 3D game engine built using raycasting techniques. The project renders a 3D-like environment from a 2D map, featuring textured walls, sprites, doors, and weapons. The engine supports player movement, collision detection, and interactive elements.

Features

  • First-person perspective rendering using raycasting
  • Textured walls, floors, and ceilings
  • Sprite rendering for enemies and objects
  • Door animations (sliding, hidden doors)
  • Weapon system with animations (pistol, knife)
  • Minimap for navigation
  • Loading screen
  • Collision detection
  • Map parsing from configuration files

MLX42 Library

The project uses the MLX42 library, a minimalist graphics library designed for rendering in C. MLX42 provides functions for:

  • Creating windows and images
  • Handling input events (keyboard, mouse)
  • Loading and manipulating textures
  • Drawing pixels and shapes

Key MLX42 functions used in the project:

  • mlx_init(): Initialize the library and create a window
  • mlx_texture_to_image(): Convert textures to images for rendering
  • mlx_put_pixel(): Draw individual pixels to an image
  • mlx_loop_hook(): Set up a function to be called each frame
  • mlx_key_hook(): Register keyboard event handlers

Map Parsing

The game reads map configurations from .cub files, which define:

  • Wall textures (north, south, east, west)
  • Floor and ceiling textures
  • The 2D map layout using characters:
    • 1: Wall
    • 0: Empty space
    • N/S/E/W: Player starting position and direction
    • P: Door
    • H: Hidden door
    • F: Enemy/sprite

The parsing process involves:

  1. Reading the file line by line
  2. Extracting texture paths and validating them
  3. Building the 2D map array
  4. Validating the map (checking for enclosed spaces, valid characters)
  5. Using flood fill algorithm to ensure the map is properly enclosed
void parse_map_content(t_cub *cub)
{
    if (check_texture(&cub->map))
        ft_exit("Error -Texture", cub, 1);
    if (check_map(&cub->map))
        ft_exit("Error :InValid map\n", cub, 1);
    build_check_map(&cub->map);
    if (is_valid_map(&cub->map))
        ft_exit("Error :\n InValid map\n", cub, 1);
}

How Raycasting Works

Raycasting is a rendering technique that creates a 3D-like perspective from a 2D map. Unlike true 3D rendering, raycasting is computationally efficient while still providing an immersive experience.

Basic Raycasting Algorithm

  1. Ray Casting: For each vertical column of the screen, a ray is cast from the player's position in the direction they're facing, plus an offset based on the column's position relative to the center of the screen.
cub->raycast.camera_x = 2 * ray / (double)WIDTH - 1;
cub->raycast.ray_dir.x = cub->player.dir.x + cub->player.plan.x * cub->raycast.camera_x;
cub->raycast.ray_dir.y = cub->player.dir.y + cub->player.plan.y * cub->raycast.camera_x;
  1. DDA (Digital Differential Analysis): The DDA algorithm is used to efficiently determine where the ray intersects with walls in the 2D grid.
void dda_perform(t_cub *cub, int ray)
{
    while (!cub->raycast.hit)
    {
        move_dda(cub);
        if (cub->map.map[cub->raycast.map_pos.y][cub->raycast.map_pos.x] == '1')
            cub->raycast.hit = 1;
        // Additional checks for doors and sprites
    }
    // Calculate perpendicular wall distance to avoid fisheye effect
    if (cub->raycast.side == HIT_VER)
        cub->raycast.perp_wall_dist = (cub->raycast.map_pos.x - cub->player.pos.x + 
            (1 - cub->raycast.step.x) / 2.0) / cub->raycast.ray_dir.x;
    else
        cub->raycast.perp_wall_dist = (cub->raycast.map_pos.y - cub->player.pos.y + 
            (1 - cub->raycast.step.y) / 2.0) / cub->raycast.ray_dir.y;
}
  1. Wall Height Calculation: The distance to the wall is used to calculate how tall the wall should appear on screen.
cub->raycast.line_height = (int)(HEIGHT / cub->raycast.perp_wall_dist);
cub->raycast.draw_start = HEIGHT / 2 - cub->raycast.line_height / 2;
cub->raycast.draw_end = cub->raycast.draw_start + cub->raycast.line_height;
  1. Texture Mapping: The appropriate texture is selected and mapped onto the wall based on which side was hit and the exact position of the hit.
cub->raycast.wall_x = cub->player.pos.y + (cub->raycast.perp_wall_dist * cub->raycast.ray_dir.y);
cub->raycast.wall_x -= floor(cub->raycast.wall_x);
cub->raycast.tx = (cub->raycast.wall_x * cub->raycast.texture->width);
  1. Z-Buffer: A z-buffer is maintained to track the distance to walls for each column, which is later used for sprite rendering.
cub->raycast.zbuffer[ray] = cub->raycast.perp_wall_dist;

Technical Concepts

Billboarding

Billboarding is a technique used to render 2D sprites in a 3D environment, ensuring they always face the camera. In cub3D, this is used for enemies and objects.

The implementation involves:

  1. Calculating the sprite's position relative to the player
  2. Transforming the sprite's coordinates to camera space
  3. Determining the sprite's screen position and dimensions
  4. Drawing the sprite with proper depth sorting
void compute_sprite_transform(t_cub *cub, t_sprite *sprite)
{
    // Calculate sprite position relative to player
    cub->vars.sprite_x = sprite->x - cub->player.pos.x;
    cub->vars.sprite_y = sprite->y - cub->player.pos.y;
    
    // Matrix inversion for camera space transformation
    cub->vars.inv_matrix = 1.0 / ((cub->player.plan.x * cub->player.dir.y) - 
                                  (cub->player.dir.x * cub->player.plan.y));
    
    // Transform sprite coordinates to camera space
    cub->vars.transform_x = cub->vars.inv_matrix * (cub->player.dir.y * cub->vars.sprite_x - 
                                                   cub->player.dir.x * cub->vars.sprite_y);
    cub->vars.transform_y = cub->vars.inv_matrix * (-cub->player.plan.y * cub->vars.sprite_x + 
                                                   cub->player.plan.x * cub->vars.sprite_y);
    
    // Calculate sprite's screen position
    cub->vars.sprite_screen_x = (int)(WIDTH / 2) * (1 + cub->vars.transform_x / cub->vars.transform_y);
}

Shadow Eye

Shadow Eye (or distance fog) is a technique that simulates atmospheric perspective by darkening objects based on their distance from the player. This enhances depth perception and creates a more realistic environment.

uint32_t get_color_fog(uint8_t *pixel, double distance)
{
    // Adjust color components based on distance
    // The further away, the darker the color
    t_colors colors;
    
    colors.red = pixel[0] / (1 + distance * distance * 0.0001);
    colors.green = pixel[1] / (1 + distance * distance * 0.0001);
    colors.blue = pixel[2] / (1 + distance * distance * 0.0001);
    colors.alpha = pixel[3];
    
    return (colors.red << 24 | colors.green << 16 | colors.blue << 8 | colors.alpha);
}

Texture Handling

The engine supports textured walls, floors, and ceilings. Textures are loaded from PNG files and mapped onto surfaces based on the ray intersection point.

For walls:

void walls_tex(t_cub *cub, int ray)
{
    int y = cub->raycast.draw_start;
    while (y < cub->raycast.draw_end)
    {
        // Calculate texture Y coordinate
        long tex_y = fmodf(cub->raycast.tex_pos, cub->raycast.texture->height);
        cub->raycast.tex_pos += cub->raycast.offset;
        
        // Get pixel from texture
        uint8_t *pixel = &cub->raycast.texture->pixels[(tex_y * cub->raycast.texture->width + 
                                                      cub->raycast.tx) * 4];
        
        // Apply fog effect and draw pixel
        uint32_t color = get_color_fog(pixel, cub->raycast.perp_wall_dist);
        mlx_put_pixel(cub->img, ray, y, color);
        y++;
    }
}

For floors and ceilings, a technique called "ray casting" is used to map textures onto horizontal surfaces:

void floor_texture(t_cub *cub, int ray, int y)
{
    while (y < HEIGHT - 1)
    {
        // Calculate ray position and direction
        float dt = HEIGHT / (2.0 * y - HEIGHT);
        t_vector fc_cord;
        fc_cord.x = cub->player.pos.x + dt * cub->raycast.ray_dir.x;
        fc_cord.y = cub->player.pos.y + dt * cub->raycast.ray_dir.y;
        
        // Sample texture at the calculated position
        t_vect_int tex_cord;
        tex_cord.x = (int)(fc_cord.x * tex->width) % tex->width;
        tex_cord.y = (int)(fc_cord.y * tex->height) % tex->height;
        
        // Draw pixel
        uint8_t *pixel = &tex->pixels[(tex_cord.y * tex->width + tex_cord.x) * 4];
        mlx_put_pixel(cub->img, ray, y, get_color_fog(pixel, dt));
        y++;
    }
}

Circular Minimap

The game features a circular minimap that provides players with a top-down view of their surroundings. The minimap is implemented with the following features:

  • Real-time updates based on player position
  • Circular mask for aesthetic appeal and focused visibility
  • Color-coded elements (walls, doors, enemies)
  • Player position and direction indicator

Implementation

The minimap is rendered using the following steps:

  1. Drawing the Background: A circular background is drawn with a semi-transparent color.
void draw_filled_circle(t_minimap *minimap, float radius, uint32_t color)
{
    int y = -radius;
    while (y <= radius)
    {
        int x = -radius;
        while (x <= radius)
        {
            if (x * x + y * y <= radius * radius)
            {
                t_vect_int c;
                c.x = minimap->center.x + x;
                c.y = minimap->center.y + y;
                if (c.x >= 0 && c.x < (int)minimap->img->width
                    && c.y >= 0 && c.y < (int)minimap->img->height)
                    mlx_put_pixel(minimap->img, c.x, c.y, color);
            }
            x++;
        }
        y++;
    }
}
  1. Calculating Visible Area: The minimap shows a portion of the map centered around the player.
void draw_minimap(t_cub *cub)
{
    t_vector pos;
    
    pos.x = cub->player.pos.x;
    pos.y = cub->player.pos.y;
    draw_filled_circle(&cub->minimap, cub->minimap.radius, 0x222222B4);
    cub->minimap.offset.x = cub->minimap.center.x / cub->minimap.tile_size - pos.x;
    cub->minimap.offset.y = cub->minimap.center.y / cub->minimap.tile_size - pos.y;
    draw_tiles(cub, pos);
    // Draw player and direction indicator
    draw_filled_circle(&cub->minimap, 3, 0x800080FF);
    mlx_put_pixel(cub->minimap.img, cub->minimap.center.x + cub->player.dir.x * 8,
        cub->minimap.center.y + cub->player.dir.y * 8, 0x800080FF);
    draw_circle_border(cub, BLACK);
}
  1. Drawing Map Elements: Different map elements are drawn with distinct colors.
void tile_helper(t_cub *cub, int x, int y)
{
    uint32_t color = 0x000000;
    
    if (cub->map.map[y][x] == '1')
        color = BLUE;
    else if (cub->map.map[y][x] == '0')
        color = WHITE;
    else if (cub->map.map[y][x] == 'P' || cub->map.map[y][x] == 'H')
        color = BROWN;
    else if (cub->map.map[y][x] == 'F')
        color = GREEN;
    
    if (color != 0x000000)
    {
        draw_tile(cub, x + cub->minimap.offset.x, y + cub->minimap.offset.y, color);
    }
}
  1. Circular Border: A border is drawn around the minimap for visual clarity.
void draw_circle_border(t_cub *cub, uint32_t color)
{
    int y = -cub->minimap.radius;
    while (y <= cub->minimap.radius)
    {
        int x = -cub->minimap.radius;
        while (x <= cub->minimap.radius)
        {
            float d = x * x + y * y;
            if (d <= cub->minimap.radius * cub->minimap.radius
                && d >= (cub->minimap.radius - 2) * (cub->minimap.radius - 2))
            {
                t_vect_int c;
                c.x = cub->minimap.center.x + x;
                c.y = cub->minimap.center.y + y;
                if (c.x >= 0 && c.x < (int)cub->minimap.img->width
                    && c.y >= 0 && c.y < (int)cub->minimap.img->height)
                    mlx_put_pixel(cub->minimap.img, c.x, c.y, color);
            }
            x++;
        }
        y++;
    }
}

Installation & Usage

  1. Clone the repository:
  • using https URL
    git clone https://github.com/Mouad-kimdil/cub3D.git
    
  • or using SSH
    [email protected]:Mouad-kimdil/cub3D.git
    
  1. Compile the project:
  • to compile the mandatory part
    make
    
  • to compile the bonus part
    make bonus
    
  1. Run the game with a map file:
  • for mandatory part
    ./cub3D assets/man_maps/map.cub
    
  • for bonus part
    ./cub3D_bonus assets/bonus_map/map.cub
    

Controls

  • W/A/S/D: Move forward/left/backward/right
  • Arrow keys: Rotate camera
  • TAB: To Interact with mouse cursor
  • Mouse movement: Rotate camera
  • Left mouse button: Shoot
  • O/C: Interact with doors
  • 1/2: Switch weapons
  • Y: Interact with weapons
  • R: Reload weapon

Resources

The development of this project was informed by the following resources:

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Contributors

About

A 3D maze exploration game built with C and miniLibX, inspired by the legendary Wolfenstein 3D. This project implements raycasting algorithms to create a first-person perspective view inside a maze environment.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 76.2%
  • Makefile 11.6%
  • C++ 8.3%
  • CMake 2.4%
  • Python 1.2%
  • GLSL 0.1%
  • Other 0.2%