Created
December 20, 2024 01:06
-
-
Save gerred/aa3a44764af9e3986fa68e3a953d504f to your computer and use it in GitHub Desktop.
This file contains 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 behind crafting the Python Doom-like raycaster: | |
1. **Understanding the Core Concept:** The request is for a "Doom-like raytracer." It's crucial to recognize that classic Doom *isn't* a raytracer in the modern sense. It uses a technique called "raycasting," which is a simplified form of ray tracing optimized for speed and the specific constraints of early 3D graphics. The key difference is that raycasting only casts rays in a 2D plane to determine wall intersections, not for lighting, reflections, or other advanced effects. | |
2. **Simplifying the Problem:** A full 3D raytracer is complex. To make this achievable in a reasonable scope, the focus needs to be on the core raycasting algorithm for rendering walls. Things like enemies, items, and complex level geometry can be considered as potential extensions, but the initial focus should be on the fundamental wall rendering. | |
3. **Core Raycasting Algorithm:** The fundamental idea is to cast rays from the player's position at different angles. For each ray, determine the distance to the nearest wall. This distance is then used to calculate the height of the wall slice to draw on the screen. | |
4. **Data Structures:** We need to represent the game world. A 2D grid (list of lists) is a natural choice for the map, where each cell represents a tile and can indicate whether it's a wall or empty space. The player's position and orientation need to be stored as variables. | |
5. **Rendering Logic:** | |
* **Iterate through screen columns:** For each vertical line of pixels on the screen, we need to cast a ray. | |
* **Calculate ray direction:** The angle of the ray depends on the screen column and the player's viewing angle. | |
* **Ray marching:** Start at the player's position and incrementally move along the ray's direction until a wall is hit. | |
* **Distance calculation:** Calculate the distance between the player and the hit point. | |
* **Wall height calculation:** The height of the wall slice is inversely proportional to the distance. Closer walls appear taller. | |
* **Texture mapping (optional but important for the "Doom-like" feel):** Determine which part of the wall texture to draw based on the hit point. | |
* **Drawing:** Draw the vertical slice of the wall on the screen. | |
6. **Player Movement and Controls:** Basic movement involves changing the player's position and orientation based on keyboard input. Forward/backward movement and turning are essential. | |
7. **Visual Representation:** The `pygame` library is a good choice for handling graphics and input in Python. It provides functions for drawing shapes, handling events, and managing the display. | |
8. **Code Structure:** Organize the code into logical functions and classes. A `Game` class can encapsulate the game state and logic. Functions for rendering, handling input, and updating the game state are necessary. | |
9. **Step-by-Step Implementation (Trial and Error):** | |
* **Basic setup:** Initialize `pygame`, create a window. | |
* **Map representation:** Create a simple 2D map. | |
* **Player representation:** Store player position and angle. | |
* **Raycasting loop (initial version):** Iterate through screen columns and cast rays. Focus on getting the basic wall detection working. | |
* **Distance calculation:** Implement the distance formula. | |
* **Wall height calculation:** Implement the formula to determine wall slice height. | |
* **Drawing walls (basic):** Draw simple colored rectangles for walls. | |
* **Texture mapping:** Load wall textures and map them to the wall slices. This is a crucial step for the visual style. | |
* **Player movement:** Implement basic forward/backward and turning controls. | |
* **Optimization (if needed):** Look for ways to improve performance, although for a simple example, this might not be critical. | |
* **Refinement:** Clean up the code, add comments, and improve the visual presentation. | |
10. **Addressing the "Doom-like" aspect:** The key elements are: | |
* **Perspective correction:** Walls should appear wider at the bottom and narrower at the top. | |
* **Textured walls:** This is essential for the visual style. | |
* **Limited vertical FOV:** Doom has a relatively narrow vertical field of view. | |
* **Simple level geometry:** Focus on orthogonal walls. | |
11. **Iterative Development:** Start with a very basic version and gradually add features. Don't try to implement everything at once. Test frequently and debug as you go. | |
**Self-Correction/Refinement during the process:** | |
* **Initial thought:** Maybe start with a full 3D raytracer. **Correction:** Too complex for a beginner example. Focus on the raycasting core. | |
* **Problem:** Walls are appearing distorted. **Solution:** Implement proper perspective correction. | |
* **Problem:** The game feels static. **Solution:** Add player movement and controls. | |
* **Problem:** The visuals are too plain. **Solution:** Implement texture mapping. | |
* **Problem:** Performance is slow (if it were a more complex implementation). **Solution:** Optimize ray marching, pre-calculate values, etc. | |
By following these steps and iteratively refining the code, a functional and visually appealing Doom-like raycaster can be created. The key is to break down the problem into smaller, manageable parts and focus on the core mechanics first..init() | |
# Screen dimensions | |
SCREEN_WIDTH = 640 | |
SCREEN_HEIGHT = 480 | |
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) | |
pygame.display.set_caption("Doom-like Raycaster") | |
# Colors | |
WHITE = (255, 255, 255) | |
BLACK = (0, 0, 0) | |
RED = (255, 0, 0) | |
GREEN = (0, 255, 0) | |
BLUE = (0, 0, 255) | |
# Player properties | |
player_x = 2.0 | |
player_y = 2.0 | |
player_angle = 0.0 | |
fov = math.pi / 3.0 # Field of view | |
# Map (1 = wall, 0 = empty) | |
map_data = [ | |
[1, 1, 1, 1, 1, 1, 1, 1], | |
[1, 0, 0, 0, 0, 0, 0, 1], | |
[1, 0, 1, 1, 0, 1, 0, 1], | |
[1, 0, 1, 0, 0, 1, 0, 1], | |
[1, 0, 1, 0, 0, 0, 0, 1], | |
[1, 0, 0, 0, 0, 1, 0, 1], | |
[1, 0, 0, 0, 0, 0, 0, 1], | |
[1, 1, 1, 1, 1, 1, 1, 1] | |
] | |
map_size_x = len(map_data[0]) | |
map_size_y = len(map_data) | |
tile_size = 64 | |
# Function to draw the map (for debugging) | |
def draw_map(): | |
for y in range(map_size_y): | |
for x in range(map_size_x): | |
if map_data[y][x] == 1: | |
pygame.draw.rect(screen, WHITE, (x * tile_size, y * tile_size, tile_size, tile_size), 1) | |
# Function to cast a single ray | |
def cast_ray(ray_angle): | |
ray_x = player_x | |
ray_y = player_y | |
ray_dir_x = math.cos(ray_angle) | |
ray_dir_y = math.sin(ray_angle) | |
map_x = int(ray_x) | |
map_y = int(ray_y) | |
delta_dist_x = abs(1 / ray_dir_x) if ray_dir_x != 0 else 1e30 | |
delta_dist_y = abs(1 / ray_dir_y) if ray_dir_y != 0 else 1e30 | |
step_x = -1 if ray_dir_x < 0 else 1 | |
step_y = -1 if ray_dir_y < 0 else 1 | |
side_dist_x = (map_x + 1 - ray_x) * delta_dist_x if ray_dir_x > 0 else (ray_x - map_x) * delta_dist_x | |
side_dist_y = (map_y + 1 - ray_y) * delta_dist_y if ray_dir_y > 0 else (ray_y - map_y) * delta_dist_y | |
hit = False | |
side = 0 # 0 for vertical hit, 1 for horizontal hit | |
while not hit: | |
if side_dist_x < side_dist_y: | |
side_dist_x += delta_dist_x | |
map_x += step_x | |
side = 0 | |
else: | |
side_dist_y += delta_dist_y | |
map_y += step_y | |
side = 1 | |
if 0 <= map_y < map_size_y and 0 <= map_x < map_size_x and map_data[map_y][map_x] == 1: | |
hit = True | |
# Calculate the distance to the wall | |
if side == 0: | |
perp_wall_dist = (side_dist_x - delta_dist_x) | |
else: | |
perp_wall_dist = (side_dist_y - delta_dist_y) | |
return perp_wall_dist, side | |
# Game loop | |
running = True | |
clock = pygame.time.Clock() | |
while running: | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
if event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_LEFT: | |
player_angle -= 0.1 | |
if event.key == pygame.K_RIGHT: | |
player_angle += 0.1 | |
if event.key == pygame.K_UP: | |
move_x = math.cos(player_angle) * 0.1 | |
move_y = math.sin(player_angle) * 0.1 | |
if 0 <= int(player_y + move_y) < map_size_y and map_data[int(player_y + move_y)][int(player_x)] == 0: | |
player_y += move_y | |
if 0 <= int(player_x + move_x) < map_size_x and map_data[int(player_y)][int(player_x + move_x)] == 0: | |
player_x += move_x | |
if event.key == pygame.K_DOWN: | |
move_x = -math.cos(player_angle) * 0.1 | |
move_y = -math.sin(player_angle) * 0.1 | |
if 0 <= int(player_y + move_y) < map_size_y and map_data[int(player_y + move_y)][int(player_x)] == 0: | |
player_y += move_y | |
if 0 <= int(player_x + move_x) < map_size_x and map_data[int(player_y)][int(player_x + move_x)] == 0: | |
player_x += move_x | |
# Clear the screen | |
screen.fill(BLACK) | |
# Raycasting | |
num_rays = SCREEN_WIDTH | |
delta_angle = fov / num_rays | |
for i in range(num_rays): | |
ray_screen_x = i - num_rays // 2 | |
ray_angle = player_angle + math.atan2(ray_screen_x, num_rays // 2 / math.tan(fov / 2)) | |
distance, side_hit = cast_ray(ray_angle) | |
# Calculate wall height | |
line_height = int(SCREEN_HEIGHT / distance) | |
# Calculate start and end points for the wall slice | |
draw_start = int((SCREEN_HEIGHT - line_height) / 2) | |
draw_end = int((SCREEN_HEIGHT + line_height) / 2) | |
# Choose wall color based on which side was hit | |
wall_color = WHITE | |
if side_hit == 1: | |
wall_color = (150, 150, 150) # Make horizontal walls slightly darker | |
# Draw the wall slice | |
pygame.draw.line(screen, wall_color, (i, draw_start), (i, draw_end)) | |
# Optional: Draw the map and player for debugging | |
# draw_map() | |
# pygame.draw.circle(screen, RED, (int(player_x * tile_size), int(player_y * tile_size)), 8) | |
# line_end_x = int((player_x + math.cos(player_angle)) * tile_size) | |
# line_end_y = int((player_y + math.sin(player_angle)) * tile_size) | |
# pygame.draw.line(screen, GREEN, (int(player_x * tile_size), int(player_y * tile_size)), (line_end_x, line_end_y), 2) | |
# Update the display | |
pygame.display.flip() | |
# Control the frame rate | |
clock.tick(60) | |
# Quit Pygame | |
pygame.quit() | |
``` | |
**Explanation:** | |
1. **Initialization:** | |
- Initializes Pygame. | |
- Sets up the screen dimensions and creates the display surface. | |
- Defines colors. | |
- Sets up player properties (position, angle, field of view). | |
- Defines the game map as a 2D list (1 for wall, 0 for empty). | |
2. **`draw_map()` Function (Debugging):** | |
- This function is for visualizing the map layout. It draws rectangles for walls. You can uncomment the call to this function in the game loop to see the map. | |
3. **`cast_ray()` Function (Core Raycasting Logic):** | |
- Takes a `ray_angle` as input. | |
- **Ray Setup:** Initializes the ray's starting position and direction. | |
- **Map Check:** Determines the integer coordinates of the map cell the ray is currently in. | |
- **Distance Calculation:** Calculates the distance the ray needs to travel along its x and y directions to cross one grid cell boundary (`delta_dist_x`, `delta_dist_y`). | |
- **Stepping:** Determines the direction to step in the x and y directions based on the ray's direction (`step_x`, `step_y`). | |
- **Initial Side Distances:** Calculates the initial distances from the ray's starting point to the nearest vertical and horizontal grid lines (`side_dist_x`, `side_dist_y`). | |
- **DDA (Digital Differential Analyzer) Algorithm:** | |
- Enters a loop that continues until a wall is hit. | |
- Compares `side_dist_x` and `side_dist_y` to determine which grid line the ray will hit next. | |
- Updates the ray's position and the corresponding side distance. | |
- Checks if the new map cell contains a wall. | |
- **Distance to Wall:** Calculates the perpendicular distance from the player to the wall to avoid the "fish-eye" effect. | |
- Returns the `perp_wall_dist` and `side` (0 for vertical hit, 1 for horizontal hit). | |
4. **Game Loop:** | |
- **Event Handling:** Checks for events like quitting and key presses. | |
- **Player Movement:** Updates the player's position and angle based on key presses. It includes basic collision detection to prevent the player from walking through walls. | |
- **Clear Screen:** Fills the screen with black. | |
- **Raycasting Loop:** | |
- Iterates through each vertical column of pixels on the screen (representing each ray). | |
- Calculates the `ray_angle` for the current screen column, adjusting for the player's angle and field of view. | |
- Calls `cast_ray()` to get the distance to the wall and the side hit. | |
- **Wall Height Calculation:** Calculates the height of the vertical wall slice to draw based on the distance. Closer walls appear taller. | |
- **Draw Start/End:** Calculates the vertical starting and ending points for the wall slice to center it on the screen. | |
- **Wall Color:** Chooses a color for the wall slice. The code makes horizontal walls slightly darker to give a sense of depth. | |
- **Draw Wall Slice:** Draws a vertical line representing the wall slice. | |
- **Optional Debugging:** Uncomment the lines to draw the map and player position for debugging purposes. | |
- **Update Display:** Updates the entire screen to show the rendered scene. | |
- **Frame Rate Control:** Limits the frame rate to 60 frames per second. | |
**How it Creates the "Doom-like" Effect:** | |
- **Raycasting:** The core technique is raycasting, which is how the original Doom rendered its 3D environment. It's a simplified form of ray tracing that's efficient for rendering walls. | |
- **Perspective Correction:** The calculation of `perp_wall_dist` helps to correct the distortion that would occur if you simply used the direct distance along the ray. This makes the walls appear straight. | |
- **Field of View (FOV):** The `fov` variable controls how wide the player's view is. A narrower FOV is more characteristic of classic Doom. | |
- **Wall Height Based on Distance:** Walls closer to the player are rendered taller, creating the illusion of depth. | |
- **Simple Geometry:** The map is made of simple orthogonal walls, which is typical of early 3D games. | |
- **Texturing (Not Implemented):** While this example doesn't include texture mapping, that's a crucial element of the Doom look. You would need to determine which part of a wall texture the ray hits and map that to the drawn wall slice. | |
**To Extend This:** | |
- **Texturing:** Load wall textures and map them to the wall slices based on the hit point. | |
- **Floor and Ceiling:** Implement rendering of the floor and ceiling using similar raycasting principles. | |
- **Sprites (Enemies, Items):** Render 2D sprites that are projected onto the 3D scene. | |
- **Level Design:** Create more complex and interesting level layouts. | |
- **Gameplay Mechanics:** Add player interaction, shooting, enemies, etc. | |
This code provides a basic foundation for a Doom-like raycaster. You can build upon it to create a more feature-rich game. Remember that the original Doom used a highly optimized and specialized engine, so this Python implementation is for educational purposes and might not be as performant for very complex scenes. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment