Area2D area_exited signal after owner destruction

Godot Version

Godot_v4.3-stable_win64

Question

I’m trying to write something like intaraction system. I have one class called “Interactor” and one class called “InteractiveObject”. Both of them require an Area2D node.

First one, an Interactor, monitors areas by connecting to its Area2D signals area_entered and area_exited. Simplified class is here:

extends Node2D
class_name Interactor

#region public

@export var active : bool = true:
	set(_new_active):
		if _new_active == active:
			return
		active = _new_active
		_update_signals_connections()


#endregion public

#region private

@export var _interactive_area : Area2D

var _is_signal_connected : bool = false
var _object_list : Array[InteractiveObject]


func _ready() -> void:
	assert(_interactive_area, "Interactive Area must be set!")
	_update_signals_connections()


func _destructor():
	active = false


func _process(_delta: float) -> void:
	pass


func _update_signals_connections():
	if active and not _is_signal_connected:
		_interactive_area.area_entered.connect(_handle_area_entered)
		_interactive_area.area_exited.connect(_handle_area_exited)
		_is_signal_connected = true
		return
	if not active and _is_signal_connected:
		_interactive_area.area_entered.disconnect(_handle_area_entered)
		_interactive_area.area_exited.disconnect(_handle_area_exited)
		_is_signal_connected = false
		return


func _handle_area_entered(_other_area : Area2D):
	var _owner = _other_area.owner
	var _interactive_object : InteractiveObject = Utils.find_first_node_of_type(_owner, InteractiveObject)
	if not _interactive_object:
		return
	_object_list.push_back(_interactive_object)
	_interactive_object.enter_interactor_area(self)


func _handle_area_exited(_other_area : Area2D):
	var _owner = _other_area.owner
	var _interactive_object : InteractiveObject = Utils.find_first_node_of_type(_owner, InteractiveObject)
	if not _interactive_object:
		return
	_object_list.erase(_interactive_object)
	_interactive_object.exit_interactor_area(self)


func _notification(what):
	match what:
		NOTIFICATION_PREDELETE:
			_destructor()

#endregion private

Utils.find_first_node_of_type - returns the first child of given node of given type, without recursion.

And the Interactive Object class:

extends Node2D
class_name InteractiveObject

#region public

func enter_interactor_area(_interactor : Interactor):
	print('Enters!')

	
func exit_interactor_area(_interactor : Interactor):
	print('Exits!')

#endregion public

Later there will be abilities, that Interactive Object will give to Interactor.

For example. Player’s Interactor’s area catched sword’s Interactive Object’s area - sword gives the player an ability to take it (and then ability system will turn on inputs etc…). Player went away from swords area - sword removes abilities.

Imagine, that player got an ability to take sword and takes it. An ability adds sword to player’s inventory and removes sword scene by calling queue_free. We can simulate it by adding queue_free call to Interactor’s _handle_area_entered method:

func _handle_area_entered(_other_area : Area2D):
	var _owner = _other_area.owner
	var _interactive_object : InteractiveObject = Utils.find_first_node_of_type(_owner, InteractiveObject)
	if not _interactive_object:
		return
	_object_list.push_back(_interactive_object)
	_interactive_object.enter_interactor_area(self)
	# Removing sword
	_owner.queue_free()

After that, when sword is removing, Interactor will catch exit_area signal, that is called by sword. But at this moment sword’s area’s owner is null! And I can’t find appropriate Interactive Object of this area.

Can anybody tell me, how to workaround this? Maybe somehow catch object’s destroying before area_exited signal. Thank you in advance!

OK, while this topic was on check, I went deeper, looked at engine source code and found out that area_exited signal is called on node destruction through tree_exiting signal. Here it is: godot/scene/2d/physics/area_2d.cpp at 4.3 · godotengine/godot · GitHub

But for some reason queue_free methods set node’s owner to null before calling tree_exiting signal, despite the claims of godot developers here. I briefly looked at the engine code, but didn’t find the reason of this behaviour.

But I found the workaround - remove object from tree and only after that call queue_free. So, the code below works good:

func _handle_area_entered(_other_area : Area2D):
	var _owner = _other_area.owner
	var _interactive_object : InteractiveObject = Utils.find_first_node_of_type(_owner, InteractiveObject)
	if not _interactive_object:
		return
	_object_list.push_back(_interactive_object)
	_interactive_object.enter_interactor_area(self)
	# Removing sword
	_owner.get_parent().remove_child(_owner)
	_owner.queue_free()

Ugly, but works. By the way, I’m still interested in queue_free behaviour.