So I have made a raycaster in Python using pygame. I cast my rays little bit by bit and once they've hit a wall, I calculate the distance and use that base the height of my walls. The problem is that my walls are curved.
I had thought I had fixed this problem by adding this
angle = math.radians(self.view - degree)
dist *= math.cos(angle)
But with that the walls to the left or right of me would still be curved.
Here is the full code or you can play it here!
#Import modules
import pygame
import math
#Set up window
pygame.init()
winWidth, winHeight = (1024, 512)
window = pygame.display.set_mode((winWidth, winHeight))
#Make player object
class Player:
def __init__(self, x, y, tm, fov, view):
self.x = x
self.y = y
self.tm = tm #tilemap
self.fov = fov #field of view
self.view = view #players angle
self.movements = {'w': False, 'a':False, 's':False, 'd':False}
self.distances = []
self.img = pygame.image.load("tile_test.png").convert_alpha()
self.img = pygame.transform.scale(self.img, (64, 64))
def update(self): #Player movements
radAngle = math.radians(self.view)
if self.movements['w'] == True and self.tm[int(self.y+(math.sin(radAngle)*0.05))][int(self.x+(math.cos(radAngle)*0.05))] == 0:
self.x += math.cos(radAngle)*0.05
self.y += math.sin(radAngle)*0.05
if self.movements['a'] == True:
self.view -= 3
if self.movements['d'] == True:
self.view += 3
def draw(self, window):
#---TopDown View---
#Map
for y, row in enumerate(self.tm):
for x, tile in enumerate(row):
if tile == 1:
pygame.draw.rect(window, (255,255,255), (x*64, y*64, 64, 64))
pygame.draw.rect(window, (0,0,0), (x*64, y*64, 64, 64), 1)
else:
pygame.draw.rect(window, (0,0,0), (x*64, y*64, 64, 64))
pygame.draw.rect(window, (255,255,255), (x*64, y*64, 64, 64), 1)
#player
pygame.draw.circle(window, (255,255,0), (self.x*64, self.y*64), 8)
#Rays :D
self.distances = []
for degree in range(int(self.view-(self.fov/2)), int(self.view+(self.fov/2))):
radAngle = math.radians(degree)
rayx = self.x
rayy = self.y
stop = False
while self.tm[int(rayy)][int(rayx)] == 0 and stop == False:
rayx += math.cos(radAngle)*0.01
rayy += math.sin(radAngle)*0.01
#Calculate ray distance
dist = math.sqrt(((rayx-self.x)*(rayx-self.x)+(rayy-self.y)*(rayy-self.y)))
#Draw the ray
pygame.draw.line(window, (0,255,0), (self.x*64, self.y*64), (rayx*64, rayy*64))
#Decide if colides horizontally or vertically (To help with drawing tiles)
rx = round(rayx - int(rayx), 5)
ry = round(rayy - int(rayy), 5)
h_col = False
if rx > .5:
if ry > .5 - (rx - .5) and ry < .5 + (rx - .5):
h_col = True
else:
h_col = False
elif rx <= .5:
if ry > .5 - (.5 - rx) and ry < .5 + (.5 - rx):
h_col = True
else:
h_col = False
if h_col == True:
num = ry
else:
num = rx
#Attempt at fixing curved walls. Works somewhat but not really
angle = math.radians(self.view - degree)
dist *= math.cos(angle)
self.distances.append((dist, num))
#draw player view ray
pygame.draw.line(window, (255,0,0), (self.x*64, self.y*64), ((self.x+math.cos(math.radians(self.view)))*64, (self.y+math.sin(math.radians(self.view)))*64))
#---3D View---
for x, line in enumerate(self.distances):
height = 256 - round(line[0], 1)*42
if height <= .5:
height = .5
w, h = self.img.get_width(), self.img.get_height()
img_x = int(line[1]*w)
if img_x > 63:
img_x = 63
elif img_x < 0:
img_x = 0
img = self.img.subsurface(img_x, 0, 1, h)
img = pygame.transform.scale(img, (8, height*2))
window.blit(img, (512+(x*8), 256-height))
class Control:
def __init__(self):
self.run = True
self.clock = pygame.time.Clock()
def update(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
player.movements["w"] = True
if event.key == pygame.K_a:
player.movements["a"] = True
if event.key == pygame.K_s:
player.movements["s"] = True
if event.key == pygame.K_d:
player.movements["d"] = True
if event.key == pygame.K_ESCAPE:
self.run = False
elif event.type == pygame.KEYUP:
if event.key == pygame.K_w:
player.movements["w"] = False
if event.key == pygame.K_a:
player.movements["a"] = False
if event.key == pygame.K_s:
player.movements["s"] = False
if event.key == pygame.K_d:
player.movements["d"] = False
game = Control()
tm = [
[1,1,1,1,1,1,1,1],
[1,0,1,0,0,0,0,1],
[1,0,1,1,0,1,0,1],
[1,0,0,0,0,1,0,1],
[1,1,1,1,0,1,0,1],
[1,0,1,1,0,0,0,1],
[1,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1]
]
player = Player(3.5, 3.5, tm, 64, 0)
while game.run:
window.fill((50,50,50))
player.draw(window)
pygame.display.update()
game.update()
player.update()
game.clock.tick(30)
pygame.quit()
Your attempt to project the distance to the line of sight is almost correct:
dist *= math.cos(math.radians(degree - self.view))
However, the calculation of the height is wrong:
height = 256 - round(line[0], 1)*42
height = round(256 / line[0])
Also calculate the angle from a projection to a plane:
class Player:
# [...]
def draw(self, window):
# [...]
for dy in range(-self.fov//2, self.fov//2):
degree = self.view + math.degrees(math.atan2(dy, 50))
radAngle = math.radians(degree)
# [...]
# project distance
dist *= math.cos(math.radians(degree - self.view))
# [...]
for x, line in enumerate(self.distances):
# calculate height
height = round(256 / line[0])
# [...]
Optimized minimal 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
direction = 'r' if x >= player_x else 'l'
else:
y = (map_y + (1 if ry < 0 else 0)) * tile_size
x = player_x + (y - player_y) * rx / ry
direction = 'd' if y >= player_y else 'u'
return (x, y), math.hypot(x - sx, y - sy), direction
def cast_fov(sx, sy, angle, fov, no_ofrays):
max_d = math.tan(math.radians(fov/2))
step = max_d * 2 / no_ofrays
rays = []
for i in range(no_ofrays):
d = -max_d + (i + 0.5) * step
ray_angle = math.atan2(d, 1)
pos, dist, direction = cast_rays(sx, sy, angle + ray_angle)
rays.append((pos, dist, dist * math.cos(ray_angle), direction))
return rays
window = pygame.display.set_mode((tile_size*map_size*2, 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
max_speed = 3
colors = {'r' : (196, 128, 64), 'l' : (128, 128, 64), 'd' : (128, 196, 64), 'u' : (64, 196, 64)}
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()
hit_pos_front, dist_front, side_front = cast_rays(player_x, player_y, player_angle)
hit_pos_back, dist_back, side_back = cast_rays(player_x, player_y, player_angle + math.pi)
player_angle += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 0.1
speed = ((0 if dist_front <= max_speed else keys[pygame.K_UP]) - (0 if dist_back <= max_speed else keys[pygame.K_DOWN])) * max_speed
player_x += math.cos(player_angle) * speed
player_y += math.sin(player_angle) * speed
rays = cast_fov(player_x, player_y, player_angle, 60, 40)
window.blit(board_surf, (0, 0))
for ray in rays:
pygame.draw.line(window, (0, 255, 0), (player_x, player_y), ray[0])
pygame.draw.line(window, (255, 0, 0), (player_x, player_y), hit_pos_front)
pygame.draw.circle(window, (255, 0, 0), (player_x, player_y), 8)
pygame.draw.rect(window, (128, 128, 255), (400, 0, 400, 200))
pygame.draw.rect(window, (128, 128, 128), (400, 200, 400, 200))
for i, ray in enumerate(rays):
height = round(256 / (ray[2]/50))
color = pygame.Color((0, 0, 0)).lerp(colors[ray[3]], min(height/256, 1))
rect = pygame.Rect(400 + i*10, 200-height//2, 10, height)
pygame.draw.rect(window, color, rect)
pygame.display.flip()
pygame.quit()
exit()