Add AwaitTweener

This commit is contained in:
kobewi 2024-08-29 16:27:01 +02:00
parent bf95b62586
commit 94f9b87063
5 changed files with 152 additions and 1 deletions

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AwaitTweener" inherits="Tweener" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Awaits a specified signal.
</brief_description>
<description>
[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.
</description>
<tutorials>
</tutorials>
<methods>
<method name="set_timeout">
<return type="AwaitTweener" />
<param index="0" name="timeout" type="float" />
<description>
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.
</description>
</method>
</methods>
</class>

View file

@ -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].
</description>
</method>
<method name="tween_await">
<return type="AwaitTweener" />
<param index="0" name="signal" type="Signal" />
<description>
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]
</description>
</method>
<method name="tween_callback">
<return type="CallbackTweener" />
<param index="0" name="callback" type="Callable" />

View file

@ -169,6 +169,14 @@ RequiredResult<SubtweenTweener> Tween::tween_subtween(RequiredParam<Tween> rp_su
return tweener;
}
RequiredResult<AwaitTweener> Tween::tween_await(const Signal &p_signal) {
CHECK_VALID();
Ref<AwaitTweener> tweener = memnew(AwaitTweener(p_signal));
append(tweener);
return tweener;
}
void Tween::append(Ref<Tweener> 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<Tween> &p_subtween) {
SubtweenTweener::SubtweenTweener() {
ERR_FAIL_MSG("SubtweenTweener can't be created directly. Use the tween_subtween() method in Tween.");
}
Ref<AwaitTweener> 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;
}

View file

@ -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<CallbackTweener> tween_callback(const Callable &p_callback);
RequiredResult<MethodTweener> tween_method(const Callable &p_callback, const Variant p_from, Variant p_to, double p_duration);
RequiredResult<SubtweenTweener> tween_subtween(RequiredParam<Tween> rp_subtween);
RequiredResult<AwaitTweener> tween_await(const Signal &p_signal);
void append(Ref<Tweener> 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<AwaitTweener> 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<RefCounted> ref_copy;
};

View file

@ -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);