Jelajahi Sumber

Merge pull request #71418 from TokageItLab/restart-anim-tree

Allow AnimationStateMachine / AnimationNode to restart when transitioning to the same state
Rémi Verschelde 2 tahun lalu
induk
melakukan
d919d77367

+ 7 - 0
doc/classes/AnimationNode.xml

@@ -49,6 +49,13 @@
 				When inheriting from [AnimationRootNode], implement this virtual method to return whether the blend tree editor should display filter editing on this node.
 			</description>
 		</method>
+		<method name="_is_parameter_read_only" qualifiers="virtual const">
+			<return type="bool" />
+			<param index="0" name="parameter" type="StringName" />
+			<description>
+				When inheriting from [AnimationRootNode], implement this virtual method to return whether the [param parameter] is read-only. Parameters are custom local memory used for your nodes, given a resource can be reused in multiple trees.
+			</description>
+		</method>
 		<method name="_process" qualifiers="virtual const">
 			<return type="float" />
 			<param index="0" name="time" type="float" />

+ 6 - 0
doc/classes/AnimationNodeOneShot.xml

@@ -28,6 +28,12 @@
 		</member>
 	</members>
 	<constants>
+		<constant name="ONE_SHOT_REQUEST_NONE" value="0" enum="OneShotRequest">
+		</constant>
+		<constant name="ONE_SHOT_REQUEST_FIRE" value="1" enum="OneShotRequest">
+		</constant>
+		<constant name="ONE_SHOT_REQUEST_ABORT" value="2" enum="OneShotRequest">
+		</constant>
 		<constant name="MIX_MODE_BLEND" value="0" enum="MixMode">
 		</constant>
 		<constant name="MIX_MODE_ADD" value="1" enum="MixMode">

+ 6 - 0
doc/classes/AnimationNodeTransition.xml

@@ -12,6 +12,12 @@
 		<link title="Third Person Shooter Demo">https://godotengine.org/asset-library/asset/678</link>
 	</tutorials>
 	<methods>
+		<method name="find_input_caption" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="caption" type="String" />
+			<description>
+			</description>
+		</method>
 		<method name="get_input_caption" qualifiers="const">
 			<return type="String" />
 			<param index="0" name="input" type="int" />

+ 7 - 2
editor/editor_properties.cpp

@@ -342,12 +342,17 @@ void EditorPropertyTextEnum::_notification(int p_what) {
 }
 
 EditorPropertyTextEnum::EditorPropertyTextEnum() {
+	HBoxContainer *hb = memnew(HBoxContainer);
+	add_child(hb);
+
 	default_layout = memnew(HBoxContainer);
-	add_child(default_layout);
+	default_layout->set_h_size_flags(SIZE_EXPAND_FILL);
+	hb->add_child(default_layout);
 
 	edit_custom_layout = memnew(HBoxContainer);
+	edit_custom_layout->set_h_size_flags(SIZE_EXPAND_FILL);
 	edit_custom_layout->hide();
-	add_child(edit_custom_layout);
+	hb->add_child(edit_custom_layout);
 
 	option_button = memnew(OptionButton);
 	option_button->set_h_size_flags(SIZE_EXPAND_FILL);

+ 1 - 1
editor/plugins/animation_blend_tree_editor_plugin.cpp

@@ -187,7 +187,7 @@ void AnimationNodeBlendTreeEditor::update_graph() {
 			String base_path = AnimationTreeEditor::get_singleton()->get_base_path() + String(E) + "/" + F.name;
 			EditorProperty *prop = EditorInspector::instantiate_property_editor(tree, F.type, base_path, F.hint, F.hint_string, F.usage);
 			if (prop) {
-				prop->set_read_only(read_only);
+				prop->set_read_only(read_only || (F.usage & PROPERTY_USAGE_READ_ONLY));
 				prop->set_object_and_property(tree, base_path);
 				prop->update_property();
 				prop->set_name_split_ratio(0);

+ 104 - 42
scene/animation/animation_blend_tree.cpp

@@ -229,15 +229,17 @@ AnimationNodeSync::AnimationNodeSync() {
 ////////////////////////////////////////////////////////
 
 void AnimationNodeOneShot::get_parameter_list(List<PropertyInfo> *r_list) const {
-	r_list->push_back(PropertyInfo(Variant::BOOL, active));
-	r_list->push_back(PropertyInfo(Variant::BOOL, prev_active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
+	r_list->push_back(PropertyInfo(Variant::BOOL, active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY));
+	r_list->push_back(PropertyInfo(Variant::INT, request, PROPERTY_HINT_ENUM, ",Fire,Abort"));
 	r_list->push_back(PropertyInfo(Variant::FLOAT, time, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 	r_list->push_back(PropertyInfo(Variant::FLOAT, remaining, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 	r_list->push_back(PropertyInfo(Variant::FLOAT, time_to_restart, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 }
 
 Variant AnimationNodeOneShot::get_parameter_default_value(const StringName &p_parameter) const {
-	if (p_parameter == active || p_parameter == prev_active) {
+	if (p_parameter == request) {
+		return ONE_SHOT_REQUEST_NONE;
+	} else if (p_parameter == active) {
 		return false;
 	} else if (p_parameter == time_to_restart) {
 		return -1;
@@ -246,6 +248,13 @@ Variant AnimationNodeOneShot::get_parameter_default_value(const StringName &p_pa
 	}
 }
 
+bool AnimationNodeOneShot::is_parameter_read_only(const StringName &p_parameter) const {
+	if (p_parameter == active) {
+		return true;
+	}
+	return false;
+}
+
 void AnimationNodeOneShot::set_fadein_time(double p_time) {
 	fade_in = p_time;
 }
@@ -303,41 +312,42 @@ bool AnimationNodeOneShot::has_filter() const {
 }
 
 double AnimationNodeOneShot::process(double p_time, bool p_seek, bool p_is_external_seeking) {
+	OneShotRequest cur_request = static_cast<OneShotRequest>((int)get_parameter(request));
 	bool cur_active = get_parameter(active);
-	bool cur_prev_active = get_parameter(prev_active);
 	double cur_time = get_parameter(time);
 	double cur_remaining = get_parameter(remaining);
 	double cur_time_to_restart = get_parameter(time_to_restart);
 
-	if (!cur_active) {
-		//make it as if this node doesn't exist, pass input 0 by.
-		if (cur_prev_active) {
-			set_parameter(prev_active, false);
-		}
+	set_parameter(request, ONE_SHOT_REQUEST_NONE);
+
+	bool do_start = cur_request == ONE_SHOT_REQUEST_FIRE;
+	if (cur_request == ONE_SHOT_REQUEST_ABORT) {
+		set_parameter(active, false);
+		set_parameter(time_to_restart, -1);
+		return blend_input(0, p_time, p_seek, p_is_external_seeking, 1.0, FILTER_IGNORE, sync);
+	} else if (!do_start && !cur_active) {
 		if (cur_time_to_restart >= 0.0 && !p_seek) {
 			cur_time_to_restart -= p_time;
 			if (cur_time_to_restart < 0) {
-				//restart
-				set_parameter(active, true);
-				cur_active = true;
+				do_start = true; // Restart.
 			}
 			set_parameter(time_to_restart, cur_time_to_restart);
 		}
-
-		return blend_input(0, p_time, p_seek, p_is_external_seeking, 1.0, FILTER_IGNORE, sync);
+		if (!do_start) {
+			return blend_input(0, p_time, p_seek, p_is_external_seeking, 1.0, FILTER_IGNORE, sync);
+		}
 	}
 
 	bool os_seek = p_seek;
-
 	if (p_seek) {
 		cur_time = p_time;
 	}
-	bool do_start = !cur_prev_active;
 
 	if (do_start) {
 		cur_time = 0;
 		os_seek = true;
-		set_parameter(prev_active, true);
+		set_parameter(request, ONE_SHOT_REQUEST_NONE);
+		set_parameter(active, true);
 	}
 
 	real_t blend;
@@ -375,7 +385,6 @@ double AnimationNodeOneShot::process(double p_time, bool p_seek, bool p_is_exter
 		cur_remaining = os_rem;
 		if (cur_remaining <= 0) {
 			set_parameter(active, false);
-			set_parameter(prev_active, false);
 			if (autorestart) {
 				double restart_sec = autorestart_delay + Math::randd() * autorestart_random_delay;
 				set_parameter(time_to_restart, restart_sec);
@@ -419,6 +428,10 @@ void AnimationNodeOneShot::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "autorestart_delay", PROPERTY_HINT_RANGE, "0,60,0.01,or_greater,suffix:s"), "set_autorestart_delay", "get_autorestart_delay");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "autorestart_random_delay", PROPERTY_HINT_RANGE, "0,60,0.01,or_greater,suffix:s"), "set_autorestart_random_delay", "get_autorestart_random_delay");
 
+	BIND_ENUM_CONSTANT(ONE_SHOT_REQUEST_NONE);
+	BIND_ENUM_CONSTANT(ONE_SHOT_REQUEST_FIRE);
+	BIND_ENUM_CONSTANT(ONE_SHOT_REQUEST_ABORT);
+
 	BIND_ENUM_CONSTANT(MIX_MODE_BLEND);
 	BIND_ENUM_CONSTANT(MIX_MODE_ADD);
 }
@@ -640,9 +653,10 @@ void AnimationNodeTransition::get_parameter_list(List<PropertyInfo> *r_list) con
 		anims += inputs[i].name;
 	}
 
-	r_list->push_back(PropertyInfo(Variant::INT, current, PROPERTY_HINT_ENUM, anims));
-	r_list->push_back(PropertyInfo(Variant::INT, prev_current, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
-	r_list->push_back(PropertyInfo(Variant::INT, prev, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
+	r_list->push_back(PropertyInfo(Variant::STRING, current_state, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY)); // For interface.
+	r_list->push_back(PropertyInfo(Variant::STRING, transition_request, PROPERTY_HINT_ENUM, anims)); // For transition request. It will be cleared after setting the value immediately.
+	r_list->push_back(PropertyInfo(Variant::INT, current_index, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY)); // To avoid finding the index every frame, use this internally.
+	r_list->push_back(PropertyInfo(Variant::INT, prev_index, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 	r_list->push_back(PropertyInfo(Variant::FLOAT, time, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 	r_list->push_back(PropertyInfo(Variant::FLOAT, prev_xfading, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
 }
@@ -650,13 +664,22 @@ void AnimationNodeTransition::get_parameter_list(List<PropertyInfo> *r_list) con
 Variant AnimationNodeTransition::get_parameter_default_value(const StringName &p_parameter) const {
 	if (p_parameter == time || p_parameter == prev_xfading) {
 		return 0.0;
-	} else if (p_parameter == prev || p_parameter == prev_current) {
+	} else if (p_parameter == prev_index) {
 		return -1;
+	} else if (p_parameter == transition_request || p_parameter == current_state) {
+		return String();
 	} else {
 		return 0;
 	}
 }
 
+bool AnimationNodeTransition::is_parameter_read_only(const StringName &p_parameter) const {
+	if (p_parameter == current_state || p_parameter == current_index) {
+		return true;
+	}
+	return false;
+}
+
 String AnimationNodeTransition::get_caption() const {
 	return "Transition";
 }
@@ -702,6 +725,17 @@ String AnimationNodeTransition::get_input_caption(int p_input) const {
 	return inputs[p_input].name;
 }
 
+int AnimationNodeTransition::find_input_caption(const String &p_name) const {
+	int idx = -1;
+	for (int i = 0; i < MAX_INPUTS; i++) {
+		if (inputs[i].name == p_name) {
+			idx = i;
+			break;
+		}
+	}
+	return idx;
+}
+
 void AnimationNodeTransition::set_xfade_time(double p_fade) {
 	xfade_time = p_fade;
 }
@@ -727,26 +761,53 @@ bool AnimationNodeTransition::is_reset() const {
 }
 
 double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_external_seeking) {
-	int cur_current = get_parameter(current);
-	int cur_prev = get_parameter(prev);
-	int cur_prev_current = get_parameter(prev_current);
+	String cur_transition_request = get_parameter(transition_request);
+	int cur_current_index = get_parameter(current_index);
+	int cur_prev_index = get_parameter(prev_index);
 
 	double cur_time = get_parameter(time);
 	double cur_prev_xfading = get_parameter(prev_xfading);
 
-	bool switched = cur_current != cur_prev_current;
+	bool switched = false;
+	bool restart = false;
+
+	if (!cur_transition_request.is_empty()) {
+		int new_idx = find_input_caption(cur_transition_request);
+		if (new_idx >= 0) {
+			if (cur_current_index == new_idx) {
+				// Transition to same state.
+				restart = reset;
+				cur_prev_xfading = 0;
+				set_parameter(prev_xfading, 0);
+				cur_prev_index = -1;
+				set_parameter(prev_index, -1);
+			} else {
+				switched = true;
+				cur_prev_index = cur_current_index;
+				set_parameter(prev_index, cur_current_index);
+			}
+			cur_current_index = new_idx;
+			set_parameter(current_index, cur_current_index);
+			set_parameter(current_state, cur_transition_request);
+		} else {
+			ERR_PRINT("No such input: '" + cur_transition_request + "'");
+		}
+		cur_transition_request = String();
+		set_parameter(transition_request, cur_transition_request);
+	}
 
-	if (switched) {
-		set_parameter(prev_current, cur_current);
-		set_parameter(prev, cur_prev_current);
+	// Special case for restart.
+	if (restart) {
+		set_parameter(time, 0);
+		return blend_input(cur_current_index, 0, true, p_is_external_seeking, 1.0, FILTER_IGNORE, true);
+	}
 
-		cur_prev = cur_prev_current;
+	if (switched) {
 		cur_prev_xfading = xfade_time;
 		cur_time = 0;
-		switched = true;
 	}
 
-	if (cur_current < 0 || cur_current >= enabled_inputs || cur_prev >= enabled_inputs) {
+	if (cur_current_index < 0 || cur_current_index >= enabled_inputs || cur_prev_index >= enabled_inputs) {
 		return 0;
 	}
 
@@ -754,15 +815,15 @@ double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_ex
 
 	if (sync) {
 		for (int i = 0; i < enabled_inputs; i++) {
-			if (i != cur_current && i != cur_prev) {
+			if (i != cur_current_index && i != cur_prev_index) {
 				blend_input(i, p_time, p_seek, p_is_external_seeking, 0, FILTER_IGNORE, true);
 			}
 		}
 	}
 
-	if (cur_prev < 0) { // process current animation, check for transition
+	if (cur_prev_index < 0) { // process current animation, check for transition
 
-		rem = blend_input(cur_current, p_time, p_seek, p_is_external_seeking, 1.0, FILTER_IGNORE, true);
+		rem = blend_input(cur_current_index, p_time, p_seek, p_is_external_seeking, 1.0, FILTER_IGNORE, true);
 
 		if (p_seek) {
 			cur_time = p_time;
@@ -770,8 +831,8 @@ double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_ex
 			cur_time += p_time;
 		}
 
-		if (inputs[cur_current].auto_advance && rem <= xfade_time) {
-			set_parameter(current, (cur_current + 1) % enabled_inputs);
+		if (inputs[cur_current_index].auto_advance && rem <= xfade_time) {
+			set_parameter(transition_request, get_input_caption((cur_current_index + 1) % enabled_inputs));
 		}
 
 	} else { // cross-fading from prev to current
@@ -784,20 +845,20 @@ double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_ex
 		// Blend values must be more than CMP_EPSILON to process discrete keys in edge.
 		real_t blend_inv = 1.0 - blend;
 		if (reset && !p_seek && switched) { //just switched, seek to start of current
-			rem = blend_input(cur_current, 0, true, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
+			rem = blend_input(cur_current_index, 0, true, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
 		} else {
-			rem = blend_input(cur_current, p_time, p_seek, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
+			rem = blend_input(cur_current_index, p_time, p_seek, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
 		}
 
 		if (p_seek) {
-			blend_input(cur_prev, p_time, true, p_is_external_seeking, Math::is_zero_approx(blend) ? CMP_EPSILON : blend, FILTER_IGNORE, true);
+			blend_input(cur_prev_index, p_time, true, p_is_external_seeking, Math::is_zero_approx(blend) ? CMP_EPSILON : blend, FILTER_IGNORE, true);
 			cur_time = p_time;
 		} else {
-			blend_input(cur_prev, p_time, false, p_is_external_seeking, Math::is_zero_approx(blend) ? CMP_EPSILON : blend, FILTER_IGNORE, true);
+			blend_input(cur_prev_index, p_time, false, p_is_external_seeking, Math::is_zero_approx(blend) ? CMP_EPSILON : blend, FILTER_IGNORE, true);
 			cur_time += p_time;
 			cur_prev_xfading -= p_time;
 			if (cur_prev_xfading < 0) {
-				set_parameter(prev, -1);
+				set_parameter(prev_index, -1);
 			}
 		}
 	}
@@ -829,6 +890,7 @@ void AnimationNodeTransition::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("set_input_caption", "input", "caption"), &AnimationNodeTransition::set_input_caption);
 	ClassDB::bind_method(D_METHOD("get_input_caption", "input"), &AnimationNodeTransition::get_input_caption);
+	ClassDB::bind_method(D_METHOD("find_input_caption", "caption"), &AnimationNodeTransition::find_input_caption);
 
 	ClassDB::bind_method(D_METHOD("set_xfade_time", "time"), &AnimationNodeTransition::set_xfade_time);
 	ClassDB::bind_method(D_METHOD("get_xfade_time"), &AnimationNodeTransition::get_xfade_time);

+ 19 - 17
scene/animation/animation_blend_tree.h

@@ -96,6 +96,12 @@ class AnimationNodeOneShot : public AnimationNodeSync {
 	GDCLASS(AnimationNodeOneShot, AnimationNodeSync);
 
 public:
+	enum OneShotRequest {
+		ONE_SHOT_REQUEST_NONE,
+		ONE_SHOT_REQUEST_FIRE,
+		ONE_SHOT_REQUEST_ABORT,
+	};
+
 	enum MixMode {
 		MIX_MODE_BLEND,
 		MIX_MODE_ADD
@@ -110,13 +116,8 @@ private:
 	double autorestart_random_delay = 0.0;
 	MixMode mix = MIX_MODE_BLEND;
 
-	/*	bool active;
-	bool do_start;
-	double time;
-	double remaining;*/
-
+	StringName request = PNAME("request");
 	StringName active = PNAME("active");
-	StringName prev_active = "prev_active";
 	StringName time = "time";
 	StringName remaining = "remaining";
 	StringName time_to_restart = "time_to_restart";
@@ -127,6 +128,7 @@ protected:
 public:
 	virtual void get_parameter_list(List<PropertyInfo> *r_list) const override;
 	virtual Variant get_parameter_default_value(const StringName &p_parameter) const override;
+	virtual bool is_parameter_read_only(const StringName &p_parameter) const override;
 
 	virtual String get_caption() const override;
 
@@ -153,6 +155,7 @@ public:
 	AnimationNodeOneShot();
 };
 
+VARIANT_ENUM_CAST(AnimationNodeOneShot::OneShotRequest)
 VARIANT_ENUM_CAST(AnimationNodeOneShot::MixMode)
 
 class AnimationNodeAdd2 : public AnimationNodeSync {
@@ -284,18 +287,15 @@ class AnimationNodeTransition : public AnimationNodeSync {
 	InputData inputs[MAX_INPUTS];
 	int enabled_inputs = 0;
 
-	/*
-	double prev_xfading;
-	int prev;
-	double time;
-	int current;
-	int prev_current; */
-
-	StringName prev_xfading = "prev_xfading";
-	StringName prev = "prev";
 	StringName time = "time";
-	StringName current = PNAME("current");
-	StringName prev_current = "prev_current";
+	StringName prev_xfading = "prev_xfading";
+	StringName prev_index = "prev_index";
+	StringName current_index = PNAME("current_index");
+	StringName current_state = PNAME("current_state");
+	StringName transition_request = PNAME("transition_request");
+
+	StringName prev_frame_current = "pf_current";
+	StringName prev_frame_current_idx = "pf_current_idx";
 
 	double xfade_time = 0.0;
 	Ref<Curve> xfade_curve;
@@ -310,6 +310,7 @@ protected:
 public:
 	virtual void get_parameter_list(List<PropertyInfo> *r_list) const override;
 	virtual Variant get_parameter_default_value(const StringName &p_parameter) const override;
+	virtual bool is_parameter_read_only(const StringName &p_parameter) const override;
 
 	virtual String get_caption() const override;
 
@@ -321,6 +322,7 @@ public:
 
 	void set_input_caption(int p_input, const String &p_name);
 	String get_input_caption(int p_input) const;
+	int find_input_caption(const String &p_name) const;
 
 	void set_xfade_time(double p_fade);
 	double get_xfade_time() const;

+ 1 - 1
scene/animation/animation_node_state_machine.cpp

@@ -236,7 +236,7 @@ bool AnimationNodeStateMachinePlayback::_travel(AnimationNodeStateMachine *p_sta
 	path.clear(); //a new one will be needed
 
 	if (current == p_travel) {
-		return true; //nothing to do
+		return false; // Will teleport oneself (restart).
 	}
 
 	Vector2 current_pos = p_state_machine->states[current].position;

+ 18 - 5
scene/animation/animation_tree.cpp

@@ -54,13 +54,19 @@ Variant AnimationNode::get_parameter_default_value(const StringName &p_parameter
 	return ret;
 }
 
+bool AnimationNode::is_parameter_read_only(const StringName &p_parameter) const {
+	bool ret = false;
+	GDVIRTUAL_CALL(_is_parameter_read_only, p_parameter, ret);
+	return ret;
+}
+
 void AnimationNode::set_parameter(const StringName &p_name, const Variant &p_value) {
 	ERR_FAIL_COND(!state);
 	ERR_FAIL_COND(!state->tree->property_parent_map.has(base_path));
 	ERR_FAIL_COND(!state->tree->property_parent_map[base_path].has(p_name));
 	StringName path = state->tree->property_parent_map[base_path][p_name];
 
-	state->tree->property_map[path] = p_value;
+	state->tree->property_map[path].first = p_value;
 }
 
 Variant AnimationNode::get_parameter(const StringName &p_name) const {
@@ -69,7 +75,7 @@ Variant AnimationNode::get_parameter(const StringName &p_name) const {
 	ERR_FAIL_COND_V(!state->tree->property_parent_map[base_path].has(p_name), Variant());
 
 	StringName path = state->tree->property_parent_map[base_path][p_name];
-	return state->tree->property_map[path];
+	return state->tree->property_map[path].first;
 }
 
 void AnimationNode::get_child_nodes(List<ChildNode> *r_child_nodes) {
@@ -427,6 +433,7 @@ void AnimationNode::_bind_methods() {
 	GDVIRTUAL_BIND(_get_parameter_list);
 	GDVIRTUAL_BIND(_get_child_by_name, "name");
 	GDVIRTUAL_BIND(_get_parameter_default_value, "parameter");
+	GDVIRTUAL_BIND(_is_parameter_read_only, "parameter");
 	GDVIRTUAL_BIND(_process, "time", "seek", "is_external_seeking");
 	GDVIRTUAL_BIND(_get_caption);
 	GDVIRTUAL_BIND(_has_filter);
@@ -1889,7 +1896,10 @@ void AnimationTree::_update_properties_for_node(const String &p_base_path, Ref<A
 		StringName key = pinfo.name;
 
 		if (!property_map.has(p_base_path + key)) {
-			property_map[p_base_path + key] = node->get_parameter_default_value(key);
+			Pair<Variant, bool> param;
+			param.first = node->get_parameter_default_value(key);
+			param.second = node->is_parameter_read_only(key);
+			property_map[p_base_path + key] = param;
 		}
 
 		property_parent_map[p_base_path][key] = p_base_path + key;
@@ -1931,7 +1941,10 @@ bool AnimationTree::_set(const StringName &p_name, const Variant &p_value) {
 	}
 
 	if (property_map.has(p_name)) {
-		property_map[p_name] = p_value;
+		if (is_inside_tree() && property_map[p_name].second) {
+			return false; // Prevent to set property by user.
+		}
+		property_map[p_name].first = p_value;
 		return true;
 	}
 
@@ -1944,7 +1957,7 @@ bool AnimationTree::_get(const StringName &p_name, Variant &r_ret) const {
 	}
 
 	if (property_map.has(p_name)) {
-		r_ret = property_map[p_name];
+		r_ret = property_map[p_name].first;
 		return true;
 	}
 

+ 3 - 1
scene/animation/animation_tree.h

@@ -117,6 +117,7 @@ protected:
 	GDVIRTUAL0RC(Array, _get_parameter_list)
 	GDVIRTUAL1RC(Ref<AnimationNode>, _get_child_by_name, StringName)
 	GDVIRTUAL1RC(Variant, _get_parameter_default_value, StringName)
+	GDVIRTUAL1RC(bool, _is_parameter_read_only, StringName)
 	GDVIRTUAL3RC(double, _process, double, bool, bool)
 	GDVIRTUAL0RC(String, _get_caption)
 	GDVIRTUAL0RC(bool, _has_filter)
@@ -124,6 +125,7 @@ protected:
 public:
 	virtual void get_parameter_list(List<PropertyInfo> *r_list) const;
 	virtual Variant get_parameter_default_value(const StringName &p_parameter) const;
+	virtual bool is_parameter_read_only(const StringName &p_parameter) const;
 
 	void set_parameter(const StringName &p_name, const Variant &p_value);
 	Variant get_parameter(const StringName &p_name) const;
@@ -304,7 +306,7 @@ private:
 	void _update_properties();
 	List<PropertyInfo> properties;
 	HashMap<StringName, HashMap<StringName, StringName>> property_parent_map;
-	HashMap<StringName, Variant> property_map;
+	HashMap<StringName, Pair<Variant, bool>> property_map; // Property value and read-only flag.
 
 	struct Activity {
 		uint64_t last_pass = 0;