0
\$\begingroup\$

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.

Current tile-based shadow-casting

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.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ It sounds like you might be interested in a 2D Visibility Algorithm. \$\endgroup\$
    – DMGregory
    Commented Feb 13, 2022 at 17:50
  • \$\begingroup\$ My game is in 2.5D - isometric space - so ray casting might be a bit tough! \$\endgroup\$
    – Jayce
    Commented Feb 13, 2022 at 17:58
  • 1
    \$\begingroup\$ You can cast your rays in the 2D plane of the floor, dealing only with the square footprints of your tiles. Mapping the result to the tilted isometric perspective is then just a simple affine transformation from the flat 2D source space. \$\endgroup\$
    – DMGregory
    Commented Feb 13, 2022 at 18:00

0

You must log in to answer this question.

Browse other questions tagged .