Repository: godotengine/godot (language: cpp)

Issue:
[Tween] PropertyTweener with a custom interpolator always stops at the 'to' value.
### Tested versions

- Tested in 4.4 stable (probably a long standing issue)

### System information

Godot v4.4.stable - Windows 11 (build 22631) - Multi-window, 3 monitors - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3050 (NVIDIA; 32.0.15.7283) - AMD Ryzen 5 4500 6-Core Processor (12 threads)

### Issue description

When tweening a property with a custom interpolator, it always set's the property to the final position after the tween finishes, rather than the value it should be at based from the custom interpolator function.

Example:

`func _ready() -> void:
	tween = create_tween()
	tween.tween_property(self, "position:x, 500.0, 2.0).set_custom_interpolator(_interpolator)

func _interpolator(time: float) -> float:
	return 0.25
`
should be aways at 25% of the way, but after the tween finishes, the x-position goes to 500.

### Steps to reproduce

- Create a function that does not return 1.0 when the input parameter is 1.0.
- Set that function as the custom interpolator for the PropertyTweener.

### Minimal reproduction project (MRP)

[tween_test_2025-03-24_07-49-49.zip](https://github.com/user-attachments/files/19437132/tween_test_2025-03-24_07-49-49.zip)

This uses almost the same example given in the docs (https://docs.godotengine.org/en/stable/classes/class_propertytweener.html#class-propertytweener-method-set-custom-interpolator).


Discussion / hints:

Fix patch:
diff --git a/scene/animation/tween.cpp b/scene/animation/tween.cpp
index 230f55de8427..db0f0e2684ba 100644
--- a/scene/animation/tween.cpp
+++ b/scene/animation/tween.cpp
@@ -542,6 +542,20 @@ Tween::Tween(SceneTree *p_parent_tree) {
 	valid = true;
 }
 
+double PropertyTweener::_get_custom_interpolated_value(const Variant &p_value) {
+	const Variant *argptr = &p_value;
+
+	Variant result;
+	Callable::CallError ce;
+	custom_method.callp(&argptr, 1, result, ce);
+	if (ce.error != Callable::CallError::CALL_OK) {
+		ERR_FAIL_V_MSG(false, "Error calling custom method from PropertyTweener: " + Variant::get_callable_error_text(custom_method, &argptr, 1, ce) + ".");
+	} else if (result.get_type() != Variant::FLOAT) {
+		ERR_FAIL_V_MSG(false, vformat("Wrong return type in PropertyTweener custom method. Expected float, got %s.", Variant::get_type_name(result.get_type())));
+	}
+	return result;
+}
+
 Ref<PropertyTweener> PropertyTweener::from(const Variant &p_value) {
 	Ref<Tween> tween = _get_tween();
 	ERR_FAIL_COND_V(tween.is_null(), nullptr);
@@ -638,17 +652,7 @@ bool PropertyTweener::step(double &r_delta) {
 	if (time < duration) {
 		if (custom_method.is_valid()) {
 			const Variant t = tween->interpolate_variant(0.0, 1.0, time, duration, trans_type, ease_type);
-			const Variant *argptr = &t;
-
-			Variant result;
-			Callable::CallError ce;
-			custom_method.callp(&argptr, 1, result, ce);
-			if (ce.error != Callable::CallError::CALL_OK) {
-				ERR_FAIL_V_MSG(false, "Error calling custom method from PropertyTweener: " + Variant::get_callable_error_text(custom_method, &argptr, 1, ce) + ".");
-			} else if (result.get_type() != Variant::FLOAT) {
-				ERR_FAIL_V_MSG(false, vformat("Wrong return type in PropertyTweener custom method. Expected float, got %s.", Variant::get_type_name(result.get_type())));
-			}
-
+			double result = _get_custom_interpolated_value(t);
 			target_instance->set_indexed(property, Animation::interpolate_variant(initial_val, final_val, result));
 		} else {
 			target_instance->set_indexed(property, tween->interpolate_variant(initial_val, delta_val, time, duration, trans_type, ease_type));
@@ -656,7 +660,12 @@ bool PropertyTweener::step(double &r_delta) {
 		r_delta = 0;
 		return true;
 	} else {
-		target_instance->set_indexed(property, final_val);
+		if (custom_method.is_valid()) {
+			double final_t = _get_custom_interpolated_value(1.0);
+			target_instance->set_indexed(property, Animation::interpolate_variant(initial_val, final_val, final_t));
+		} else {
+			target_instance->set_indexed(property, final_val);
+		}
 		r_delta = elapsed_time - delay - duration;
 		_finish();
 		return false;
diff --git a/scene/animation/tween.h b/scene/animation/tween.h
index 8d4b9eae6b48..d987b6e9a867 100644
--- a/scene/animation/tween.h
+++ b/scene/animation/tween.h
@@ -200,6 +200,8 @@ VARIANT_ENUM_CAST(Tween::EaseType);
 class PropertyTweener : public Tweener {
 	GDCLASS(PropertyTweener, Tweener);
 
+	double _get_custom_interpolated_value(const Variant &p_value);
+
 public:
 	Ref<PropertyTweener> from(const Variant &p_value);
 	Ref<PropertyTweener> from_current();


Test patch: