Skip to content

Instantly share code, notes, and snippets.

@gerred
Created December 20, 2024 01:04
Show Gist options
  • Save gerred/d323bfb35539668073b0ecdd45f96586 to your computer and use it in GitHub Desktop.
Save gerred/d323bfb35539668073b0ecdd45f96586 to your computer and use it in GitHub Desktop.
Here's a breakdown of the thought process to generate the Doom-like raytracer:
1. **Understand the Core Request:** The user wants a "Doom-like raytracer game." This immediately brings several key concepts to mind:
* **Raycasting, not Raytracing:** Doom uses a fast, simpler technique called raycasting, not full raytracing, for its 2.5D effect. This is crucial for performance in a real-time game.
* **Walls, not Full 3D:** Doom renders vertical walls, not true 3D objects. The floor and ceiling are often simple textures.
* **Limited Vertical Look:** Basic raycasters often don't have true up/down look.
* **Movement and Interaction:** The game needs to allow player movement and possibly some interaction (though this initial request doesn't emphasize complex interaction).
2. **High-Level Structure:** A game needs a main loop, input handling, rendering, and some way to represent the game world. I'll start with a basic game loop using Pygame.
3. **Representing the World:** A 2D map grid is the standard way to represent the level in a raycaster. Each cell in the grid can represent a wall or an empty space. A simple list of lists (or NumPy array if performance becomes a concern) will work. I need to define what number represents a wall.
4. **Raycasting Algorithm (the core):**
* **Iterate through screen columns:** For each vertical line of pixels on the screen, cast a ray.
* **Ray Origin and Direction:** The ray starts at the player's position. The direction depends on the current screen column and the player's view angle.
* **Step through the grid:** Increment the ray's position in small steps until it hits a wall.
* **Distance Calculation:** Calculate the distance to the wall. This is crucial for determining the height of the wall on the screen.
* **Wall Height:** The height of the wall on the screen is inversely proportional to the distance. Closer walls are taller.
* **Wall Rendering:** Draw a vertical line of the appropriate color or texture (initially, just color) for each wall hit.
5. **Player Representation:** The player needs a position (x, y coordinates) and an orientation (angle).
6. **Movement:** Implement basic movement based on keyboard input (W, A, S, D). Forward/backward movement changes the player's position based on their angle. Left/right movement changes the player's angle.
7. **Rendering the Scene:**
* **Clear the screen:** Start with a clean slate in each frame.
* **Cast rays for each column:** Implement the raycasting algorithm.
* **Draw floor and ceiling (optional but adds to the feel):** Simple rectangles or colors can represent the floor and ceiling.
* **Update the display:** Show the rendered frame.
8. **Refinement and Enhancements (Initial Thoughts - not all implemented in the basic version):**
* **Texturing:** Instead of solid colors, use textures for walls. This involves calculating texture coordinates based on the ray's intersection point.
* **Variable Wall Heights:** The map can store different wall heights for more interesting level design.
* **Sprite Rendering:** Add enemies or objects represented by sprites. This requires sorting sprites by distance and drawing them after the walls.
* **Collision Detection:** Prevent the player from walking through walls.
* **Basic AI:** For enemies.
* **Sound Effects:** Enhance the atmosphere.
* **More complex maps:** Load maps from external files.
9. **Code Structure:** Break the code into functions for clarity and organization:
* `game_loop()`: The main game loop.
* `handle_input()`: Processes keyboard events.
* `cast_ray()`: Performs the core raycasting logic for a single ray.
* `render()`: Draws the scene.
* `draw_wall_stripe()`: Draws a single vertical line representing a wall.
10. **Initial Implementation (Focus on the Core):** Start with a very basic version that just renders walls. Get the core raycasting working correctly before adding complexity. Use solid colors for walls initially.
11. **Iterative Development:** Build upon the basic implementation, adding features one at a time (e.g., movement, then floor/ceiling, then simple texturing).
12. **Debugging:** Use print statements and visual inspection to debug issues. Make sure the ray angles and distances are calculated correctly.
By following this thought process, focusing on the core raycasting algorithm first and then gradually adding features, we arrive at a functional basic Doom-like raytracer. The key is to simplify the problem initially and incrementally add complexity.
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Doom-like Raytracer")
# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
GRAY = (100, 100, 100)
# Player properties
player_x = 2
player_y = 2
player_angle = 0 # Radians
fov = math.pi / 3 # Field of view
# Map (0 = empty, 1 = wall)
MAP = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]
MAP_WIDTH = len(MAP[0])
MAP_HEIGHT = len(MAP)
TILE_SIZE = 64 # For visual representation of the map
def get_map_value(x, y):
"""Returns the value at the given map coordinates."""
if 0 <= int(y) < MAP_HEIGHT and 0 <= int(x) < MAP_WIDTH:
return MAP[int(y)][int(x)]
return 1 # Treat out-of-bounds as a wall
def cast_ray(ray_angle):
"""Casts a single ray and returns the distance to the wall."""
ray_x = player_x
ray_y = player_y
ray_dir_x = math.cos(ray_angle)
ray_dir_y = math.sin(ray_angle)
step_x = 1 if ray_dir_x > 0 else -1
step_y = 1 if ray_dir_y > 0 else -1
side_dist_x = (int(ray_x + step_x) - ray_x) * abs(1 / ray_dir_x) if ray_dir_x != 0 else float('inf')
side_dist_y = (int(ray_y + step_y) - ray_y) * abs(1 / ray_dir_y) if ray_dir_y != 0 else float('inf')
map_check_x = int(ray_x)
map_check_y = int(ray_y)
hit = False
while not hit:
if side_dist_x < side_dist_y:
side_dist_x += abs(1 / ray_dir_x)
map_check_x += step_x
side = 0 # Hit a vertical wall
else:
side_dist_y += abs(1 / ray_dir_y)
map_check_y += step_y
side = 1 # Hit a horizontal wall
if get_map_value(map_check_x, map_check_y) == 1:
hit = True
# Calculate distance
if side == 0:
distance = (map_check_x - ray_x + (1 - step_x) / 2) / ray_dir_x
else:
distance = (map_check_y - ray_y + (1 - step_y) / 2) / ray_dir_y
return distance
def render():
"""Renders the scene."""
screen.fill(BLACK) # Clear the screen
num_rays = SCREEN_WIDTH
delta_angle = fov / num_rays
for i in range(num_rays):
ray_angle = player_angle - fov / 2 + i * delta_angle
distance = cast_ray(ray_angle)
# Calculate projected wall height
wall_height = SCREEN_HEIGHT / distance
# Calculate start and end points for the wall slice
wall_start = int((SCREEN_HEIGHT - wall_height) / 2)
wall_end = int((SCREEN_HEIGHT + wall_height) / 2)
# Choose wall color based on distance (simple shading)
brightness = int(255 - min(distance * 20, 200))
wall_color = (brightness, brightness, brightness)
# Draw the wall slice
pygame.draw.line(screen, wall_color, (i, wall_start), (i, wall_end))
# Optional: Draw a simple floor and ceiling
pygame.draw.rect(screen, GRAY, (0, SCREEN_HEIGHT // 2, SCREEN_WIDTH, SCREEN_HEIGHT // 2)) # Floor
pygame.draw.rect(screen, BLUE, (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT // 2)) # Ceiling
# Optional: Draw the 2D map for debugging
# draw_map()
pygame.display.flip()
def handle_input():
"""Handles player input."""
keys = pygame.key.get_pressed()
move_speed = 0.1
rotation_speed = 0.05
if keys[pygame.K_w]:
new_x = player_x + move_speed * math.cos(player_angle)
new_y = player_y + move_speed * math.sin(player_angle)
if get_map_value(new_x, new_y) == 0:
player_x = new_x
player_y = new_y
if keys[pygame.K_s]:
new_x = player_x - move_speed * math.cos(player_angle)
new_y = player_y - move_speed * math.sin(player_angle)
if get_map_value(new_x, new_y) == 0:
player_x = new_x
player_y = new_y
if keys[pygame.K_a]:
player_angle -= rotation_speed
if keys[pygame.K_d]:
player_angle += rotation_speed
if keys[pygame.K_LEFT]:
player_angle -= rotation_speed
if keys[pygame.K_RIGHT]:
player_angle += rotation_speed
# Keep angle within 0 to 2*pi
player_angle %= (2 * math.pi)
def draw_map():
"""Draws a simple 2D representation of the map."""
for y in range(MAP_HEIGHT):
for x in range(MAP_WIDTH):
tile_x = x * TILE_SIZE
tile_y = y * TILE_SIZE
if MAP[y][x] == 1:
pygame.draw.rect(screen, WHITE, (tile_x, tile_y, TILE_SIZE, TILE_SIZE), 1)
else:
pygame.draw.rect(screen, GRAY, (tile_x, tile_y, TILE_SIZE, TILE_SIZE), 1)
# Draw player position on the map
player_screen_x = player_x * TILE_SIZE
player_screen_y = player_y * TILE_SIZE
pygame.draw.circle(screen, RED, (int(player_screen_x), int(player_screen_y)), 8)
# Draw player's view direction
line_length = 20
end_x = player_screen_x + line_length * math.cos(player_angle)
end_y = player_screen_y + line_length * math.sin(player_angle)
pygame.draw.line(screen, GREEN, (int(player_screen_x), int(player_screen_y)), (int(end_x), int(end_y)))
# Game loop
running = True
clock = pygame.time.Clock()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
handle_input()
render()
clock.tick(60) # Limit frame rate to 60 FPS
pygame.quit()
```
**Explanation:**
1. **Initialization:**
- Initializes Pygame.
- Sets up the screen dimensions and window title.
- Defines colors.
- Sets up player properties (position, angle, field of view).
- Defines the game map as a 2D list (0 for empty space, 1 for walls).
2. **`get_map_value(x, y)`:**
- A helper function to safely access the map array, returning 1 (wall) if the coordinates are out of bounds.
3. **`cast_ray(ray_angle)`:**
- This is the core of the raycasting algorithm.
- Takes a `ray_angle` as input.
- Starts the ray at the player's position.
- Calculates the direction of the ray (`ray_dir_x`, `ray_dir_y`).
- Uses the DDA (Digital Differential Analyzer) algorithm to efficiently step through the grid along the ray's path.
- `side_dist_x` and `side_dist_y` track the distances to the next vertical and horizontal grid lines.
- The loop continues until the ray hits a wall (where `get_map_value` returns 1).
- Calculates the distance to the hit wall.
- Returns the calculated distance.
4. **`render()`:**
- Clears the screen.
- Determines the number of rays to cast (equal to the screen width for a vertical line per pixel).
- Calculates the angle increment between each ray.
- Iterates through each column of pixels (representing a ray):
- Calculates the `ray_angle` for the current column.
- Calls `cast_ray()` to get the distance to the wall hit by that ray.
- Calculates the projected `wall_height` on the screen based on the distance (closer walls are taller).
- Calculates the `wall_start` and `wall_end` y-coordinates for drawing the wall slice.
- Applies simple shading to the wall color based on the distance.
- Draws a vertical line (`pygame.draw.line`) representing the wall slice.
- Optionally draws a simple floor and ceiling using `pygame.draw.rect`.
- Optionally calls `draw_map()` for debugging.
- Updates the display (`pygame.display.flip()`).
5. **`handle_input()`:**
- Gets the state of all keyboard keys.
- Implements basic player movement:
- 'W' and 'S' to move forward and backward.
- 'A', 'D', LEFT ARROW, and RIGHT ARROW to rotate the player's angle.
- Includes basic collision detection by checking if the next position is a wall.
- Keeps the player's angle within the range of 0 to 2*pi.
6. **`draw_map()` (Optional for Debugging):**
- Draws a 2D representation of the map on the screen.
- Shows walls as white rectangles and empty spaces as gray rectangles.
- Draws the player's position as a red circle and their viewing direction as a green line.
7. **Game Loop:**
- The main loop of the game.
- Handles events (like closing the window).
- Calls `handle_input()` to process user input.
- Calls `render()` to draw the scene.
- Limits the frame rate to 60 FPS using `clock.tick(60)`.
**How it Achieves the "Doom-like" Look:**
- **Raycasting:** The core technique is raycasting, which is what classic Doom used. It's faster than full 3D rendering because it only calculates distances to walls along specific rays.
- **Vertical Wall Slices:** The rendering process creates vertical slices of walls, giving the illusion of depth.
- **Limited Verticality:** This basic example doesn't have true up/down looking. You're essentially looking straight ahead.
- **No True 3D Objects:** Everything is rendered as walls perpendicular to the floor. There are no truly 3D objects.
**To Make it More "Doom-like":**
- **Texturing:** Instead of solid colors, load textures for the walls and map the texture to the vertical slices.
- **Sprites:** Add enemies and objects as sprites that are rendered on top of the walls.
- **Floor and Ceiling Textures:** Apply textures to the floor and ceiling.
- **Variable Wall Heights:** Allow walls of different heights to create more complex environments.
- **Basic AI:** Implement basic movement and behavior for enemies.
- **Sound Effects:** Add sound effects for footsteps, shooting, etc.
- **More Complex Maps:** Load map data from external files instead of hardcoding it.
- **Weapon and Shooting Mechanics:** Implement weapons and the ability to shoot.
This basic raytracer provides a foundation for building a more complex and engaging Doom-like game in Python. You can gradually add features and complexity to achieve your desired gameplay.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment