From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#!/usr/bin/env perl
#
# Field of View (FOV) via shadowcast
#
# # radius seven
# $ perl eg/shadowcast 7
# ...
# # radius seven, 60 degree view angle, looking up
# $ perl eg/shadowcast 7 60 90
# ...
# # disable walls (default is 0.5 fill)
# $ perl eg/shadowcast --fill=0 7
# ...
use 5.10.0;
use strict;
use Getopt::Long qw(GetOptions);
use Game::RaycastFOV v2.00 qw(shadowcast);
sub at { "\e[" . ( 1 + $_[1] ) . ';' . ( 1 + $_[0] ) . 'H' }
sub clear_screen () { "\e[1;1H\e[2J" }
sub t_norm () { "\e[m" }
our $MAX_X = 78;
our $MAX_Y = 23;
GetOptions( 'fill|F=f' => \my $Flag_FillPercent ) or exit 1;
$Flag_FillPercent //= 0.5;
my $radius = shift || 7;
my $fov_angle = shift;
my $direction = shift;
die "Usage: $0 [radius] [fov-angle direction]\n"
if defined $fov_angle and !defined $direction;
# center
my $x = 60;
my $y = 12;
my @bounds;
if ( defined $fov_angle ) {
die "direction must be 0..359" if $direction < 0 or $direction > 359;
die "FOV angle must be 0..359" if $fov_angle < 0 or $fov_angle > 359;
$fov_angle = deg2rad($fov_angle) / 2;
$direction = deg2rad( 360 - $direction );
my $loangle = $direction - $fov_angle;
my $hiangle = $direction + $fov_angle;
if ( $loangle < 0 ) {
push @bounds, [ pi * 2 + $loangle, pi * 2 ], [ 0, $hiangle ];
} elsif ( $hiangle > pi * 2 ) {
push @bounds, [ $loangle, pi * 2 ], [ 0, $hiangle - pi * 2 ];
} else {
push @bounds, [ $loangle, $hiangle ];
}
}
*STDOUT->autoflush(1);
print clear_screen, t_norm;
# some random fill to restrict the FOV with, mirrored
my @map;
for my $r ( 0 .. 23 ) {
for my $c ( 0 .. 40 ) {
my $ch = rand() < $Flag_FillPercent ? '#' : '.';
$map[$r][$c] = $ch;
$map[$r][ $c + 40 ] = $ch;
}
}
$map[$y][$x] = '@';
my $radius_sq = $radius**2;
shadowcast(
$x, $y, $radius,
sub { # does this cell block the FOV?
my ( $curx, $cury, $dx, $dy ) = @_;
return 1 if $map[$cury][$curx] eq '#';
# this results in more of a "keyhole" around the player though
# does avoid a problem where the FOV angle is too narrow and the
# direction is at 45 degrees where the view gets clipped down to
# a narrow strip, unlike at other angles
#return 0 if abs($dx+$dy) < 2;
return 0 if !defined $fov_angle;
# TODO better way to calculate this? raycast might be good to
# benchmark against this as it has the angle being used
my $angle = 0;
my $offx = $curx - $x;
my $offy = $cury - $y;
if ( $offx > 0 and $offy == 0 ) {
$angle = 0;
} elsif ( $offx == 0 and $offy > 0 ) {
$angle = 1.5707963267949;
} elsif ( $offx < 0 and $offy == 0 ) {
$angle = 3.14159265358979;
} elsif ( $offx == 0 and $offy < 0 ) {
$angle = 4.71238898038469;
} else {
eval { $angle = atan( $offy / $offx ) };
if ( $offx < 0 ) { # quadrant II or III
$angle = 3.1415926535898 + $angle;
} elsif ( $offx > 0 and $offy < 0 ) { # quadrant IV
$angle = 6.2831853071796 + $angle;
}
}
for my $bound (@bounds) {
return 0 if $angle >= $bound->[0] and $angle <= $bound->[1];
}
return 1; # blocked
},
sub { # light up this cell
my ( $curx, $cury, $dx, $dy ) = @_;
print at( $curx, $cury ), $map[$cury][$curx];
},
sub { # is the cell inside the radius?
my ( $dx, $dy ) = @_;
return ( $dx**2 + $dy**2 ) < $radius_sq;
}
);
$x = 20;
$map[$y][$x] = '@';
shadowcast(
$x, $y, $radius,
sub { return 0 }, # nothing is blocked
sub {
my ( $curx, $cury, $dx, $dy ) = @_;
print at( $curx, $cury ), $map[$cury][$curx];
},
sub {
my ( $dx, $dy ) = @_;
return ( $dx**2 + $dy**2 ) < $radius_sq;
}
);
print at( 0, $MAX_Y );