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)
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