BlendTree nodes can now be removed.

This commit is contained in:
Martin Felis 2026-01-25 01:31:02 +01:00
parent 1e7dd4ba45
commit 4c428a865a
4 changed files with 110 additions and 27 deletions

View File

@ -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("set_tree_root", "animation_node"), &BLTAnimationGraph::set_root_animation_node);
ClassDB::bind_method(D_METHOD("get_tree_root"), &BLTAnimationGraph::get_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("set_skeleton", "skeleton"), &BLTAnimationGraph::set_skeleton);
ClassDB::bind_method(D_METHOD("get_skeleton"), &BLTAnimationGraph::get_skeleton); ClassDB::bind_method(D_METHOD("get_skeleton"), &BLTAnimationGraph::get_skeleton);

View File

@ -50,12 +50,14 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri
void BLTAnimationNodeBlendTree::_bind_methods() { void BLTAnimationNodeBlendTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); 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_node", "node_name"), &BLTAnimationNodeBlendTree::get_node);
ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_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("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("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("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); ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array);
BIND_CONSTANT(CONNECTION_OK); BIND_CONSTANT(CONNECTION_OK);
@ -435,7 +437,7 @@ Ref<BLTAnimationNode> BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_output_n
} }
int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref<BLTAnimationNode> &node) const { int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref<BLTAnimationNode> &node) const {
for (int i = 0; i < nodes.size(); i++) { for (uint32_t i = 0; i < nodes.size(); i++) {
if (nodes[i] == node) { if (nodes[i] == node) {
return i; return i;
} }
@ -445,7 +447,7 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref<BLTA
} }
int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index_by_name(const StringName &name) const { int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index_by_name(const StringName &name) const {
for (int i = 0; i < nodes.size(); i++) { for (uint32_t i = 0; i < nodes.size(); i++) {
if (nodes[i]->get_name() == name) { if (nodes[i]->get_name() == name) {
return i; return i;
} }
@ -474,6 +476,42 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref<BLTAnimati
node_connection_info.push_back(connection_info); node_connection_info.push_back(connection_info);
} }
void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref<BLTAnimationNode> &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<int> 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() { void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_and_references() {
LocalVector<int> sorted_node_indices = get_sorted_node_indices(); LocalVector<int> sorted_node_indices = get_sorted_node_indices();

View File

@ -510,7 +510,7 @@ public:
} }
}; };
Vector<Ref<BLTAnimationNode>> nodes; // All added nodes LocalVector<Ref<BLTAnimationNode>> nodes; // All added nodes
LocalVector<NodeConnectionInfo> node_connection_info; LocalVector<NodeConnectionInfo> node_connection_info;
LocalVector<BLTBlendTreeConnection> connections; LocalVector<BLTBlendTreeConnection> connections;
@ -526,6 +526,7 @@ public:
void remove_subtree_and_update_subtrees_recursive(int node, const HashSet<int> &removed_subtree_indices); void remove_subtree_and_update_subtrees_recursive(int node, const HashSet<int> &removed_subtree_indices);
void add_node(const Ref<BLTAnimationNode> &node); void add_node(const Ref<BLTAnimationNode> &node);
void remove_node(const Ref<BLTAnimationNode> &node);
ConnectionError is_connection_valid(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, StringName target_port_name) const; ConnectionError is_connection_valid(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, StringName target_port_name) const;
ConnectionError add_connection(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, const StringName &target_port_name); ConnectionError add_connection(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, const StringName &target_port_name);
@ -544,7 +545,7 @@ private:
void setup_runtime_data() { void setup_runtime_data() {
// Add nodes and allocate 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<BLTAnimationNode> node = tree_graph.nodes[i]; const Ref<BLTAnimationNode> node = tree_graph.nodes[i];
NodeRuntimeData node_runtime_data; NodeRuntimeData node_runtime_data;
@ -557,7 +558,7 @@ private:
} }
// Populate runtime data (only now is this.nodes populated to retrieve the nodes) // 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<BLTAnimationNode> node = tree_graph.nodes[i]; Ref<BLTAnimationNode> node = tree_graph.nodes[i];
NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
@ -599,6 +600,15 @@ public:
tree_graph.add_node(node); tree_graph.add_node(node);
} }
void remove_node(const Ref<BLTAnimationNode> &node) {
if (tree_initialized) {
print_error("Cannot remove node from BlendTree: BlendTree already initialized.");
return;
}
tree_graph.remove_node(node);
}
TypedArray<StringName> get_node_names_as_typed_array() const { TypedArray<StringName> get_node_names_as_typed_array() const {
Vector<StringName> vec; Vector<StringName> vec;
for (const Ref<BLTAnimationNode> &node : tree_graph.nodes) { for (const Ref<BLTAnimationNode> &node : tree_graph.nodes) {
@ -624,7 +634,7 @@ public:
} }
Ref<BLTAnimationNode> get_node_by_index(int node_index) const { Ref<BLTAnimationNode> 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<int>(tree_graph.nodes.size())) {
return nullptr; return nullptr;
} }
@ -673,7 +683,7 @@ public:
return result; return result;
} }
// overrides from SyncedAnimationNode // overrides from BLTAnimationNode
bool initialize(GraphEvaluationContext &context) override { bool initialize(GraphEvaluationContext &context) override {
if (!BLTAnimationNode::initialize(context)) { if (!BLTAnimationNode::initialize(context)) {
return false; return false;
@ -698,7 +708,7 @@ public:
GodotProfileZone("SyncedBlendTree::activate_inputs"); GodotProfileZone("SyncedBlendTree::activate_inputs");
tree_graph.nodes[0]->active = true; 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<BLTAnimationNode> &node = tree_graph.nodes[i]; const Ref<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
@ -712,7 +722,7 @@ public:
void calculate_sync_track(const Vector<Ref<BLTAnimationNode>> &input_nodes) override { void calculate_sync_track(const Vector<Ref<BLTAnimationNode>> &input_nodes) override {
GodotProfileZone("SyncedBlendTree::calculate_sync_track"); 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<BLTAnimationNode> &node = tree_graph.nodes[i]; const Ref<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { 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.delta = p_delta;
tree_graph.nodes[0]->node_time_info.position += 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<BLTAnimationNode> &node = tree_graph.nodes[i]; const Ref<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
@ -751,7 +761,7 @@ public:
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &input_datas, AnimationData &output_data) override { void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &input_datas, AnimationData &output_data) override {
ZoneScopedN("SyncedBlendTree::evaluate"); 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<BLTAnimationNode> &node = tree_graph.nodes[i]; const Ref<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {

View File

@ -505,6 +505,7 @@ 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_b, blend2_node_b, "Input0"));
REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1"));
SUBCASE("Add and remove a connection") {
HashSet<int> subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; HashSet<int> subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices;
HashSet<int> subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; HashSet<int> subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices;
HashSet<int> subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; HashSet<int> subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices;
@ -524,6 +525,40 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; 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(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));
}
} }
} //namespace TestSyncedAnimationGraph } //namespace TestSyncedAnimationGraph