From 537712c80674bccc0f5b1813c0e973388ec643b0 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 29 Dec 2025 15:25:10 +0100 Subject: [PATCH] Added saving and loading blend tree resources. --- register_types.cpp | 4 + synced_animation_node.cpp | 146 ++++++++++++++++++++++++++++ synced_animation_node.h | 87 +++++++++++++---- tests/test_synced_animation_graph.h | 58 +++++++++-- 4 files changed, 267 insertions(+), 28 deletions(-) diff --git a/register_types.cpp b/register_types.cpp index e37dd15..0ceeefd 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -8,6 +8,10 @@ void initialize_synced_blend_tree_module(ModuleInitializationLevel p_level) { return; } ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); } void uninitialize_synced_blend_tree_module(ModuleInitializationLevel p_level) { diff --git a/synced_animation_node.cpp b/synced_animation_node.cpp index 2b005e3..38e8710 100644 --- a/synced_animation_node.cpp +++ b/synced_animation_node.cpp @@ -4,6 +4,100 @@ #include "synced_animation_node.h" +void SyncedBlendTree::_get_property_list(List *p_list) const { + for (const Ref &node : nodes) { + String prop_name = node->name; + if (prop_name != "Output") { + p_list->push_back(PropertyInfo(Variant::OBJECT, "nodes/" + prop_name + "/node", PROPERTY_HINT_RESOURCE_TYPE, "AnimationNode", PROPERTY_USAGE_NO_EDITOR)); + } + p_list->push_back(PropertyInfo(Variant::VECTOR2, "nodes/" + prop_name + "/position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + } + + p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); +} + +bool SyncedBlendTree::_get(const StringName &p_name, Variant &r_value) const { + String prop_name = p_name; + if (prop_name.begins_with("nodes/")) { + String node_name = prop_name.get_slicec('/', 1); + String what = prop_name.get_slicec('/', 2); + int node_index = find_node_index_by_name(node_name); + + if (what == "node") { + if (node_index != -1) { + r_value = nodes[node_index]; + return true; + } + } + + if (what == "position") { + if (node_index != -1) { + r_value = nodes[node_index]->position; + return true; + } + } + } else if (prop_name == "node_connections") { + Array conns; + conns.resize(tree_builder.connections.size() * 3); + + int idx = 0; + for (const BlendTreeConnection &connection : tree_builder.connections) { + conns[idx * 3 + 0] = connection.target_node->name; + conns[idx * 3 + 1] = connection.target_node->get_node_input_index(connection.target_port_name); + conns[idx * 3 + 2] = connection.source_node->name; + idx++; + } + + r_value = conns; + return true; + } + + return false; +} + +bool SyncedBlendTree::_set(const StringName &p_name, const Variant &p_value) { + String prop_name = p_name; + if (prop_name.begins_with("nodes/")) { + String node_name = prop_name.get_slicec('/', 1); + String what = prop_name.get_slicec('/', 2); + + if (what == "node") { + Ref anode = p_value; + if (anode.is_valid()) { + anode->name = node_name; + add_node(anode); + } + return true; + } + + if (what == "position") { + int node_index = find_node_index_by_name(node_name); + if (node_index > -1) { + tree_builder.nodes[node_index]->position = p_value; + } + return true; + } + } else if (prop_name == "node_connections") { + Array conns = p_value; + ERR_FAIL_COND_V(conns.size() % 3 != 0, false); + + for (int i = 0; i < conns.size(); i += 3) { + int target_node_index = find_node_index_by_name(conns[i]); + int target_node_port_index = conns[i + 1]; + int source_node_index = find_node_index_by_name(conns[i + 2]); + + Ref target_node = tree_builder.nodes[target_node_index]; + Vector target_input_names; + target_node->get_input_names(target_input_names); + + add_connection(tree_builder.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); + } + return true; + } + + return false; +} + void AnimationData::sample_from_animation(const Ref &animation, const Skeleton3D *skeleton_3d, double p_time) { const Vector tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); @@ -78,7 +172,59 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Local output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position); } +void AnimationSamplerNode::set_animation(const StringName &p_name) { + animation_name = p_name; +} + +StringName AnimationSamplerNode::get_animation() const { + return animation_name; +} + +void AnimationSamplerNode::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_animation", "name"), &AnimationSamplerNode::set_animation); + ClassDB::bind_method(D_METHOD("get_animation"), &AnimationSamplerNode::get_animation); + + ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation"); +} + void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { output = *inputs[0]; output.blend(*inputs[1], blend_weight); +} + +void AnimationBlend2Node::set_use_sync(bool p_sync) { + sync = p_sync; +} + +bool AnimationBlend2Node::is_using_sync() const { + return sync; +} + +void AnimationBlend2Node::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationBlend2Node::set_use_sync); + ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationBlend2Node::is_using_sync); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync"), "set_use_sync", "is_using_sync"); +} + +void AnimationBlend2Node::_get_property_list(List *p_list) const { + p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); +} + +bool AnimationBlend2Node::_get(const StringName &p_name, Variant &r_value) const { + if (p_name == blend_amount) { + r_value = blend_weight; + return true; + } + + return false; +} + +bool AnimationBlend2Node::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == blend_amount) { + blend_weight = p_value; + return true; + } + + return false; } \ No newline at end of file diff --git a/synced_animation_node.h b/synced_animation_node.h index fbfe887..d5d8dcc 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -225,6 +225,7 @@ public: bool active = false; StringName name; + Vector2 position; virtual ~SyncedAnimationNode() override = default; virtual void initialize(GraphEvaluationContext &context) {} @@ -284,6 +285,11 @@ public: get_input_names(inputs); return inputs.size(); } + + //protected: + // void _get_property_list(List *p_list) const; + // bool _get(const StringName &p_name, Variant &r_value) const; + // bool _set(const StringName &p_name, const Variant &p_value); }; class AnimationSamplerNode : public SyncedAnimationNode { @@ -292,14 +298,22 @@ class AnimationSamplerNode : public SyncedAnimationNode { public: StringName animation_name; + void set_animation(const StringName &p_name); + StringName get_animation() const; + private: Ref animation; void initialize(GraphEvaluationContext &context) override; void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; + +protected: + static void _bind_methods(); }; class OutputNode : public SyncedAnimationNode { + GDCLASS(OutputNode, SyncedAnimationNode); + public: void get_input_names(Vector &inputs) const override { inputs.push_back("Input"); @@ -307,8 +321,12 @@ public: }; class AnimationBlend2Node : public SyncedAnimationNode { + GDCLASS(AnimationBlend2Node, SyncedAnimationNode); + public: + StringName blend_amount = PNAME("blend_amount"); float blend_weight = 0.0f; + bool sync = false; void get_input_names(Vector &inputs) const override { inputs.push_back("Input0"); @@ -316,6 +334,16 @@ public: } void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; + + void set_use_sync(bool p_sync); + bool is_using_sync() const; + +protected: + static void _bind_methods(); + + void _get_property_list(List *p_list) const; + bool _get(const StringName &p_name, Variant &r_value) const; + bool _set(const StringName &p_name, const Variant &p_value); }; struct BlendTreeConnection { @@ -375,7 +403,7 @@ struct BlendTreeBuilder { Vector> nodes; // All added nodes LocalVector node_connection_info; - Vector connections; + LocalVector connections; BlendTreeBuilder() { Ref output_node; @@ -388,7 +416,7 @@ struct BlendTreeBuilder { return nodes[0]; } - int get_node_index(const Ref &node) const { + int find_node_index(const Ref &node) const { for (int i = 0; i < nodes.size(); i++) { if (nodes[i] == node) { return i; @@ -398,7 +426,29 @@ struct BlendTreeBuilder { return -1; } + int find_node_index_by_name(const StringName &name) const { + for (int i = 0; i < nodes.size(); i++) { + if (nodes[i]->name == name) { + return i; + } + } + + return -1; + } + void add_node(const Ref &node) { + StringName node_base_name = node->name; + if (node_base_name.is_empty()) { + node_base_name = node->get_class_name(); + } + node->name = node_base_name; + + int number_suffix = 1; + while (find_node_index_by_name(node->name) != -1) { + node->name = vformat("%s %d", node_base_name, number_suffix); + number_suffix++; + } + nodes.push_back(node); node_connection_info.push_back(NodeConnectionInfo(node.ptr())); } @@ -460,12 +510,13 @@ struct BlendTreeBuilder { return false; } - int source_node_index = get_node_index(source_node); - int target_node_index = get_node_index(target_node); + int source_node_index = find_node_index(source_node); + int target_node_index = find_node_index(target_node); int target_input_port_index = target_node->get_node_input_index(target_port_name); node_connection_info[source_node_index].parent_node_index = target_node_index; node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] = source_node_index; + connections.push_back(BlendTreeConnection{ source_node, target_node, target_port_name }); add_index_and_update_subtrees_recursive(source_node_index, target_node_index); @@ -473,7 +524,7 @@ struct BlendTreeBuilder { } bool is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) { - int source_node_index = get_node_index(source_node); + int source_node_index = find_node_index(source_node); if (source_node_index == -1) { print_error("Cannot connect nodes: source node not found."); return false; @@ -484,17 +535,12 @@ struct BlendTreeBuilder { return false; } - int target_node_index = get_node_index(target_node); + int target_node_index = find_node_index(target_node); if (target_node_index == -1) { print_error("Cannot connect nodes: target node not found."); return false; } - if (target_node == get_output_node() && connections.size() > 0) { - print_error("Cannot add connection to output node: output node is already connected"); - return false; - } - Vector target_inputs; target_node->get_input_names(target_inputs); @@ -519,6 +565,8 @@ struct BlendTreeBuilder { }; class SyncedBlendTree : public SyncedAnimationNode { + GDCLASS(SyncedBlendTree, SyncedAnimationNode); + Vector> nodes; BlendTreeBuilder tree_builder; @@ -557,6 +605,11 @@ class SyncedBlendTree : public SyncedAnimationNode { } } +protected: + void _get_property_list(List *p_list) const; + bool _get(const StringName &p_name, Variant &r_value) const; + bool _set(const StringName &p_name, const Variant &p_value); + public: struct NodeRuntimeData { Vector> input_nodes; @@ -569,14 +622,12 @@ public: return tree_builder.nodes[0]; } - int get_node_index(const Ref &node) const { - for (int i = 0; i < nodes.size(); i++) { - if (nodes[i] == node) { - return i; - } - } + int find_node_index(const Ref &node) const { + return tree_builder.find_node_index(node); + } - return -1; + int find_node_index_by_name(const StringName &name) const { + return tree_builder.find_node_index_by_name(name); } void add_node(const Ref &node) { diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index eeb65ef..e5fc2f9 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -1,6 +1,7 @@ #pragma once #include "../synced_animation_graph.h" +#include "scene/animation/animation_tree.h" #include "scene/main/window.h" #include "tests/test_macros.h" @@ -105,8 +106,8 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { CHECK(tree_constructor.add_connection(animation_sampler_node0, node_blend0, "Input0")); // Ensure that subtree is properly updated - int sampler0_index = tree_constructor.get_node_index(animation_sampler_node0); - int blend0_index = tree_constructor.get_node_index(node_blend0); + int sampler0_index = tree_constructor.find_node_index(animation_sampler_node0); + int blend0_index = tree_constructor.find_node_index(node_blend0); CHECK(tree_constructor.node_connection_info[blend0_index].input_subtree_node_indices.has(sampler0_index)); // Connect blend0 to blend1 @@ -118,8 +119,8 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { CHECK(tree_constructor.add_connection(animation_sampler_node1, node_blend0, "Input1")); // Ensure that subtree is properly updated - int sampler1_index = tree_constructor.get_node_index(animation_sampler_node0); - int blend1_index = tree_constructor.get_node_index(node_blend1); + int sampler1_index = tree_constructor.find_node_index(animation_sampler_node0); + int blend1_index = tree_constructor.find_node_index(node_blend1); CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(sampler1_index)); CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(sampler0_index)); CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(blend0_index)); @@ -262,12 +263,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph synced_blend_tree_node->initialize(synced_animation_graph->get_context()); - // int sampler_node_1_index = synced_blend_tree_node->get_node_index(animation_sampler_node_1); - // const SyncedBlendTree::NodeRuntimeData &sampler_node_1_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_1_index]; - - // int sampler_node_2_index = synced_blend_tree_node->get_node_index(animation_sampler_node_2); - // const SyncedBlendTree::NodeRuntimeData &sampler_node_2_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_2_index]; - int blend2_node_index = synced_blend_tree_node->get_node_index(blend2_node); + int blend2_node_index = synced_blend_tree_node->find_node_index(blend2_node); const SyncedBlendTree::NodeRuntimeData &blend2_runtime_data = synced_blend_tree_node->_node_runtime_data[blend2_node_index]; CHECK(blend2_runtime_data.input_nodes[0] == animation_sampler_node_a); @@ -288,6 +284,48 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.x == doctest::Approx(0.75)); CHECK(hip_bone_position.y == doctest::Approx(1.5)); CHECK(hip_bone_position.z == doctest::Approx(2.25)); + + // Test saving and loading of the blend tree to a resource + ResourceSaver::save(synced_blend_tree_node, "synced_blend_tree_node.tres"); + + REQUIRE(ClassDB::class_exists("AnimationSamplerNode")); + + // Load blend tree + Ref loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres"); + REQUIRE(loaded_synced_blend_tree.is_valid()); + + loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); + synced_animation_graph->set_graph_root_node(loaded_synced_blend_tree); + + // Re-evaluate using a different time. All animation samplers will start again from 0. + SceneTree::get_singleton()->process(0.2); + + hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + + CHECK(hip_bone_position.x == doctest::Approx(0.3)); + CHECK(hip_bone_position.y == doctest::Approx(0.6)); + CHECK(hip_bone_position.z == doctest::Approx(0.9)); +} + +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] Serialize AnimationTree" * doctest::skip(true)) { + AnimationTree *animation_tree = memnew(AnimationTree); + + character_node->add_child(animation_tree); + animation_tree->set_animation_player(player_node->get_path()); + animation_tree->set_root_node(character_node->get_path()); + Ref animation_node_animation; + animation_node_animation.instantiate(); + animation_node_animation->set_animation("TestAnimationA"); + + Ref animation_node_blend_tree; + animation_node_blend_tree.instantiate(); + animation_node_blend_tree->add_node("SamplerTestAnimationA", animation_node_animation, Vector2(0, 0)); + animation_node_blend_tree->connect_node("output", 0, "SamplerTestAnimationA"); + animation_node_blend_tree->setup_local_to_scene(); + + animation_tree->set_root_animation_node(animation_node_blend_tree); + + ResourceSaver::save(animation_node_blend_tree, "animation_tree.tres"); } } //namespace TestSyncedAnimationGraph \ No newline at end of file