From 4c428a865acdf2b21a392fedb7308aaf8728bef0 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 25 Jan 2026 01:31:02 +0100 Subject: [PATCH] BlendTree nodes can now be removed. --- blendalot_animation_graph.cpp | 2 +- blendalot_animation_node.cpp | 42 ++++++++++++++++++- blendalot_animation_node.h | 28 +++++++++---- tests/test_synced_animation_graph.h | 65 ++++++++++++++++++++++------- 4 files changed, 110 insertions(+), 27 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 53a013c..ee3921b 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -23,7 +23,7 @@ void BLTAnimationGraph::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tree_root", "animation_node"), &BLTAnimationGraph::set_root_animation_node); ClassDB::bind_method(D_METHOD("get_tree_root"), &BLTAnimationGraph::get_root_animation_node); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "SyncedAnimationNode"), "set_tree_root", "get_tree_root"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "BLTAnimationNode"), "set_tree_root", "get_tree_root"); ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &BLTAnimationGraph::set_skeleton); ClassDB::bind_method(D_METHOD("get_skeleton"), &BLTAnimationGraph::get_skeleton); diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index fc3d97b..05cc9b3 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -50,12 +50,14 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri void BLTAnimationNodeBlendTree::_bind_methods() { ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); + ClassDB::bind_method(D_METHOD("remove_node", "animation_node"), &BLTAnimationNodeBlendTree::remove_node); ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); + ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); BIND_CONSTANT(CONNECTION_OK); @@ -435,7 +437,7 @@ Ref BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_output_n } int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref &node) const { - for (int i = 0; i < nodes.size(); i++) { + for (uint32_t i = 0; i < nodes.size(); i++) { if (nodes[i] == node) { return i; } @@ -445,7 +447,7 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Refget_name() == name) { return i; } @@ -474,6 +476,42 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { + int removed_node_index = find_node_index(node); + assert(removed_node_index >= 0); + + // Remove all connections to and from this node + for (uint32_t i = connections.size() - 1; i > 0; i--) { + if (connections[i].source_node == node || connections[i].target_node == node) { + remove_connection(connections[i].source_node, connections[i].target_node, connections[i].target_port_name); + } + } + + // Remove the data directly related to this node + node_connection_info.remove_at(removed_node_index); + nodes.remove_at(removed_node_index); + + // Ensure all indices are cleaned up. + for (NodeConnectionInfo &connection_info : node_connection_info) { + for (unsigned int j = 0; j < connection_info.connected_child_node_index_at_port.size(); j++) { + if (connection_info.connected_child_node_index_at_port[j] > removed_node_index) { + connection_info.connected_child_node_index_at_port[j] = connection_info.connected_child_node_index_at_port[j] - 1; + } + } + + // Map connected subtrees + HashSet old_indices = connection_info.input_subtree_node_indices; + connection_info.input_subtree_node_indices.clear(); + for (int old_index : old_indices) { + if (old_index > removed_node_index) { + connection_info.input_subtree_node_indices.insert(old_index - 1); + } else { + connection_info.input_subtree_node_indices.insert(old_index); + } + } + } +} + void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_and_references() { LocalVector sorted_node_indices = get_sorted_node_indices(); diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index d9f24c5..5c6cb2a 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -510,7 +510,7 @@ public: } }; - Vector> nodes; // All added nodes + LocalVector> nodes; // All added nodes LocalVector node_connection_info; LocalVector connections; @@ -526,6 +526,7 @@ public: void remove_subtree_and_update_subtrees_recursive(int node, const HashSet &removed_subtree_indices); void add_node(const Ref &node); + void remove_node(const Ref &node); ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); @@ -544,7 +545,7 @@ private: void setup_runtime_data() { // Add nodes and allocate runtime data - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref node = tree_graph.nodes[i]; NodeRuntimeData node_runtime_data; @@ -557,7 +558,7 @@ private: } // Populate runtime data (only now is this.nodes populated to retrieve the nodes) - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { Ref node = tree_graph.nodes[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; @@ -599,6 +600,15 @@ public: tree_graph.add_node(node); } + void remove_node(const Ref &node) { + if (tree_initialized) { + print_error("Cannot remove node from BlendTree: BlendTree already initialized."); + return; + } + + tree_graph.remove_node(node); + } + TypedArray get_node_names_as_typed_array() const { Vector vec; for (const Ref &node : tree_graph.nodes) { @@ -624,7 +634,7 @@ public: } Ref get_node_by_index(int node_index) const { - if (node_index < 0 || node_index > tree_graph.nodes.size()) { + if (node_index < 0 || node_index > static_cast(tree_graph.nodes.size())) { return nullptr; } @@ -673,7 +683,7 @@ public: return result; } - // overrides from SyncedAnimationNode + // overrides from BLTAnimationNode bool initialize(GraphEvaluationContext &context) override { if (!BLTAnimationNode::initialize(context)) { return false; @@ -698,7 +708,7 @@ public: GodotProfileZone("SyncedBlendTree::activate_inputs"); tree_graph.nodes[0]->active = true; - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -712,7 +722,7 @@ public: void calculate_sync_track(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::calculate_sync_track"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -731,7 +741,7 @@ public: tree_graph.nodes[0]->node_time_info.delta = p_delta; tree_graph.nodes[0]->node_time_info.position += p_delta; - for (int i = 1; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 1; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -751,7 +761,7 @@ public: void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { ZoneScopedN("SyncedBlendTree::evaluate"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 1b272ff..ac97d2a 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -505,24 +505,59 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_b, blend2_node_b, "Input0")); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); - HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; - HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; - HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; + SUBCASE("Add and remove a connection") { + HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; + HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; + HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; - // Add and remove connection - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); - blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); + // Add and remove connection + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); - // Check that we have the same subgraphs as before the connection - CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); - CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); - CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); + // Check that we have the same subgraphs as before the connection + CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); + CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); + CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); - // Check that we also do not - for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { - bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; - CHECK(connection_equals_removed_connection == false); + // Check that we also do not + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; + CHECK(connection_equals_removed_connection == false); + } + } + + SUBCASE("Remove a node") { + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + + int animation_sampler_node_b_index_pre_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 6); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_pre_remove].input_subtree_node_indices.size() == 5); + + blend_tree_graph.remove_node(animation_sampler_node_a); + + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == animation_sampler_node_a || connection.target_node == animation_sampler_node_a; + CHECK(is_connection_with_removed_node == false); + } + + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_post_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(animation_sampler_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove - 1); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 5); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].input_subtree_node_indices.size() == 4); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[0] == -1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[1] == blend2_node_b_index_post_remove); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); } }