Roguelike: line-of-sight rewrite
The original line-of-sight implementation was naive: cast rays from the player to every tile in a radius, mark visible if no wall intersects. It worked. It was slow (O(r²) rays per frame), and it had the classic asymmetry bug — you could see around corners from one direction but not the other.
Replaced it with symmetric shadowcasting (the algorithm documented by Brésenham and refined by Bob Nystrom in his roguelike tutorial series).
Why symmetry matters
The asymmetry bug isn’t just aesthetic. In a turn-based game, if the player can see an enemy that can’t see the player, the player gets a free attack. If the enemy can see the player but the player can’t see the enemy, the enemy gets a free attack. Both feel wrong. Symmetric LOS fixes this: if A can see B, B can see A.
The implementation
Shadowcasting divides the field of view into eight octants and processes each independently. Within each octant, it scans rows of tiles outward from the player, tracking which angular ranges are blocked by walls.
func compute_fov(origin: Vector2i, radius: int) -> void:
visible_tiles.clear()
visible_tiles.insert(origin)
for octant in range(8):
_cast_light(origin, radius, 1, 1.0, 0.0, octant)
func _cast_light(origin, radius, row, start_slope, end_slope, octant) -> void:
if start_slope < end_slope:
return
# ... process row, recurse for unblocked ranges
The Godot implementation runs in under 0.1ms for a 15-tile radius on a tilemap with ~2000 tiles. Compare to the old raycasting approach at ~3ms for the same radius.
The visible tile set is used for rendering (fog of war) and for AI targeting. The symmetry property means I didn’t have to write separate logic for “can enemy see player” — it’s the same computation run from the enemy’s position.
Took about four hours to implement correctly. The edge cases at octant boundaries are subtle. Test with a map that has diagonal walls.