there is a library for godot called VisionCone3D and it's the only library i could find for godot 4 which serves this purpose. obviously i don't want to reinvent everything so i grabbed this and used it. awesome. vision cone done. it's pretty configurable and even has a nice editor visualizer to show the cone.
except sometimes the vision cone would fail to notice something... or forget to notify when something left its vision. this was pretty awkward to deal with when trying to write enemy behavior that relies on enemy vision.
so i ended up writing my own. a cone check is essentially a spherical shape cast plus a dot product check. for a complete vision cone, it should also do an occlusion check for objects which are concealed by geometry.
putting all this together, it looks something like this
var shape_cast: ShapeCast3D
var nodes: Dictionary[CollisionObject3D] = {}
var cos_angle: float
func _ready() -> void:
var radius: float = shape_cast.shape.radius
var height: float = shape_cast.target_position.length()
angle = atan(radius / height)
# calculate this ahead of time to avoid needing to do it every frame
# its needed for the cone check later
cos_angle = cos(angle)
func _physics_process(_delta: float) -> void:
# a bunch of stuff probably goes here
var collisions := shape_cast.collision_result
for i in collisions.size():
var collider := shape_cast.get_collider(i)
var result: Dictionary = collisions[i]
if collider == null or result.is_empty():
continue
# this is the point of collision between the shape cast and the object which
# should be more accurate when checking if the object actually has line of sight
# than the object's global_position
var collision_point: Vector3 = result.get("point")
if in_vision_cone(collision_point) and is_visible(direct_space_state, collision_point):
nodes[collider] = null
# quick occlusion check using raycasts
func is_visible(space: PhysicsDirectSpaceState3D, point: Vector3) -> bool:
params = PhysicsRayQueryParameters3D.create(global_position, point)
return space.intersect_ray(params).is_empty()
func in_vision_cone(point: Vector3) -> bool:
var cone_dir := -global_basis.y.normalized()
var point_dir := (point - global_position).normalized()
var dot := cone_dir.dot(point_dir)
return dot > cos_angle
if the math is wrong im sorry, im bad at math. ok bye <3