diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index a5097521dc45..d8607e8671b1 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -238,6 +238,9 @@
If [code]true[/code], new node created when reparenting node(s) will be positioned at the average position of the selected node(s).
+
+ If [code]true[/code], a checkbox button will appear next to each node in the scene tree which can be used to toggle the [code]node_active[/code] property.
+
If [code]true[/code], the Create dialog (Create New Node/Create New Resource) will start with all its sections expanded. Otherwise, sections will be collapsed until the user starts searching (which will automatically expand sections as needed).
diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml
index c07948b546d2..c42d3a015020 100644
--- a/doc/classes/Node.xml
+++ b/doc/classes/Node.xml
@@ -67,6 +67,24 @@
[b]Note:[/b] This method is only called if the node is present in the scene tree (i.e. if it's not an orphan).
+
+
+
+ Called when the node has become "active in tree", i.e. when [member node_active] has become [code]true[/code], or after the node enters the [SceneTree] if it is active.
+ In the first case, this method is also called if it was previously inactive in tree and has become active as a result of an ancestor becoming active. See [method is_node_active_in_tree].
+ In the second case, this method is run after [method _ready] has finished. You can use this method for conditional initialization.
+ Corresponds to the [constant NOTIFICATION_NODE_ACTIVE] notification in [method Object._notification].
+
+
+
+
+
+ Called when the node has become "inactive in tree", i.e. when [member node_active] has become [code]false[/code], or before the node exits the [SceneTree] if it is active.
+ In the first case, this method is also called if it was previously active in tree and has become inactive due to an ancestor becoming inactive. See [method is_node_active_in_tree].
+ You can use this method for lifecycle cleanup, since it is run when this node or an ancestor node was set to inactive, or if this node is about to be removed from the scene.
+ Corresponds to the [constant NOTIFICATION_NODE_INACTIVE] notification in [method Object._notification].
+
+
@@ -620,6 +638,12 @@
Returns [code]true[/code] if the local system is the multiplayer authority of this node.
+
+
+
+ Returns [code]true[/code] if this node and all of its ancestors have [member node_active] set to [code]true[/code].
+
+
@@ -1005,6 +1029,11 @@
The name of the node. This name must be unique among the siblings (other child nodes from the same parent). When set to an existing sibling's name, the node is automatically renamed.
[b]Note:[/b] When changing the name, the following characters will be replaced with an underscore: ([code].[/code] [code]:[/code] [code]@[/code] [code]/[/code] [code]"[/code] [code]%[/code]). In particular, the [code]@[/code] character is reserved for auto-generated names. See also [method String.validate_node_name].
+
+ Whether the node should be "active" in the scene. Inactive nodes will not process scripts, will not be visible in the viewport, and will not act as a valid [Camera2D] or [Camera3D].
+ If an ancestor of this node is not active, this node will behave as though it is inactive, even if [member node_active] is [code]true[/code]. See [method is_node_active_in_tree].
+ When [member node_active] changes, [method _node_active] or [method _node_inactive] will be run on this node and on descendant nodes that have had their "active in tree" status changed as a result.
+
The owner of this node. The owner must be an ancestor of this node. When packing the owner node in a [PackedScene], all the nodes it owns are also saved with it.
[b]Note:[/b] In the editor, nodes not owned by the scene root are usually not displayed in the Scene dock, and will [b]not[/b] be saved. To prevent this, remember to set the owner after calling [method add_child]. See also (see [member unique_name_in_owner])
@@ -1068,6 +1097,16 @@
Emitted when the node's editor description field changed.
+
+
+ Emitted when [member node_active] has changed on this node.
+
+
+
+
+ Emitted when this node's "active in tree" status has changed, i.e. when [member node_active] has changed on this node or any ancestor nodes.
+
+
Emitted when the node is considered ready, after [method _ready] is called.
@@ -1171,6 +1210,15 @@
Notification received when the node is enabled again after being disabled. See [constant PROCESS_MODE_DISABLED].
+
+ Notification received when [member node_active] has changed for this node.
+
+
+ Notification received when the node has become active in tree. See [method _node_active].
+
+
+ Notification received when the node has become inactive in tree. See [method _node_inactive].
+
Notification received when [method reset_physics_interpolation] is called on the node or its ancestors.
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 12a7c3a2ff80..552d24fc4fc7 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -621,6 +621,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) {
_initial_set("docks/scene_tree/start_create_dialog_fully_expanded", false);
_initial_set("docks/scene_tree/auto_expand_to_selected", true);
_initial_set("docks/scene_tree/center_node_on_reparent", false);
+ _initial_set("docks/scene_tree/show_node_active_checkbox", false);
// FileSystem
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "docks/filesystem/thumbnail_size", 64, "32,128,16")
diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp
index 2e36b66025a2..275e716511b1 100644
--- a/editor/gui/scene_tree_editor.cpp
+++ b/editor/gui/scene_tree_editor.cpp
@@ -101,6 +101,20 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
}
}
undo_redo->commit_action();
+ } else if (p_id == BUTTON_ACTIVE) {
+ undo_redo->create_action(TTR("Toggle Node Active"));
+ _toggle_active(n);
+ List selection = editor_selection->get_selected_node_list();
+ if (selection.size() > 1 && selection.find(n) != nullptr) {
+ for (Node *nv : selection) {
+ ERR_FAIL_NULL(nv);
+ if (nv == n) {
+ continue;
+ }
+ _toggle_active(nv);
+ }
+ }
+ undo_redo->commit_action();
} else if (p_id == BUTTON_LOCK) {
undo_redo->create_action(TTR("Unlock Node"));
undo_redo->add_do_method(n, "remove_meta", "_edit_lock_");
@@ -217,6 +231,20 @@ void SceneTreeEditor::_toggle_visible(Node *p_node) {
}
}
+void SceneTreeEditor::_toggle_active(Node *p_node) {
+ if (p_node->has_method("is_node_active_self") && p_node->has_method("set_node_active")) {
+ bool currentlyActive = bool(p_node->call("is_node_active_self"));
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ if (currentlyActive) {
+ undo_redo->add_do_method(p_node, "set_node_active", false);
+ undo_redo->add_undo_method(p_node, "set_node_active", true);
+ } else {
+ undo_redo->add_do_method(p_node, "set_node_active", true);
+ undo_redo->add_undo_method(p_node, "set_node_active", false);
+ }
+ }
+}
+
void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
if (!p_node) {
return;
@@ -445,6 +473,16 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
_update_visibility_color(p_node, item);
}
+ if (p_node->has_method("is_node_active_self") && p_node->has_method("set_node_active")) {
+ bool show_node_active_checkbox = EDITOR_GET("docks/scene_tree/show_node_active_checkbox").operator bool();
+ _set_node_active_checkbox_enabled(item, p_node, show_node_active_checkbox);
+
+ const Callable active_in_tree_changed = callable_mp(this, &SceneTreeEditor::_node_active_in_tree_changed);
+ if (!p_node->is_connected(SceneStringName(node_active_in_tree_changed), active_in_tree_changed)) {
+ p_node->connect(SceneStringName(node_active_in_tree_changed), active_in_tree_changed.bind(p_node));
+ }
+ }
+
if (p_node->is_class("AnimationMixer")) {
bool is_pinned = AnimationPlayerEditor::get_singleton()->get_editing_node() == p_node && AnimationPlayerEditor::get_singleton()->is_pinned();
@@ -588,11 +626,83 @@ void SceneTreeEditor::_update_visibility_color(Node *p_node, TreeItem *p_item) {
}
}
+void SceneTreeEditor::_set_node_active_checkbox_enabled(TreeItem *p_item, Node *p_node, bool p_enabled) {
+ int idx = p_item->get_button_by_id(0, BUTTON_ACTIVE);
+ if (p_enabled) {
+ if (idx == -1) {
+ // Only add checkbox if it does not exist.
+ bool is_active = p_node->call("is_node_active_self");
+ if (is_active) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("GuiChecked")), BUTTON_ACTIVE, false, TTR("Toggle Node Active"));
+ } else {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("GuiUnchecked")), BUTTON_ACTIVE, false, TTR("Toggle Node Active"));
+ }
+ }
+ } else {
+ if (idx != -1) {
+ // Only erase checkbox if it exists.
+ p_item->erase_button(0, idx);
+ }
+ }
+}
+
+void SceneTreeEditor::_node_active_in_tree_changed(Node *p_node) {
+ if (!p_node || (p_node != get_scene_node() && !p_node->get_owner())) {
+ return;
+ }
+
+ TreeItem *item = _find(tree->get_root(), p_node->get_path());
+
+ if (!item) {
+ return;
+ }
+
+ bool show_node_active_checkbox = EDITOR_GET("docks/scene_tree/show_node_active_checkbox").operator bool();
+ if (!show_node_active_checkbox) {
+ return;
+ }
+
+ int idx = item->get_button_by_id(0, BUTTON_ACTIVE);
+ ERR_FAIL_COND(idx == -1);
+
+ bool node_active = false;
+
+ if (p_node->has_method("is_node_active_self")) {
+ node_active = p_node->call("is_node_active_self");
+ if (p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) {
+ CanvasItemEditor::get_singleton()->get_viewport_control()->queue_redraw();
+ }
+ }
+
+ if (node_active) {
+ item->set_button(0, idx, get_editor_theme_icon(SNAME("GuiChecked")));
+ } else {
+ item->set_button(0, idx, get_editor_theme_icon(SNAME("GuiUnchecked")));
+ }
+
+ if (p_node->call("can_process")) {
+ _clear_item_custom_color(item);
+ } else {
+ _set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+ }
+
+ if (p_node->has_method("is_visible") && p_node->has_method("set_visible") && p_node->has_signal(SceneStringName(visibility_changed))) {
+ _node_visibility_changed(p_node);
+ }
+}
+
void SceneTreeEditor::_set_item_custom_color(TreeItem *p_item, Color p_color) {
p_item->set_custom_color(0, p_color);
p_item->set_meta(SNAME("custom_color"), p_color);
}
+void SceneTreeEditor::_clear_item_custom_color(TreeItem *p_item) {
+ if (p_item->has_meta(SNAME("custom_color"))) {
+ p_item->clear_custom_color(0);
+ p_item->remove_meta(SNAME("custom_color"));
+ }
+}
+
void SceneTreeEditor::_node_script_changed(Node *p_node) {
if (tree_dirty) {
return;
@@ -617,6 +727,12 @@ void SceneTreeEditor::_node_removed(Node *p_node) {
}
}
+ if (p_node->has_signal(SceneStringName(node_active_in_tree_changed))) {
+ if (p_node->is_connected(SceneStringName(node_active_in_tree_changed), callable_mp(this, &SceneTreeEditor::_node_active_in_tree_changed))) {
+ p_node->disconnect(SceneStringName(node_active_in_tree_changed), callable_mp(this, &SceneTreeEditor::_node_active_in_tree_changed));
+ }
+ }
+
if (p_node == selected) {
selected = nullptr;
}
@@ -967,6 +1083,12 @@ void SceneTreeEditor::_notification(int p_what) {
_update_tree();
} break;
+ case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+ if (EditorSettings::get_singleton()->check_changed_settings_in_group("docks/scene_tree")) {
+ _update_tree();
+ }
+ } break;
+
case NOTIFICATION_VISIBILITY_CHANGED: {
if (is_visible()) {
TreeItem *item = nullptr;
@@ -1842,6 +1964,8 @@ SceneTreeDialog::SceneTreeDialog() {
// Disable the OK button when no node is selected.
get_ok_button()->set_disabled(!tree->get_selected());
tree->connect("node_selected", callable_mp(this, &SceneTreeDialog::_selected_changed));
+
+ EDITOR_DEF("docks/scene_tree/show_node_active_checkbox", true);
}
SceneTreeDialog::~SceneTreeDialog() {
diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h
index e623c8405d37..8d8c7167d8a0 100644
--- a/editor/gui/scene_tree_editor.h
+++ b/editor/gui/scene_tree_editor.h
@@ -56,6 +56,7 @@ class SceneTreeEditor : public Control {
BUTTON_GROUPS = 7,
BUTTON_PIN = 8,
BUTTON_UNIQUE = 9,
+ BUTTON_ACTIVE = 10,
};
Tree *tree = nullptr;
@@ -121,12 +122,16 @@ class SceneTreeEditor : public Control {
void _cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
void _toggle_visible(Node *p_node);
+ void _toggle_active(Node *p_node);
void _cell_multi_selected(Object *p_object, int p_cell, bool p_selected);
void _update_selection(TreeItem *item);
void _node_script_changed(Node *p_node);
+ void _set_node_active_checkbox_enabled(TreeItem *p_item, Node *p_node, bool p_enabled);
+ void _node_active_in_tree_changed(Node *p_node);
void _node_visibility_changed(Node *p_node);
void _update_visibility_color(Node *p_node, TreeItem *p_item);
void _set_item_custom_color(TreeItem *p_item, Color p_color);
+ void _clear_item_custom_color(TreeItem *p_item);
void _update_node_tooltip(Node *p_node, TreeItem *p_item);
void _queue_update_node_tooltip(Node *p_node, TreeItem *p_item);
void _tree_scroll_to_item(ObjectID p_item_id);
diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp
index 74f26df70629..7011f17138bd 100644
--- a/scene/2d/camera_2d.cpp
+++ b/scene/2d/camera_2d.cpp
@@ -365,6 +365,16 @@ void Camera2D::_notification(int p_what) {
callable_mp(this, &Camera2D::_reset_just_exited).call_deferred();
} break;
+ case NOTIFICATION_NODE_ACTIVE_CHANGED: {
+ bool enabled_and_active = is_enabled();
+
+ if (enabled_and_active && !viewport->get_camera_2d()) {
+ make_current();
+ } else if (!enabled_and_active && is_current()) {
+ clear_current();
+ }
+ } break;
+
#ifdef TOOLS_ENABLED
case NOTIFICATION_DRAW: {
if (!is_inside_tree() || !_is_editing_in_editor()) {
@@ -497,15 +507,17 @@ void Camera2D::set_enabled(bool p_enabled) {
return;
}
- if (enabled && !viewport->get_camera_2d()) {
+ bool enabled_and_active = is_enabled();
+
+ if (enabled_and_active && !viewport->get_camera_2d()) {
make_current();
- } else if (!enabled && is_current()) {
+ } else if (!enabled_and_active && is_current()) {
clear_current();
}
}
bool Camera2D::is_enabled() const {
- return enabled;
+ return enabled && is_node_active_in_tree();
}
Camera2D::Camera2DProcessCallback Camera2D::get_process_callback() const {
diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp
index 2b841c4e0de5..41c85f007b4e 100644
--- a/scene/3d/camera_3d.cpp
+++ b/scene/3d/camera_3d.cpp
@@ -175,9 +175,8 @@ void Camera3D::_notification(int p_what) {
viewport = get_viewport();
ERR_FAIL_NULL(viewport);
- bool first_camera = viewport->_camera_3d_add(this);
- if (current || first_camera) {
- viewport->_camera_3d_set(this);
+ if (is_node_active_in_tree()) {
+ _add_camera_to_set();
}
#ifdef TOOLS_ENABLED
@@ -243,15 +242,7 @@ void Camera3D::_notification(int p_what) {
} break;
case NOTIFICATION_EXIT_WORLD: {
- if (!is_part_of_edited_scene()) {
- if (is_current()) {
- clear_current();
- current = true; //keep it true
-
- } else {
- current = false;
- }
- }
+ _remove_camera_from_set();
if (viewport) {
#ifdef TOOLS_ENABLED
@@ -259,11 +250,18 @@ void Camera3D::_notification(int p_what) {
viewport->disconnect(SNAME("size_changed"), callable_mp((Node3D *)this, &Camera3D::update_gizmos));
}
#endif
- viewport->_camera_3d_remove(this);
viewport = nullptr;
}
} break;
+ case NOTIFICATION_NODE_ACTIVE: {
+ _add_camera_to_set();
+ } break;
+
+ case NOTIFICATION_NODE_INACTIVE: {
+ _remove_camera_from_set();
+ } break;
+
case NOTIFICATION_BECAME_CURRENT: {
if (viewport) {
viewport->find_world_3d()->_register_camera(this);
@@ -280,6 +278,38 @@ void Camera3D::_notification(int p_what) {
}
}
+void Camera3D::_add_camera_to_set() {
+ if (added_to_camera_set || viewport == nullptr) {
+ return;
+ }
+
+ added_to_camera_set = true;
+ bool first_camera = viewport->_camera_3d_add(this);
+ if (current || first_camera) {
+ viewport->_camera_3d_set(this);
+ }
+}
+
+void Camera3D::_remove_camera_from_set() {
+ if (!added_to_camera_set) {
+ return;
+ }
+
+ added_to_camera_set = false;
+ if (!is_part_of_edited_scene()) {
+ if (is_current()) {
+ clear_current();
+ current = true; //keep it true
+
+ } else {
+ current = false;
+ }
+ }
+ if (viewport) {
+ viewport->_camera_3d_remove(this);
+ }
+}
+
Transform3D Camera3D::_get_adjusted_camera_transform(const Transform3D &p_xform) const {
Transform3D tr = p_xform.orthonormalized();
tr.origin += tr.basis.get_column(1) * v_offset;
diff --git a/scene/3d/camera_3d.h b/scene/3d/camera_3d.h
index 3e9f940ad648..c965150fab22 100644
--- a/scene/3d/camera_3d.h
+++ b/scene/3d/camera_3d.h
@@ -62,6 +62,7 @@ class Camera3D : public Node3D {
bool force_change = false;
bool current = false;
Viewport *viewport = nullptr;
+ bool added_to_camera_set = false;
ProjectionType mode = PROJECTION_PERSPECTIVE;
@@ -134,6 +135,8 @@ class Camera3D : public Node3D {
void _update_camera();
virtual void _request_camera_update();
void _update_camera_mode();
+ void _add_camera_to_set();
+ void _remove_camera_from_set();
void _notification(int p_what);
void _validate_property(PropertyInfo &p_property) const;
diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp
index 85de85a9a679..4005b01ddb21 100644
--- a/scene/3d/node_3d.cpp
+++ b/scene/3d/node_3d.cpp
@@ -234,6 +234,14 @@ void Node3D::_notification(int p_what) {
data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
}
} break;
+
+ case NOTIFICATION_NODE_ACTIVE_CHANGED: {
+ ERR_THREAD_GUARD;
+
+ if (is_inside_tree()) {
+ _propagate_visibility_changed();
+ }
+ } break;
}
}
@@ -907,7 +915,7 @@ void Node3D::_propagate_visibility_changed() {
#endif
for (Node3D *c : data.children) {
- if (!c || !c->data.visible) {
+ if (!c || !c->is_visible()) {
continue;
}
c->_propagate_visibility_changed();
@@ -948,7 +956,7 @@ bool Node3D::is_visible_in_tree() const {
const Node3D *s = this;
while (s) {
- if (!s->data.visible) {
+ if (!s->is_visible() || !s->is_node_active_self()) {
return false;
}
s = s->data.parent;
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index 7c8bf9c80945..6c5a6a836f95 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -60,7 +60,7 @@ Transform2D CanvasItem::_edit_get_transform() const {
bool CanvasItem::is_visible_in_tree() const {
ERR_READ_THREAD_GUARD_V(false);
- return visible && parent_visible_in_tree;
+ return visible && parent_visible_in_tree && is_node_active_in_tree();
}
void CanvasItem::_propagate_visibility_changed(bool p_parent_visible_in_tree) {
@@ -385,6 +385,13 @@ void CanvasItem::_notification(int p_what) {
emit_signal(SceneStringName(visibility_changed));
} break;
+ case NOTIFICATION_NODE_ACTIVE_CHANGED: {
+ ERR_THREAD_GUARD;
+
+ if (is_inside_tree()) {
+ _propagate_visibility_changed(parent_visible_in_tree);
+ }
+ } break;
case NOTIFICATION_WORLD_2D_CHANGED: {
ERR_MAIN_THREAD_GUARD;
diff --git a/scene/main/canvas_layer.cpp b/scene/main/canvas_layer.cpp
index 11d074f0143d..782f0a60bf8a 100644
--- a/scene/main/canvas_layer.cpp
+++ b/scene/main/canvas_layer.cpp
@@ -57,9 +57,9 @@ void CanvasLayer::set_visible(bool p_visible) {
for (int i = 0; i < get_child_count(); i++) {
CanvasItem *c = Object::cast_to(get_child(i));
if (c) {
- RenderingServer::get_singleton()->canvas_item_set_visible(c->get_canvas_item(), p_visible && c->is_visible());
+ RenderingServer::get_singleton()->canvas_item_set_visible(c->get_canvas_item(), is_visible() && c->is_visible());
- c->_propagate_visibility_changed(p_visible);
+ c->_propagate_visibility_changed(is_visible());
}
}
}
@@ -73,7 +73,7 @@ void CanvasLayer::hide() {
}
bool CanvasLayer::is_visible() const {
- return visible;
+ return visible && is_node_active_in_tree();
}
void CanvasLayer::set_transform(const Transform2D &p_xform) {
@@ -198,6 +198,19 @@ void CanvasLayer::_notification(int p_what) {
viewport = RID();
_update_follow_viewport(false);
} break;
+
+ case NOTIFICATION_NODE_ACTIVE_CHANGED: {
+ emit_signal(SceneStringName(visibility_changed));
+
+ for (int i = 0; i < get_child_count(); i++) {
+ CanvasItem *c = Object::cast_to(get_child(i));
+ if (c) {
+ RenderingServer::get_singleton()->canvas_item_set_visible(c->get_canvas_item(), is_visible() && c->is_visible());
+
+ c->_propagate_visibility_changed(is_visible());
+ }
+ }
+ }
}
}
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index 8dc7b4a87ccf..f66b4c16fdf6 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -97,6 +97,14 @@ void Node::_notification(int p_notification) {
}
}
+ if (data.node_active) {
+ if (data.parent) {
+ data.parent_active_in_tree = data.parent->is_node_active_in_tree();
+ } else {
+ data.parent_active_in_tree = true;
+ }
+ }
+
if (data.physics_interpolation_mode == PHYSICS_INTERPOLATION_MODE_INHERIT) {
bool interpolate = true; // Root node default is for interpolation to be on.
if (data.parent) {
@@ -225,6 +233,23 @@ void Node::_notification(int p_notification) {
GDVIRTUAL_CALL(_ready);
} break;
+ case NOTIFICATION_NODE_ACTIVE: {
+ if (data.completed_active_callback) {
+ // We've already run the callback.
+ // This can happen if a user sets node_active in _ready() before the lifecycle call starts.
+ return;
+ }
+ data.completed_active_callback = true;
+
+ GDVIRTUAL_CALL(_node_active);
+ } break;
+
+ case NOTIFICATION_NODE_INACTIVE: {
+ data.completed_active_callback = false;
+
+ GDVIRTUAL_CALL(_node_inactive);
+ } break;
+
case NOTIFICATION_POSTINITIALIZE: {
data.in_constructor = false;
} break;
@@ -366,6 +391,12 @@ void Node::_propagate_after_exit_tree() {
}
void Node::_propagate_exit_tree() {
+ // For lifecycle consistency, run the inactive signal.
+ // Note we don't actually disable the node so that tab switching in the editor doesn't change node state.
+ if (is_node_active_in_tree()) {
+ notification(NOTIFICATION_NODE_INACTIVE);
+ }
+
//block while removing children
#ifdef DEBUG_ENABLED
@@ -415,6 +446,24 @@ void Node::_propagate_exit_tree() {
data.depth = -1;
}
+void Node::_propagate_initial_node_active() {
+ if (is_node_active_in_tree()) {
+ notification(NOTIFICATION_NODE_ACTIVE);
+ } else {
+ // No need to check further down.
+ return;
+ }
+
+ // Block while checking children.
+ data.blocked++;
+
+ for (KeyValue &K : data.children) {
+ K.value->_propagate_initial_node_active();
+ }
+
+ data.blocked--;
+}
+
void Node::_propagate_physics_interpolated(bool p_interpolated) {
switch (data.physics_interpolation_mode) {
case PHYSICS_INTERPOLATION_MODE_INHERIT:
@@ -679,6 +728,86 @@ void Node::set_process_mode(ProcessMode p_mode) {
#endif
}
+void Node::set_node_active(bool p_enabled) {
+ ERR_THREAD_GUARD
+ if (data.node_active == p_enabled) {
+ return;
+ }
+
+ if (!is_inside_tree()) {
+ data.node_active = p_enabled;
+ return;
+ }
+
+ bool prev_can_process = can_process();
+ bool prev_enabled = _is_enabled();
+
+ if (p_enabled) {
+ if (data.parent) {
+ data.parent_active_in_tree = data.parent->is_node_active_in_tree();
+ } else {
+ data.parent_active_in_tree = true;
+ }
+ }
+
+ data.node_active = p_enabled;
+
+ bool next_can_process = can_process();
+ bool next_enabled = _is_enabled();
+
+ int pause_notification = 0;
+
+ if (prev_can_process && !next_can_process) {
+ pause_notification = NOTIFICATION_PAUSED;
+ } else if (!prev_can_process && next_can_process) {
+ pause_notification = NOTIFICATION_UNPAUSED;
+ }
+
+ int enabled_notification = 0;
+
+ if (prev_enabled && !next_enabled) {
+ enabled_notification = NOTIFICATION_DISABLED;
+ } else if (!prev_enabled && next_enabled) {
+ enabled_notification = NOTIFICATION_ENABLED;
+ }
+
+ _propagate_node_active(p_enabled, false, pause_notification, enabled_notification);
+
+ notification(NOTIFICATION_NODE_ACTIVE_CHANGED);
+ emit_signal(SceneStringName(node_active_changed));
+}
+
+void Node::_propagate_node_active(bool p_enabled, bool p_set_parent_active, int p_pause_notification, int p_enabled_notification) {
+ if (p_set_parent_active) {
+ data.parent_active_in_tree = p_enabled;
+ }
+
+ if (p_pause_notification != 0) {
+ notification(p_pause_notification);
+ }
+
+ if (p_enabled_notification != 0) {
+ notification(p_enabled_notification);
+ }
+
+ if (p_enabled) {
+ notification(NOTIFICATION_NODE_ACTIVE);
+ } else {
+ notification(NOTIFICATION_NODE_INACTIVE);
+ }
+ emit_signal(SceneStringName(node_active_in_tree_changed));
+
+ data.blocked++;
+ for (KeyValue &K : data.children) {
+ Node *c = K.value;
+ if (c->data.node_active) {
+ // Only active children will change based on parent changing.
+ c->_propagate_node_active(p_enabled, true, p_pause_notification, p_enabled_notification);
+ }
+ }
+ data.blocked--;
+}
+
void Node::_propagate_pause_notification(bool p_enable) {
bool prev_can_process = _can_process(!p_enable);
bool next_can_process = _can_process(p_enable);
@@ -865,6 +994,11 @@ bool Node::can_process() const {
}
bool Node::_can_process(bool p_paused) const {
+ if (!_is_node_active_in_tree()) {
+ // Active state being disabled overrides process_mode
+ return false;
+ }
+
ProcessMode process_mode;
if (data.process_mode == PROCESS_MODE_INHERIT) {
@@ -950,7 +1084,7 @@ bool Node::_is_enabled() const {
process_mode = data.process_mode;
}
- return (process_mode != PROCESS_MODE_DISABLED);
+ return (process_mode != PROCESS_MODE_DISABLED && _is_node_active_in_tree());
}
bool Node::is_enabled() const {
@@ -958,6 +1092,19 @@ bool Node::is_enabled() const {
return _is_enabled();
}
+bool Node::is_node_active_in_tree() const {
+ ERR_THREAD_GUARD_V(false);
+ return _is_node_active_in_tree();
+}
+
+bool Node::_is_node_active_in_tree() const {
+ return is_node_active_self() && data.parent_active_in_tree;
+}
+
+bool Node::is_node_active_self() const {
+ return data.node_active;
+}
+
double Node::get_physics_process_delta_time() const {
if (data.tree) {
return data.tree->get_physics_process_time();
@@ -3256,6 +3403,9 @@ void Node::_set_tree(SceneTree *p_tree) {
_propagate_ready(); //reverse_notification(NOTIFICATION_READY);
}
+ // Send initial lifecycle notification after ready but on each time a tree is set.
+ _propagate_initial_node_active();
+
tree_changed_b = data.tree;
}
@@ -3640,6 +3790,9 @@ void Node::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_process_mode", "mode"), &Node::set_process_mode);
ClassDB::bind_method(D_METHOD("get_process_mode"), &Node::get_process_mode);
ClassDB::bind_method(D_METHOD("can_process"), &Node::can_process);
+ ClassDB::bind_method(D_METHOD("set_node_active", "enable"), &Node::set_node_active);
+ ClassDB::bind_method(D_METHOD("is_node_active_in_tree"), &Node::is_node_active_in_tree);
+ ClassDB::bind_method(D_METHOD("is_node_active_self"), &Node::is_node_active_self);
ClassDB::bind_method(D_METHOD("set_process_thread_group", "mode"), &Node::set_process_thread_group);
ClassDB::bind_method(D_METHOD("get_process_thread_group"), &Node::get_process_thread_group);
@@ -3772,6 +3925,9 @@ void Node::_bind_methods() {
BIND_CONSTANT(NOTIFICATION_POST_ENTER_TREE);
BIND_CONSTANT(NOTIFICATION_DISABLED);
BIND_CONSTANT(NOTIFICATION_ENABLED);
+ BIND_CONSTANT(NOTIFICATION_NODE_ACTIVE_CHANGED);
+ BIND_CONSTANT(NOTIFICATION_NODE_ACTIVE);
+ BIND_CONSTANT(NOTIFICATION_NODE_INACTIVE);
BIND_CONSTANT(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
BIND_CONSTANT(NOTIFICATION_EDITOR_PRE_SAVE);
@@ -3836,6 +3992,8 @@ void Node::_bind_methods() {
ADD_SIGNAL(MethodInfo("tree_exited"));
ADD_SIGNAL(MethodInfo("child_entered_tree", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
ADD_SIGNAL(MethodInfo("child_exiting_tree", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
+ ADD_SIGNAL(MethodInfo("node_active_changed"));
+ ADD_SIGNAL(MethodInfo("node_active_in_tree_changed"));
ADD_SIGNAL(MethodInfo("child_order_changed"));
ADD_SIGNAL(MethodInfo("replacing_by", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
@@ -3847,6 +4005,8 @@ void Node::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "owner", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_owner", "get_owner");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "multiplayer", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", PROPERTY_USAGE_NONE), "", "get_multiplayer");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "node_active", PROPERTY_HINT_NONE, ""), "set_node_active", "is_node_active_self");
+
ADD_GROUP("Process", "process_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "process_mode", PROPERTY_HINT_ENUM, "Inherit,Pausable,When Paused,Always,Disabled"), "set_process_mode", "get_process_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "process_priority"), "set_process_priority", "get_process_priority");
@@ -3871,6 +4031,8 @@ void Node::_bind_methods() {
GDVIRTUAL_BIND(_enter_tree);
GDVIRTUAL_BIND(_exit_tree);
GDVIRTUAL_BIND(_ready);
+ GDVIRTUAL_BIND(_node_active);
+ GDVIRTUAL_BIND(_node_inactive);
GDVIRTUAL_BIND(_get_configuration_warnings);
GDVIRTUAL_BIND(_input, "event");
GDVIRTUAL_BIND(_shortcut_input, "event");
@@ -3906,6 +4068,10 @@ Node::Node() {
data.physics_process_internal = false;
data.process_internal = false;
+ data.node_active = true;
+ data.parent_active_in_tree = true;
+ data.completed_active_callback = false;
+
data.input = false;
data.shortcut_input = false;
data.unhandled_input = false;
diff --git a/scene/main/node.h b/scene/main/node.h
index e2f3ce9b7801..2e98d53b5eed 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -215,6 +215,10 @@ class Node : public Object {
bool physics_process_internal : 1;
bool process_internal : 1;
+ bool node_active : 1;
+ bool parent_active_in_tree : 1;
+ bool completed_active_callback : 1;
+
bool input : 1;
bool shortcut_input : 1;
bool unhandled_input : 1;
@@ -280,9 +284,11 @@ class Node : public Object {
void _propagate_ready();
void _propagate_exit_tree();
void _propagate_after_exit_tree();
+ void _propagate_initial_node_active();
void _propagate_physics_interpolated(bool p_interpolated);
void _propagate_physics_interpolation_reset_requested(bool p_requested);
void _propagate_process_owner(Node *p_owner, int p_pause_notification, int p_enabled_notification);
+ void _propagate_node_active(bool p_enabled, bool p_set_parent_active, int p_pause_notification, int p_enabled_notification);
void _propagate_groups_dirty();
void _propagate_translation_domain_dirty();
Array _get_node_and_resource(const NodePath &p_path);
@@ -304,6 +310,7 @@ class Node : public Object {
_FORCE_INLINE_ bool _can_process(bool p_paused) const;
_FORCE_INLINE_ bool _is_enabled() const;
+ _FORCE_INLINE_ bool _is_node_active_in_tree() const;
void _release_unique_name_in_owner();
void _acquire_unique_name_in_owner();
@@ -383,6 +390,8 @@ class Node : public Object {
GDVIRTUAL0(_enter_tree)
GDVIRTUAL0(_exit_tree)
GDVIRTUAL0(_ready)
+ GDVIRTUAL0(_node_active)
+ GDVIRTUAL0(_node_inactive)
GDVIRTUAL0RC(Vector, _get_configuration_warnings)
GDVIRTUAL1(_input, Ref)
@@ -413,6 +422,9 @@ class Node : public Object {
NOTIFICATION_POST_ENTER_TREE = 27,
NOTIFICATION_DISABLED = 28,
NOTIFICATION_ENABLED = 29,
+ NOTIFICATION_NODE_ACTIVE_CHANGED = 90,
+ NOTIFICATION_NODE_ACTIVE = 91,
+ NOTIFICATION_NODE_INACTIVE = 92,
NOTIFICATION_RESET_PHYSICS_INTERPOLATION = 2001, // A GodotSpace Odyssey.
// Keep these linked to Node.
NOTIFICATION_WM_MOUSE_ENTER = 1002,
@@ -671,6 +683,10 @@ class Node : public Object {
bool can_process() const;
bool can_process_notification(int p_what) const;
+ void set_node_active(bool p_enable);
+ bool is_node_active_in_tree() const;
+ bool is_node_active_self() const;
+
void set_physics_interpolation_mode(PhysicsInterpolationMode p_mode);
PhysicsInterpolationMode get_physics_interpolation_mode() const { return data.physics_interpolation_mode; }
_FORCE_INLINE_ bool is_physics_interpolated() const { return data.physics_interpolated; }
diff --git a/scene/scene_string_names.cpp b/scene/scene_string_names.cpp
index 31daeb3ae3cd..b2e15c2f2d90 100644
--- a/scene/scene_string_names.cpp
+++ b/scene/scene_string_names.cpp
@@ -37,6 +37,8 @@ SceneStringNames::SceneStringNames() {
draw = StaticCString::create("draw");
hidden = StaticCString::create("hidden");
visibility_changed = StaticCString::create("visibility_changed");
+ node_active_changed = StaticCString::create("node_active_changed");
+ node_active_in_tree_changed = StaticCString::create("node_active_in_tree_changed");
input_event = StaticCString::create("input_event");
shader = StaticCString::create("shader");
tree_entered = StaticCString::create("tree_entered");
@@ -82,6 +84,8 @@ SceneStringNames::SceneStringNames() {
updated = StaticCString::create("updated");
_ready = StaticCString::create("_ready");
+ _node_active = StaticCString::create("_node_active");
+ _node_inactive = StaticCString::create("_node_inactive");
screen_entered = StaticCString::create("screen_entered");
screen_exited = StaticCString::create("screen_exited");
diff --git a/scene/scene_string_names.h b/scene/scene_string_names.h
index 0a2ebeda7af7..1c9e1bd03f4b 100644
--- a/scene/scene_string_names.h
+++ b/scene/scene_string_names.h
@@ -55,6 +55,8 @@ class SceneStringNames {
StringName draw;
StringName hidden;
StringName visibility_changed;
+ StringName node_active_changed;
+ StringName node_active_in_tree_changed;
StringName input_event;
StringName gui_input;
StringName item_rect_changed;
@@ -104,6 +106,8 @@ class SceneStringNames {
StringName area_shape_exited;
StringName _ready;
+ StringName _node_active;
+ StringName _node_inactive;
StringName screen_entered;
StringName screen_exited;
diff --git a/tests/scene/test_node.h b/tests/scene/test_node.h
index e387c73f9f98..69b957dd564d 100644
--- a/tests/scene/test_node.h
+++ b/tests/scene/test_node.h
@@ -892,6 +892,39 @@ TEST_CASE("[SceneTree][Node] Test the process priority") {
memdelete(node4);
}
+TEST_CASE("[SceneTree][Node] Test node active operations") {
+ TestNode *node = memnew(TestNode);
+ SceneTree::get_singleton()->get_root()->add_child(node);
+
+ TestNode *child = memnew(TestNode);
+ node->add_child(child);
+
+ SUBCASE("Default active state") {
+ CHECK(node->is_node_active_self());
+ CHECK(node->is_node_active_in_tree());
+ CHECK(child->is_node_active_in_tree());
+ CHECK(node->can_process());
+ }
+
+ SUBCASE("Active changed") {
+ node->set_node_active(false);
+
+ CHECK_FALSE(node->is_node_active_self());
+ CHECK_FALSE(node->is_node_active_in_tree());
+ CHECK(child->is_node_active_self());
+ CHECK_FALSE(child->is_node_active_in_tree());
+ CHECK_FALSE(node->can_process());
+
+ node->set_node_active(true);
+
+ CHECK(node->is_node_active_self());
+ CHECK(node->is_node_active_in_tree());
+ CHECK(child->is_node_active_in_tree());
+ }
+
+ memdelete(node);
+}
+
} // namespace TestNode
#endif // TEST_NODE_H