An Introduction to Cross Shaped FOV
This article is also available on GitHub.
Cross shaped field of view casts four rays in cardinal directions. All rays have a unified width, but the maximum range of each ray can be defined separately. It suits well if your dungeon is filled with narrow corridors and your field of view is asymmetric. I implement the algorithm with Godot script language (see CrossShapedFOV.gd) in One More Level. In the latest version (0.3.0), it is used in Railgun. Below is a screenshot.
If you want to use CrossShapedFOV.gd
in your own Godot project, first you need to set dungeon width and height in the script:
const DUNGEON_WIDTH: int = 21
const DUNGEON_HEIGHT: int = 15
Then load and initialize the script:
var _new_CrossShapedFOV := preload("res://library/CrossShapedFOV.gd").new()
The script has two public functions. Call set_rectangular_sight()
to set internal data, then call is_in_sight()
to check if a given position is in sight. Their signatures are as follows:
set_field_of_view(center_x: int, center_y: int,
face_x: int, face_y: int, half_width: int,
front_range: int, right_range: int,
back_range: int, left_range: int,
func_host: Object, is_obstacle_func: String,
opt_arg: Array) -> void
is_in_sight(x: int, y: int) -> bool
In set_rectangular_sight()
, center_x
and center_y
is the source position. [face_x, face_y]
should be either [0, y]
or [x, 0]
. They represent the front direction. Other directions, right, back and left, are defined relatively. The width of a ray is half_width * 2 + 1
. The function also requires a function (is_obstacle_func(x: int, y: int, opt_arg: Array)
) to verify is a grid blocks line of sight, which is hosted in func_host
. See FuncRef in Godot manual for more information.
If your field of view is somewhat symmetric, you can use set_t_shaped_sight()
or set_symmetric_sight()
instead of set_rectangular_sight()
.
set_t_shaped_sight(center_x: int, center_y: int,
face_x: int, face_y: int, half_width: int,
front_range: int, side_range: int,
func_host: Object, is_obstacle_func: String,
opt_arg: Array) -> void
set_symmetric_sight(center_x: int, center_y: int,
half_width: int, max_range: int,
func_host: Object, is_obstacle_func: String,
opt_arg: Array) -> void:
See RailgunPCAction.gd about how to use the fov script.
Next I will explain how does the algorithm work. In set_rectangular_sight()
, we cast four rays in four directions. These rays stop when they encounter an obstacle or when they are out of range. Thus we can draw a rectangle that limits the field of view. We use four private variables to record the position of the rectangle: _max_x
, _max_y
, _min_x
and _min_y
.
^
| left
|
<- back - @ - front ->
|
| right
v
In is_in_sight()
, we check if a given position is inside the rectangle and is close enough to an axis.
func is_in_sight(x: int, y: int) -> bool:
if (x < _min_x) or (x > _max_x) or (y < _min_y) or (y > _max_y):
return false
elif (abs(x - _center_x) > _half_width) \
and (abs(y - _center_y) > _half_width):
return false
return true
Now let’s go back to implement set_rectangular_sight()
. The core part is casting four rays in a loop and updating the position of the rectangule.
func set_rectangular_sight(...) -> void:
for i in ...:
end_point = _cast_ray(...)
_update_min_max(end_point[0], end_point[1])
The function _cast_ray()
returns the coordinates of the farthest grid a ray can reach. It requires at least three groups of arguments: the starting point, the maximum distance a ray can cover if not blocked, and a way to decided whether a grid blocks the ray. We can hard wire the verification logic into the fov script, pass a function as an argument to _cast_ray()
, pass an object that implements an interface, or in Godot script, pass a function reference. The detail does not matter. Let’s suppose that _cast_ray()
accpets a function _ray_is_blocked()
that returns a Boolean value.
func _cast_ray(start_x: int, start_y: int, shift_x: int, shift_y: int,
max_range: int, ...) -> Array:
var x: int = start_x
var y: int = start_y
for _i in range(max_range):
x += shift_x
y += shift_y
if not _is_inside_dungeon(x, y):
x -= shift_x
y -= shift_y
break
elif _ray_is_blocked(x, y):
break
return [x, y]
Note that _cast_ray()
also requires shift_x
and shift_y
as inputs because we need to cast four rays in four directions and we’d prefer to let the caller decide the direction.
The function _update_min_max()
resets private variables if necessary.
func _update_min_max(x: int, y: int) -> void:
if x > _max_x:
_max_x = x
elif x < _min_x:
_min_x = x
if y > _max_y:
_max_y = y
elif y < _min_y:
_min_y = y
In set_rectangular_sight()
, we need to pass four sets of arguments to _cast_ray()
. Suppose we have aleardy processed face_x
and face_y
so that one of them is 0 and the other is either 1 or -1. We define an array of arguments like this:
func set_rectangular_sight(center_x: int, center_y: int,
face_x: int, face_y: int, half_width: int,
front_range: int, right_range: int,
back_range: int, left_range: int, ...) -> void:
...
if face_x == 0:
cast_ray_arg = [
[face_x, face_y, front_range],
[face_x, -face_y, back_range],
[face_y, face_x, right_range],
[-face_y, face_x, left_range],
]
else:
cast_ray_arg = [
[face_x, face_y, front_range],
[-face_x, face_y, back_range],
[face_y, -face_x, right_range],
[face_y, face_x, left_range],
]
for i in cast_ray_arg:
end_point = _cast_ray(center_x, center_y, i[0], i[1], i[2], ...)
_update_min_max(end_point[0], end_point[1])
Thus we have implement cross shaped field of view.
Get One More Level
One More Level
A turn-based Roguelike game made with Godot engine.
More posts
- Version 0.4.4Aug 18, 2022
- How to Create Dungeon Prefabs for a Roguelike GameAug 14, 2022
- Version 0.4.3Jul 17, 2022
- Version 0.4.2Mar 12, 2022
- Version 0.4.1Oct 28, 2021
- Version 0.4.0Sep 08, 2021
- Version 0.3.2Jun 05, 2021
- Version 0.3.1Apr 29, 2021
- Version 0.3.1-Nightly-04-16-2021Apr 16, 2021
Leave a comment
Log in with itch.io to leave a comment.