From 94f9b87063713819ce2268221ee33fcf60420aca Mon Sep 17 00:00:00 2001 From: kobewi Date: Thu, 29 Aug 2024 16:27:01 +0200 Subject: [PATCH] Add AwaitTweener --- doc/classes/AwaitTweener.xml | 21 +++++++++++ doc/classes/Tween.xml | 33 +++++++++++++++- scene/animation/tween.cpp | 69 ++++++++++++++++++++++++++++++++++ scene/animation/tween.h | 29 ++++++++++++++ scene/register_scene_types.cpp | 1 + 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 doc/classes/AwaitTweener.xml 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);