Created
December 20, 2024 01:04
-
-
Save gerred/d323bfb35539668073b0ecdd45f96586 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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