diff --git a/doc/classes/AwaitTweener.xml b/doc/classes/AwaitTweener.xml
new file mode 100644
index 0000000000..48903dd747
--- /dev/null
+++ b/doc/classes/AwaitTweener.xml
@@ -0,0 +1,21 @@
+
+
+
+ Awaits a specified signal.
+
+
+ [AwaitTweener] is used to await a specified signal, allowing asynchronous steps in [Tween] animation. See [method Tween.tween_await] for more usage information.
+ The [signal Tweener.finished] signal is emitted when either the awaited signal is received, when timeout is reached, or when the target object is freed.
+
+
+
+
+
+
+
+
+ Sets the maximum time an [AwaitTweener] can wait for the signal. Can be used as a safeguard for signals that may never be emitted. If not specified, the tweener will wait indefinitely.
+
+
+
+
diff --git a/doc/classes/Tween.xml b/doc/classes/Tween.xml
index f7084898c7..544ccc7746 100644
--- a/doc/classes/Tween.xml
+++ b/doc/classes/Tween.xml
@@ -7,7 +7,7 @@
Tweens are mostly useful for animations requiring a numerical property to be interpolated over a range of values. The name [i]tween[/i] comes from [i]in-betweening[/i], an animation technique where you specify [i]keyframes[/i] and the computer interpolates the frames that appear between them. Animating something with a [Tween] is called tweening.
[Tween] is more suited than [AnimationPlayer] for animations where you don't know the final values in advance. For example, interpolating a dynamically-chosen camera zoom value is best done with a [Tween]; it would be difficult to do the same thing with an [AnimationPlayer] node. Tweens are also more light-weight than [AnimationPlayer], so they are very much suited for simple animations or general tasks that don't require visual tweaking provided by the editor. They can be used in a "fire-and-forget" manner for some logic that normally would be done by code. You can e.g. make something shoot periodically by using a looped [CallbackTweener] with a delay.
A [Tween] can be created by using either [method SceneTree.create_tween] or [method Node.create_tween]. [Tween]s created manually (i.e. by using [code]Tween.new()[/code]) are invalid and can't be used for tweening values.
- A tween animation is created by adding [Tweener]s to the [Tween] object, using [method tween_property], [method tween_interval], [method tween_callback] or [method tween_method]:
+ A tween animation is created by adding [Tweener]s to the [Tween] object, using [method tween_property], [method tween_interval], [method tween_callback], [method tween_method], [method tween_subtween], or [method tween_await]:
[codeblocks]
[gdscript]
var tween = get_tree().create_tween()
@@ -317,6 +317,37 @@
[b]Note:[/b] If a Tween is stopped and not bound to any node, it will exist indefinitely until manually started or invalidated. If you lose a reference to such Tween, you can retrieve it using [method SceneTree.get_processed_tweens].
+
+
+
+
+ Creates and appends an [AwaitTweener]. This method can be used to await a signal to be emitted and create asynchronous animations or cutscenes.
+ The animation will not progress to the next step until the awaited signal is emitted or the connection becomes invalid (e.g. as a result of freeing the target object). If you know that the emission may not happen, use [method AwaitTweener.set_timeout].
+ [b]Note:[/b] The awaited signal should be emitted during the step when [AwaitTweener] is active.
+ [b]Example:[/b] An object launches itself and explodes upon collision or after 4 seconds.
+ [codeblock]
+ var tween = create_tween()
+ tween.tween_callback(launch)
+ tween.tween_await(collided).set_timeout(4.0)
+ tween.tween_callback(explode)
+ [/codeblock]
+ [b]Example:[/b] A character walks to a specific point, says some lines and walks back when the player closes the message box.
+ [codeblock]
+ var tween = create_tween()
+ tween.tween_callback(walk_to.bind(600.0))
+ tween.tween_await(destination_reached)
+ tween.tween_callback(say_dialogue.bind("Good day, sir!"))
+ tween.tween_await(dialogue_closed)
+ tween.tween_callback(walk_to.bind(0.0))
+ [/codeblock]
+ [b]Note:[/b] If you are awaiting a signal from a callback called in the same [Tween], make sure the signal is emitted [i]after[/i] the await starts. If it can't be reasonably guaranteed, you can await and emit in the same step:
+ [codeblock]
+ var tween = create_tween()
+ tween.tween_await(signal)
+ tween.parallel().tween_callback(method_that_emits_signal)
+ [/codeblock]
+
+
diff --git a/scene/animation/tween.cpp b/scene/animation/tween.cpp
index 94e74d4063..35146edb87 100644
--- a/scene/animation/tween.cpp
+++ b/scene/animation/tween.cpp
@@ -169,6 +169,14 @@ RequiredResult Tween::tween_subtween(RequiredParam rp_su
return tweener;
}
+RequiredResult Tween::tween_await(const Signal &p_signal) {
+ CHECK_VALID();
+
+ Ref tweener = memnew(AwaitTweener(p_signal));
+ append(tweener);
+ return tweener;
+}
+
void Tween::append(Ref p_tweener) {
p_tweener->set_tween(this);
@@ -474,6 +482,7 @@ void Tween::_bind_methods() {
ClassDB::bind_method(D_METHOD("tween_callback", "callback"), &Tween::tween_callback);
ClassDB::bind_method(D_METHOD("tween_method", "method", "from", "to", "duration"), &Tween::tween_method);
ClassDB::bind_method(D_METHOD("tween_subtween", "subtween"), &Tween::tween_subtween);
+ ClassDB::bind_method(D_METHOD("tween_await", "signal"), &Tween::tween_await);
ClassDB::bind_method(D_METHOD("custom_step", "delta"), &Tween::custom_step);
ClassDB::bind_method(D_METHOD("stop"), &Tween::stop);
@@ -928,3 +937,63 @@ SubtweenTweener::SubtweenTweener(const Ref &p_subtween) {
SubtweenTweener::SubtweenTweener() {
ERR_FAIL_MSG("SubtweenTweener can't be created directly. Use the tween_subtween() method in Tween.");
}
+
+Ref AwaitTweener::set_timeout(double p_timeout) {
+ timeout = p_timeout;
+ return this;
+}
+
+void AwaitTweener::start() {
+ Tweener::start();
+ received = false;
+}
+
+bool AwaitTweener::step(double &r_delta) {
+ if (finished) {
+ return false;
+ }
+
+ if (!signal.get_object() || !signal.is_connected(target_callable)) { // In case the object was destroyed before emitting.
+ _finish();
+ return false;
+ }
+
+ elapsed_time += r_delta;
+
+ if (timeout >= 0 && elapsed_time >= timeout) {
+ _finish();
+ r_delta = elapsed_time - timeout;
+ return false;
+ }
+
+ r_delta = 0; // "Consume" all remaining time to prevent infinite loops.
+ if (received) {
+ _finish();
+ return false;
+ }
+ return true;
+}
+
+AwaitTweener::AwaitTweener(const Signal &p_signal) {
+ signal = p_signal;
+ target_callable = Callable(this, SNAME("_signal_callback"));
+ signal.connect(target_callable);
+
+ Object *signal_instance = p_signal.get_object();
+ if (signal_instance && signal_instance->is_ref_counted()) {
+ ref_copy = signal_instance;
+ }
+}
+
+AwaitTweener::AwaitTweener() {
+ ERR_FAIL_MSG("AwaitTweener can't be created directly. Use the tween_await() method in Tween.");
+}
+
+void AwaitTweener::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_timeout", "timeout"), &AwaitTweener::set_timeout);
+ ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "_signal_callback", &AwaitTweener::_signal_received, MethodInfo("_signal_callback"));
+}
+
+void AwaitTweener::_signal_received(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
+ received = true;
+}
diff --git a/scene/animation/tween.h b/scene/animation/tween.h
index e417b06470..e84ead8cb5 100644
--- a/scene/animation/tween.h
+++ b/scene/animation/tween.h
@@ -61,6 +61,7 @@ class IntervalTweener;
class CallbackTweener;
class MethodTweener;
class SubtweenTweener;
+class AwaitTweener;
class Tween : public RefCounted {
GDCLASS(Tween, RefCounted);
@@ -148,6 +149,7 @@ public:
RequiredResult tween_callback(const Callable &p_callback);
RequiredResult tween_method(const Callable &p_callback, const Variant p_from, Variant p_to, double p_duration);
RequiredResult tween_subtween(RequiredParam rp_subtween);
+ RequiredResult tween_await(const Signal &p_signal);
void append(Ref p_tweener);
bool custom_step(double p_delta);
@@ -326,3 +328,30 @@ protected:
private:
double delay = 0;
};
+
+class AwaitTweener : public Tweener {
+ GDCLASS(AwaitTweener, Tweener);
+
+public:
+ Ref set_timeout(double p_timeout);
+
+ void start() override;
+ bool step(double &r_delta) override;
+
+ AwaitTweener(const Signal &p_signal);
+ AwaitTweener();
+
+protected:
+ static void _bind_methods();
+
+private:
+ Signal signal;
+ Callable target_callable;
+ bool received = false;
+
+ double timeout = -1;
+
+ void _signal_received(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
+
+ Ref ref_copy;
+};
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index dd51cdc624..5e96da0c73 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -582,6 +582,7 @@ void register_scene_types() {
GDREGISTER_CLASS(CallbackTweener);
GDREGISTER_CLASS(MethodTweener);
GDREGISTER_CLASS(SubtweenTweener);
+ GDREGISTER_CLASS(AwaitTweener);
GDREGISTER_ABSTRACT_CLASS(AnimationMixer);
GDREGISTER_CLASS(AnimationPlayer);