Godot Version
Godot 4.7 dev4
Question
I have a component that lets the player interact with an object. Depending on if the player is close enough, if the player is standing in front of (or behind) it, and if the player is facing the correct direction, it will call a signal to let the object know that it can do its thing.
That’s not the issue (though I’d welcome advice/improvements on it). It’s just that when there’s multiple objects with this ComponentInteractable node, all the ComponentInteractable Area3D nodes will be triggered when I enter/exit and I only want the one the player has entered/exited.
Here is the code:
@tool
class_name ComponentInteractable
extends Node
## A component that handles [code]Player[/code] interactability with game objects (e.g. signposts, treasure chests, and NPCs).
@export var enabled: bool = true
@export_group("Use Distance for Detection")
@export_custom(PROPERTY_HINT_GROUP_ENABLE, "Use Distance for Detection") var use_distance_for_detection: bool = true:
set(value):
if value:
area.body_entered.connect(_on_body_entered)
area.body_exited.connect(_on_body_exited)
@export_range(0, 150, 0.001) var range_threshold: float = 2
@export_group("Use Dot Product to Find Face")
@export_custom(PROPERTY_HINT_GROUP_ENABLE, "Use Dot Product to Find Face") var use_dot_product: bool = true
@export_range(-1, 0, 0.001) var can_use_front_threshold: float = -0.45:
set(value):
value = clampf(abs(value), -1, 0)
can_use_front_threshold = value
if not unlink_front_back_thresholds:
can_use_back_threshold = -can_use_front_threshold
@export_subgroup("Unlink Front-Back Thresholds")
@export_custom(PROPERTY_HINT_GROUP_ENABLE, "Unlink Front-Back Thresholds") var unlink_front_back_thresholds: bool = false:
set(value):
if not value:
can_use_front_threshold = can_use_front_threshold # Activate setter
@export_range(0, 1, 0.001) var can_use_back_threshold: float = 0.45:
set(value):
value = clampf(value, 0, 1)
can_use_back_threshold = value
@export_group("Player Direction Angles")
@export_range(0, 360, 0.001) var player_direction_angle_threshold_front: float = 110
@export_range(0, 360, 0.001) var player_direction_angle_threshold_back: float = 50
@export_group("")
@export var show_debug: bool = true
@onready var area: Area3D = $Area
@onready var lbl_debug: Label = $Label
var in_range: bool
var can_use_front: bool
var can_use_back: bool
signal use
func _ready() -> void:
if not use_distance_for_detection:
if not area.body_entered.is_connected(_on_body_entered):
area.body_entered.connect(_on_body_entered)
if not area.body_exited.is_connected(_on_body_exited):
area.body_exited.connect(_on_body_exited)
func ascertain_usability(object: Node3D = get_parent(), player: Player = Global.player) -> void:
# Either use the player's distance to the object or Area3D nodes to determine if the player is in range.
var player_distance_to_object: float = player.get_distance_to(object)
if use_distance_for_detection:
in_range = player_distance_to_object <= range_threshold
if enabled and in_range:
can_use_front = true
can_use_back = true
if enabled and in_range and use_dot_product:
# This determines if the player is in front of, behind, or to the sides of the object.
var object_global_forward_direction: Vector3 = Vector3.FORWARD.rotated(Vector3.UP, object.global_rotation.y) # Get the object's direction in global space
var player_global_direction_from_object: Vector3 = player.global_position.direction_to(object.global_position) # Get the player's direction to the object in global space
var dot: float = object_global_forward_direction.dot(player_global_direction_from_object) # Returns a range where -1 for directly in front, 0 for perpendicular on either side, 1 for directly behind.
# Determine if the player is facing the object or not.
var object_angle_to_player_direction: float = object_global_forward_direction.angle_to(Vector3.FORWARD.rotated(Vector3.UP, player.global_rotation.y))
can_use_front = dot <= can_use_front_threshold and object_angle_to_player_direction >= deg_to_rad(player_direction_angle_threshold_front)
can_use_back = dot >= can_use_back_threshold and object_angle_to_player_direction <= deg_to_rad(player_direction_angle_threshold_back)
#region Debug Stuff
lbl_debug.text = """Distance to Player: {dist}
In Range?: {in_range}
Dot Product: {dot}
Obj Forward Dir θ to Player Dir: {angle}
Can Use Front: {front}
Can Use Back: {back}
""".format(
{
"dist": snappedf(player_distance_to_object, 0.01),
"in_range": in_range,
"dot": snappedf(dot, 0.01),
"angle": str(snappedf( rad_to_deg(object_angle_to_player_direction), 0.01) ) + "°",
"front": can_use_front,
"back": can_use_back,
}
)
#endregion
# Do something when the action button is pressed.
if enabled and Input.is_action_just_pressed("action"):
if can_use_front:
use.emit("front")
print('front')
if can_use_back:
use.emit("back")
print('back')
func _on_body_entered(body: CharacterBody3D) -> void:
if body is Player:
in_range = true
if OS.is_debug_build():
lbl_debug.show()
print("body entered")
func _on_body_exited(body: CharacterBody3D) -> void:
if body is Player:
in_range = false
if OS.is_debug_build():
lbl_debug.hide()
print("body exited")
(Also if anyone has any advice on how to make a visualization of the dot product and ranges to create a kind of vision cone that, especially one that appears in the Editor, that would be super awesome too!)
