I'm making my first isometric game in Python using Pygame. It's a 2D tile-based dungeon crawler, but I want to create a nice lighting system. I've made a random level generator that creates the rooms and joins them with corridors. My current lighting method uses a tile-based shadow-casting algorithm to determine line-of-sight from the player. Each tile has an alpha mask with the alpha value determined by the algorithm.
I want something that looks a bit more natural, and instead of the lighting moving 1 tile at a time as the player crosses the tile boundary, something that can move per-pixel if that's indeed possible? The player walks smoothly around, but my lighting "jumps" by tile.
Here is some of my code to show how the shadow-casting works:
# Set multipliers for transforming coords to octants.
MULTI = [[1, 0, 0, -1, -1, 0, 0, 1],
[0, 1, -1, 0, 0, -1, 1, 0],
[0, 1, 1, 0, 0, -1, -1, 0],
[1, 0, 0, 1, -1, 0, 0, -1]]
OCTANTS = [[1, 0, 0, 1], [-1, 0, 0, 1], [0, -1, 1, 0], [0, -1, -1, 0],
[-1, 0, 0, -1], [1, 0, 0, -1], [0, 1, -1, 0], [0, 1, 1, 0]]
def render(self):
screen.surface.fill(BLACK)
# Calculate the radius around the player.
x, y, r = player.x, player.y, player.light_radius
x1 = 1 if x - r < 1 else x - r
y1 = 1 if y - r < 1 else y - r
x2 = self.width - 1 if x + r > self.width - 1 else x + r
y2 = self.height - 1 if y + r > self.height - 1 else y + r
# Set the alpha level of the tiles in range.
for row in range(y1, y2 + 1):
for col in range(x1, x2 + 1):
if self.map[row][col].is_seen:
self.map[row][col].alpha_layer.set_alpha(ALPHA_SEEN)
else:
self.map[row][col].alpha_layer.set_alpha(ALPHA_UNSEEN)
self.map[player.y][player.x].alpha_layer.set_alpha(0)
# Apply shadow casting to the area.
for octant in range(8):
self.shadow_cast(1, 1.0, 0.0,
self.MULTI[0][octant], self.MULTI[1][octant],
self.MULTI[2][octant], self.MULTI[3][octant])
# Draw all the tiles in the area.
for row in range(y1, y2 + 1):
for col in range(x1, x2 + 1):
tile = self.map[row][col]
if tile.name != 'void':
x, y = convert_to_iso(tile.x, tile.y)
if tile.name == 'wall_tile':
y -= TILE_H
screen.surface.blit(tile.image, (x, y))
screen.surface.blit(tile.alpha_layer, (x, y))
def shadow_cast(self, row, start, end, xx, xy, yx, yy):
if start < end:
return
new_start = 0
radius2 = (player.light_radius + 1) ** 2
for depth in range(row, player.light_radius + 1):
dx, dy = -depth - 1, -depth
blocked = False
while dx <= 0:
dx += 1
cell_x = player.x + dx * xx + dy * xy
cell_y = player.y + dx * yx + dy * yy
l_slope = (dx - 0.5) / (dy + 0.5)
r_slope = (dx + 0.5) / (dy - 0.5)
if start < r_slope:
continue
elif end > l_slope:
break
else:
if dx * dx + dy * dy < radius2:
self.set_visible(cell_x, cell_y, depth)
if blocked:
if self.blocked(cell_x, cell_y):
new_start = r_slope
continue
else:
blocked = False
start = new_start
else:
if (self.blocked(cell_x, cell_y) and
depth < player.light_radius):
blocked = True
self.shadow_cast(
depth + 1, start, l_slope, xx, xy, yx, yy)
new_start = r_slope
if blocked:
break
def set_visible(self, x, y, d):
if 0 <= x < self.width and 0 <= y < self.height:
self.map[y][x].is_seen = True
self.map[y][x].alpha_layer.set_alpha(
int(ALPHA_SEEN / (player.light_radius + 1)) * d)
def blocked(self, x, y):
return (x < 0 or y < 0 or x >= self.width or
y >= self.height or self.map[y][x].is_solid)
I haven't added the player in yet, but he is currently the source of the light at the centre. I really want to do something with the lighting that looks a lot better. I don't really want to get into OpenGL or anything like that, I'm currently using Python 3.10, Pygame 2.1.2 and Pycharm Community Edition as my IDE.