Search code examples
pythonpygameraycasting

Problem with recognising where a ray in raycaster intersects a wall along the horizontal axis


I am making a ray caster in python with pygame. https://youtu.be/gYRrGTC7GtA?t=407 The problem is the cast_rays function. I have commented out the previous method that I was using.I used the above video written in C and adapted it to Python. I wanted to use the raycasting algorithm in the above video since it would be casted then checking pixel by pixel.I have tried checking only horizontal lines and checking to see if there is a wall there. But, it doesn't work.

import pygame
import sys
import math

pygame.init()

screen_height = 480
screen_width = screen_height * 2
map_size = 8
tile_size = screen_height / 8
player_x = screen_width / 4
player_y = screen_width / 4
FOV = math.pi / 3
HALF_FOV = FOV / 2
player_angle = math.pi + math.pi / 2
casted_rays = 120
step_angle = FOV / casted_rays
scale = screen_height / casted_rays

MAP = (
    '########'
    '#   #  #'
    '#   #  #'
    '#  ##  #'
    '#      #'
    '###    #'
    '###    #'
    '########'
    )
def draw_map():
    for row in range(8):
        for col in range(8):
            # square index
            square = row * map_size + col
            pygame.draw.rect(win, (200,200,200) if MAP[square] == '#' else (100,100,100),(row * tile_size, col * tile_size, tile_size - 2, tile_size - 2))
    pygame.draw.circle(win, (255,0,0), (player_x, player_y), 8)
    #pygame.draw.line(win, (0,255,0), (player_x, player_y), (player_x + math.cos(player_angle) * 50,  player_y + math.sin(player_angle) * 50) ,3)
    #pygame.draw.line(win, (0,255,0), (player_x, player_y), (player_x + math.cos(player_angle - HALF_FOV) * 50,  player_y + math.sin(player_angle - HALF_FOV) * 50) ,3)
    #pygame.draw.line(win, (0,255,0), (player_x, player_y), (player_x + math.cos(player_angle + HALF_FOV) * 50,  player_y + math.sin(player_angle + HALF_FOV) * 50) ,3)
def cast_rays():
    '''
    start_angle = player_angle - HALF_FOV
    for ray in range(casted_rays):
        for depth in range(screen_height):
            target_x = player_x + math.cos(start_angle) * depth
            target_y = player_y + math.sin(start_angle) * depth
            pygame.draw.line(win, (255,255,0), (player_x, player_y), (target_x, target_y) ,3)
            row = int(target_x / tile_size)
            col = int(target_y / tile_size)
            
            square = int(row * map_size + col)

            if MAP[square] == "#":
                pygame.draw.rect(win, (0,255, 0),(row * tile_size, col * tile_size, tile_size - 2, tile_size - 2))
                wall_height = 21000 / (depth + 0.00001)
                pygame.draw.rect(win, (100,100,100), (screen_height + ray * scale, (screen_height - wall_height) / 2 ,scale,wall_height))
                break


        start_angle += step_angle
    '''
    #dof = 0
    r = 0
    ra = player_angle
    ry = 0
    rx = 0
    while r < 1:
        dof = 0
        aTan = -1/math.tan(ra);
        if ra > math.pi:
            ry = ((ry * tile_size) / tile_size) - 0.0001
            rx = (player_y - ry) * aTan + player_x
            yo = -64
            xo = -yo * aTan
        if ra < math.pi:
            ry = ((ry * tile_size) / tile_size) + 64
            rx = (player_y - ry) * aTan + player_x
            yo = 64
            xo = -yo * aTan
        if ra == 0 or ra == math.pi:
            dof = 8
            ra = 0
            rx = player_x
            ry = player_y
        while dof < 8:
            mx = rx * tile_size
            my = ry * tile_size
            mp = my * tile_size
            if mp < tile_size * 8 * tile_size * 8 and MAP[int(mp)] == '#':
                dof = 8
            else:
                rx += xo
                ry += yo
        pygame.draw.line(win, (255,255,0), (player_x, player_y), (rx, ry) ,3)
        r += 1
win = pygame.display.set_mode((screen_width, screen_height))

clock = pygame.time.Clock()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    pygame.draw.rect(win, (0,0,0), (0, 0, screen_width, screen_height))
    draw_map()
    cast_rays()
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        player_angle -= 0.1
    if keys[pygame.K_RIGHT]:
        player_angle += 0.1
    if keys[pygame.K_UP]:
        player_x, player_y = player_x + math.cos(player_angle) * 3, player_y + math.sin(player_angle) * 3
    if keys[pygame.K_DOWN]:
        player_x, player_y = player_x - math.cos(player_angle) * 3, player_y - math.sin(player_angle) * 3
    pygame.display.flip()
    clock.tick(30)

Solution

  • Calculate the ray vextore:

    rx = math.cos(player_angle)
    ry = math.sin(player_angle)
    

    Calculate the row and column of the players position in the map:

    map_x = player_x // tile_size
    map_y = player_y // tile_size
    

    Calculation of the initial position relative to the tile and the direction:

    t_max_x = player_x/tile_size - map_x
    if rx > 0:
        t_max_x = 1 - t_max_x
    t_max_y = player_y/tile_size - map_y
    if ry > 0:
        t_max_y = 1 - t_max_y
    

    Step forward until you leave the map or hit a block in a loop. This is the performance critical part, namely the actual raycasting loop:

    while True:
        if ry == 0 or t_max_x < t_max_y * abs(rx / ry):
            side = 'x'
            map_x += 1 if rx > 0 else -1
            t_max_x += 1
            if map_x < 0 or map_x >= map_size:
                break
        else:
            side = 'y'
            map_y += 1 if ry > 0 else -1
            t_max_y += 1
            if map_x < 0 or map_y >= map_size:
                break
        if MAP[int(map_x * map_size + map_y)] == "#":
            break
    

    Calculate the position at the edge of the hit block:

    if side == 'x':
        x = (map_x + (1 if rx < 0 else 0)) * tile_size
        y = player_y + (x - player_x) * ry / rx
    else:
        y = (map_y + (1 if ry < 0 else 0)) * tile_size
        x = player_x + (y - player_y) * rx / ry
    

    Draw the ray:

    pygame.draw.line(win, (0,255, 0), (player_x, player_y), (x, y))
    

    Complete example:

    import pygame
    import math
    
    pygame.init()
    
    tile_size, map_size = 50, 8
    board = [
        '########',
        '#   #  #',
        '#   # ##',
        '#  ##  #',
        '#      #',
        '###  ###',
        '#      #',
        '########']
    
    def cast_rays(sx, sy, angle):
        rx = math.cos(angle)
        ry = math.sin(angle)
        map_x = sx // tile_size
        map_y = sy // tile_size
    
        t_max_x = sx/tile_size - map_x
        if rx > 0:
            t_max_x = 1 - t_max_x
        t_max_y = sy/tile_size - map_y
        if ry > 0:
            t_max_y = 1 - t_max_y
    
        while True:
            if ry == 0 or t_max_x < t_max_y * abs(rx / ry):
                side = 'x'
                map_x += 1 if rx > 0 else -1
                t_max_x += 1
                if map_x < 0 or map_x >= map_size:
                    break
            else:
                side = 'y'
                map_y += 1 if ry > 0 else -1
                t_max_y += 1
                if map_x < 0 or map_y >= map_size:
                    break
            if board[int(map_y)][int(map_x)] == "#":
                break
    
        if side == 'x':
            x = (map_x + (1 if rx < 0 else 0)) * tile_size
            y = player_y + (x - player_x) * ry / rx
        else:
            y = (map_y + (1 if ry < 0 else 0)) * tile_size
            x = player_x + (y - player_y) * rx / ry
        return x, y    
    
    
    window = pygame.display.set_mode((tile_size*map_size, tile_size*map_size))
    clock = pygame.time.Clock()
    
    board_surf = pygame.Surface((tile_size*map_size, tile_size*map_size))
    for row in range(8):
        for col in range(8):
            color = (192, 192, 192) if board[row][col] == '#' else (96, 96, 96)
            pygame.draw.rect(board_surf, color, (col * tile_size, row * tile_size, tile_size - 2, tile_size - 2))
    
    player_x, player_y = round(tile_size * 4.5) + 0.5, round(tile_size * 4.5) + 0.5
    player_angle = 0
    
    run = True
    while run:
        clock.tick(30)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False    
        
        keys = pygame.key.get_pressed()
        player_angle += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 0.1
        speed = (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * 3
        player_x -= math.cos(player_angle) * speed
        player_y -= math.sin(player_angle) * speed
        hit_pos = cast_rays(player_x, player_y, player_angle)
    
        window.blit(board_surf, (0, 0))
        pygame.draw.line(window, (0,255, 0), (player_x, player_y), hit_pos)
        pygame.draw.circle(window, (255,0,0), (player_x, player_y), 8)
        pygame.display.flip()
    
    pygame.quit()
    exit()
    

    I actually used the same algorithm (just the 3D version) in a simple voxel raytracer:
    Voxel Ray Tracing