Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AwaitTweener #79712

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Add AwaitTweener #79712

wants to merge 1 commit into from

Conversation

KoBeWi
Copy link
Member

@KoBeWi KoBeWi commented Jul 20, 2023

Closes godotengine/godot-proposals#7337

Implemented more or less as described in the proposal, see the docs for details.

Using custom_step on the tween should finish the Tweener immediately.

This was not possible, because custom step works as if the time has passed. I could maybe add a method to AwaitTweener to force it to finish, but no other Tweener has such method (though maybe it could be in the base class?).


Example:

extends RigidBody2D

func _ready() -> void:
	var tween := create_tween().set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
	tween.tween_property(self, ^"modulate", Color.RED, 1.0)
	tween.tween_property(self, ^"freeze", false, 0)
	tween.tween_await($"../Area2D".body_entered).set_unbinds(1)
	tween.tween_callback(queue_free)
godot.windows.editor.dev.x86_64_gh2jrUpbxv.mp4

@Marigem
Copy link

Marigem commented Jul 20, 2023

Thinking about it, the only time I use custom_step on tweens is when I want to instantly finalize an animation in a predictable manner. For example:

func animation():
    tween = create_tween().set_process_mode(Tween.TWEEN_PROCESS_PHYSICS).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
    tween.tween_property(some_control, "modulate:a", 1.0, 0.25)
    tween.tween_property(some_control, "position:x", final_pos, 0.25)
    tween.tween_property(other_control, "modulate:a", 1.0, 0.25)
    tween.tween_property(other_control, "position:x", final_pos2, 0.25)
    ...
    
func skip_animation():
    if tween:
        while(tween.custom_step(10.0)):
            continue
        tween.kill()

When you use skip_animation(), you can know exactly what the final state of the objects will be.

But if you finish an await tween with custom_step, the object that is being waited on to emit its signal, would still be doing it's thing regardless of the tweener, so the animation would end in an unpredictable state. Maybe custom_step shouldn't do anything for the await tweener and shouldn't be expected to. That also should be noted in the docs if it ends up being the case.

@KoBeWi
Copy link
Member Author

KoBeWi commented Jul 20, 2023

custom_step() will only progress the timeout, it does not affect waiting for the signal. AwaitTweener will wait no matter how much time passes.

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from 97bacf3 to bd12682 Compare July 20, 2023 17:11
@Marigem
Copy link

Marigem commented Jul 20, 2023

Currently, if the timeout is set to 0, it will skip the check and step won't return false.

bool AwaitTweener::step(double &r_delta) {
	if (timeout > 0) {
		timeout -= r_delta;
		if (timeout <= 0) {
			return false;
		}
	}

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from bd12682 to e8bb39a Compare July 20, 2023 17:34
@KoBeWi
Copy link
Member Author

KoBeWi commented Jul 20, 2023

Changed. 0 timeout makes the tweener finish immediately, so it's a weird use-case, but why not.

@dalexeev
Copy link
Member

Maybe I'm worried for nothing, but wouldn't the name tween_await_signal (and AwaitSignalTweener) be more clear? In GDScript, you can use await with signals, coroutines, and arbitrary values (they will just be passed as is). And tween_await can only be used with signals. Yes, this is checked by the method signature, but not everyone uses typed GDScript, and in some rare cases the error will only occur at runtime. This is just a suggestion, not a request.

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from e8bb39a to 0d26900 Compare July 20, 2023 21:12
@KoBeWi
Copy link
Member Author

KoBeWi commented Jul 20, 2023

The autocompletion type hint appears even if your Tween is not typed (not to mention the argument is literally called signal), so I don't think it will cause confusion:
image

@Meorge
Copy link
Contributor

Meorge commented Jul 11, 2024

Apparently I came up with the same idea yesterday and tried to implement it myself, and came remarkably close to this PR without even knowing it existed 😅 .

There were two things I had in my implementation that I don't see here, that I thought might be worth bringing up as things to add before a merge. (I'm much less experienced with the engine than you are, though, so please let me know if there are reasons for these that I'm just not aware of!)

Unbinds

As I understand, this version of tween_await requires the user to know the number of arguments the signal will pass to all of its connected methods, so they can set it with set_unbinds. While in most cases this should be pretty trivial for the user to determine, it still feels to me like a bit of an unnecessary step – IMO, the engine itself should be able to read the signal it's passed, count the number of arguments, and then use that value.

I attempted to solve this problem on my version by getting the signal's parent's signal list, iterating through it to find the correct signal by name, and counting its arguments there. AThousandShips and I agreed that it feels like a rather hacky workaround, and could potentially fall apart at some point. The solution in this PR definitely feels safer in that regard, but as I said before I think it would be preferable to let the engine count the number of unbinds necessary. (Maybe this would be a reason to file a feature proposal or issue for adding a get_arguments or similar method for Signal?)

Various bits of code to match other Tweeners

For compatibility/parity with the other Tweeners, I think

emit_signal(SceneStringName(finished));

should be included in AwaitTweener::step for the first time it is run after the signal is received. There are also some modifications of r_delta and elapsed_time that I saw in other similar tweeners like CallbackTweener and IntervalTweener that I copied over for my version of AwaitTweener, although I'm not as sure how important those are.

Overall I'm really excited that this implementation exists, and I'm hoping it can make its way to 4.4! I really like tween sequences, and I think this will be a very valuable tool for using them more extensively! 😄

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from 0d26900 to a12a8a6 Compare July 12, 2024 23:04
@KoBeWi
Copy link
Member Author

KoBeWi commented Jul 12, 2024

Unbinds

I managed to handle with vararg methods (it's the same what await does).

For compatibility/parity with the other Tweeners, I think

Yeah I totally forgot about finish signal.

elapsed_time

Also forgot about it 🤦‍♂️

I realized the implementation was broken, thanks for the feedback.
I also made the signal connected immediately, because start() can be called multiple times.

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from a12a8a6 to d0622ba Compare July 12, 2024 23:06
@KoBeWi KoBeWi requested a review from a team as a code owner August 29, 2024 14:24
@KoBeWi KoBeWi removed the request for review from a team August 29, 2024 14:27
@KoBeWi
Copy link
Member Author

KoBeWi commented Aug 29, 2024

Tweaked the behavior to match #96216

</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 or when timeout is reached.
Copy link
Contributor

@Mickeon Mickeon Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think? This is assuming it's going to be the behavior.

Suggested change
The [signal Tweener.finished] signal is emitted when either the awaited signal is received or when timeout is reached.
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.

You may have purposely omitted this detail as it may be noted down elsewhere. However, the way this sentence is worded makes it sound like the condition at which finished is emitted has been overridden by this class.

Copy link
Member Author

@KoBeWi KoBeWi Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the last case just now. Previously it would not emit the signal.
I think we could add such note to all Tweeners (in #96216 or after).

Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested but looks good and the idea is good (just needs the method declaration readded)

@jinyangcruise
Copy link

jinyangcruise commented Feb 21, 2025

Looking forward to this feature. When will this be merged?

@Meorge
Copy link
Contributor

Meorge commented Feb 21, 2025

Unfortunately we're in 4.4's feature freeze right now, so I don't think it'll be in a stable release for quite a while ☹️

@blackears
Copy link

Looks good. One thing, though - is there any way to make this validate the signal based on the parameter list passed along with the signal? For example, for a signal hp_change(value:int), perhaps you want the tween to wait until the passed value is equal to 0. Looking at the code, I didn't see anything like that, but I might have missed it.

@KoBeWi
Copy link
Member Author

KoBeWi commented Mar 11, 2025

No, the tweener discards all arguments, just like await.

@KoBeWi KoBeWi force-pushed the the_waiting_game branch from 1bfa3dd to e57ccc2 Compare March 11, 2025 21:42
@Meorge
Copy link
Contributor

Meorge commented Mar 11, 2025

Looks good. One thing, though - is there any way to make this validate the signal based on the parameter list passed along with the signal? For example, for a signal hp_change(value:int), perhaps you want the tween to wait until the passed value is equal to 0. Looking at the code, I didn't see anything like that, but I might have missed it.

Perhaps a workaround for something like this would be to connect hp_change to a function which checks if value is 0, and if so, emits another signal like hp_is_zero - then you could use tween_await(hp_is_zero).

var something: ThingThatHasHpChangeSignal = ...

signal hp_is_zero

func _ready():
    something.hp_change.connect(_check_if_hp_zero)
    
func _check_if_hp_zero(value: int):
    if value == 0: hp_is_zero.emit()
    
func do_tween():
    var tw := create_tween()
    tw.tween_await(hp_is_zero)
    tw.tween_property(self, "scale", Vector2.ZERO, 1.0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add tweener that waits for a signal to be emitted
8 participants