A 3D game engine built using raycasting techniques, inspired by classic games like Wolfenstein 3D.
- Overview
- Features
- MLX42 Library
- Map Parsing
- How Raycasting Works
- Technical Concepts
- Installation & Usage
- Resources
- Contributions
- Contributors
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.
- 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
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 windowmlx_texture_to_image(): Convert textures to images for renderingmlx_put_pixel(): Draw individual pixels to an imagemlx_loop_hook(): Set up a function to be called each framemlx_key_hook(): Register keyboard event handlers
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: Wall0: Empty spaceN/S/E/W: Player starting position and directionP: DoorH: Hidden doorF: Enemy/sprite
The parsing process involves:
- Reading the file line by line
- Extracting texture paths and validating them
- Building the 2D map array
- Validating the map (checking for enclosed spaces, valid characters)
- 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);
}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.
- 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;- 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;
}- 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;- 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);- 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;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:
- Calculating the sprite's position relative to the player
- Transforming the sprite's coordinates to camera space
- Determining the sprite's screen position and dimensions
- 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 (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);
}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++;
}
}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
The minimap is rendered using the following steps:
- 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++;
}
}- 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);
}- 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);
}
}- 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++;
}
}- Clone the repository:
- using https URL
git clone https://github.com/Mouad-kimdil/cub3D.git - or using SSH
[email protected]:Mouad-kimdil/cub3D.git
- Compile the project:
- to compile the mandatory part
make - to compile the bonus part
make bonus
- 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
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 doors1/2: Switch weaponsY: Interact with weaponsR: Reload weapon
The development of this project was informed by the following resources:
- Lodev's Raycasting Tutorial: Comprehensive guide on raycasting techniques, billboarding, and texture mapping
- Math is Fun - Matrix Inverse: Reference for matrix operations used in sprite transformations
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.