-
-
Notifications
You must be signed in to change notification settings - Fork 21.1k
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
Complete rewrite of Tweens #41794
Complete rewrite of Tweens #41794
Conversation
Nice to see it works out, see my previous comments revolving around this kind of interface in #26529 (comment) supporting this on the high-level. |
@Xrayez If you mean autostarting and handling pause, then it's already planned.
|
Maybe something like this: var tween := get_tree().create_tween()
tween.tween_property($icon, "position", Vector2(340, 100), 4)
tween.group_begin()
tween.tween_property($icon, "modulate", Color.red, 1)
tween.tween_property($icon, "scale", Vector2(2, 2), 1)
tween.group_end() |
Nah, it will be simpler. It's not mentioned in the OP, but the Tweens will make heavy use of chaining (every Tween/Tweener method returns that Tween/Tweener to allow multiple calls). That said, your code will look like this:
Parallel Tweening is going to be achieved with |
We must try to make this not only simple, but also as versatile as possible. How to get the following using your way: I had a thought about
But this is too GDScript specific. |
By using two Tweens. You'd need to add CallbackTweener 5b with a delay equal to duration of 5, which would create a new Tween that does 6. Tweens are cheap in the new implementation, so you don't have to worry about creating many of them. Allowing asynchronous steps would make the implementation much more complicated. Although maybe I could add at least a |
I got it. Something like this:
That is, you can always do without |
Ok, I completed the features. Check the added section in the OP. |
|
While I agree tween should be a reference, I think it should be something created in Node, not in SceneTree (even though it is processed in SceneTree), so you simply use it like
I would do the same change for timers |
Hi I just saw your rewrite on Twitter and I very much agree with the 'fire and forget' idea of tweens and many of the other changes and improvements you have made. In the past I found myself always creating tween objects on demand and never really figured out how to properly restart or reuse them. Creating tweensIs there a specific reason that the Also it fits a bit better into the composition philosophy of Godot when the tween is created on the current Auto start tweensIf I understand correctly tweens automatically start on the next frame after their creation? And if you want to delay the start you would add a delay with Parallel tweensJust creating multiple tweens to run at the same time will probably enough in most cases but you are right that there needs to be some sort of
This should run tween A, then B and C parallel and after that tween D. This underlines the idea that calling the I think the names of the |
I don't like it accessed via SceneTree, it makes the tween access a bit dirty, while it can use a Node like it is currently and expose configuration via inspector which would be great for many workflows and more godot-like. |
Usually you would combine tweens with one or multiple AnimationPlayer to get most of the functionality in this pull request. I think it is overdoing it a little, trying to do too much with tweens alone. Edit: the relative movement and the fixed amount of loops is something I would like to see in the AnimationPlayer instead. |
The idea seems solid and very much an improvement, removing it from being a node and only accessible through the SceneTree just seems a bit....off. It's probably just me. I certainly like occasionally using "get_tree().create_timer" in an in-line way for yields but I will often pull that back into a node composed within a scene so it's a bit cleaner and self-contained. I know it's handled now, so that in case a yield returns to a now dead node/scene it wont crash but this would have to be handled as well with tweens. |
Maybe it's just me, but I feel like the default behaviour of a tween should be to process in parallel, which is how it works currently. The join() function doesn't really make sense in terms of doing things in parallel. I'd probably do something like: parallel: sequence: (not sure if tween.start() is still a thing in this PR?) Not sure if 'chain' is the best word (maybe append?), but I think something like that would better describe the idea of creating/adding to a sequence rather than 'join' |
I actually had this idea already. Seeing how some people requested it I'll add it as an alternative way of creating Tweens.
If you don't want the Tween to start immediately, you can call
This can be already achieved with
When you use
In my prototype they were called
Long before the PR I made a prototype in GDScipt and have been using it in my project for few months. Tweens made this way are fun to abuse for lots of different things. I never needed to combine them with AnimationPlayer, this is only useful when you want to visually design some animation.
This is irrelevant. You don't need to use yield/away with Tweens, you just tell them to animate something and they animate it. If the animated object is removed midway, the Tween will automatically stop and get deleted.
Dunno, Tween chaining was requested for ages. Personally I more often tween something sequentially than parallely. Maybe I could add a method that makes paralleling default? 🤔 |
The main problem here is behaviour, not naming. I think the most common use case will be to make tween execute exactly For current behaviour: yes, |
Totally agree. Iterating n + 1 times is just confusing, makes it feel like a do loop. I feel like most poeple exspect it to iterate n times and will have to go back and change their code. |
I definitely support that chaining should be the default instead of parallel running but a good compromise would be to have two seperate functions to create the tween object: one creates a chaining one and the other creates one that runs all tweens at the same time.
Fair enough then. If the documentation covers this all is well.
I don't know but something about doing it this way feels odd to me, maybe the empty |
After some feedback, I added a
This actually gives lots of possibilities for creating custom Tween-convenience methods. But creating Tweeners manually is not exposed to GDScript (right now). Consequently, From other changes, I added a pause mode called TWEEN_PAUSE_BOUND, which makes the Tween pause when the bound node is paused (without bound node it works like TWEEN_PAUSE_STOP). I haven't touched I yet have to update the OP. |
The ability to use custom curves for both the ease and trans would be greatly appreciated |
It's great to have nodes ability to create local tweens, very similar API to my JavaScript port of Godot ;) I did the same thing for performance, but have made |
Not sure if I am a fan of this. If I understand this correctly I would have to use create_tween().append_property(...) to tween a property. My problem with this is that "append property" doesn't make it obvious for people who haven't read the docs that the appended property is being tweened. (Is tweened a word?) In contrast I immediately understood your create_tween().tween_properties(...) examples even without reading your explanation. I am also not a huge fan of parallel() and join(). Based on my understanding of the earlier comments the two of them both run the next command in the chain concurrently with the previous command(s). (Edit: minor spelling correction, I typing am on my phone) |
I also vote for custom curve support, but we may not need Godot's built in |
Is it possible to find existing tweens bound to a particular node? If I understand right, the only method right now is iterating through all the results of |
It's not possible right now. Also If you think Tweens should support that normally, open a proposal. |
This Tweens implementation is much better than the 3.x. |
@alexzheng There is a very similar implementation in GDScript that you can use in 3.x: https://github.com/godot-extended-libraries/godot-next/tree/master/addons/godot-next/references/tween_sequence |
This new tween may not required to be compatible with the Tween Node, It would be nice if it could be available as another option in 3.x, just keep the Tween Node as it was, personally, I really do not like the Tween node. |
@KoBeWi var tween := get_tree().create_tween() |
@alexzheng Open an issue about it. |
Hello @KoBeWi I don't know if it is a bug or not so I am posting this comment. I tried to queue_free() my node when there was tween in the scene looping. When I tried to do it whole game just crashed without errors or anything. Solution was to kill tween before freeing node. Maybe there is some way to auto free tween when someone free node? Ps: Seems like creating tween under this node and not by get_tree() is also solution. When I did it I didnt need to call kill() anymore. So probably fault on my side. I will delete this comment soon. Leaving for now. Maybe in fact there is bug or not. |
@seppoday The Tween should auto-kill when the object gets deleted, so it sounds like a bug. Open a new issue with some minimal project |
i guess it's a bug, because when i use a tween in a button for a menu, then close and free that menu i get this warning :
|
This is expected, if you free an object before animation starts, you get this warning. Although maybe it's not useful, idk 🤔 |
But the animation is already started and finished, but for some reasons i still get this warning
This code i used it in a button when it's pressed for a Pause menu to animate the button, but if i change the level or go to the main menu or close the game i get the warning |
I tried your code and I don't get this warning. Can you share some minimal project? Also you could open a new issue. |
Just a bit of clarification (that maybe could be added somewhere?): What about tweens that are used over and over again, when the node doesn't get removed? I tested this, and the object count in the debugger continues to shoot up. Do those eventually get removed, or does the node need to be free'd? |
All Tweens are removed automatically when not used anymore, but there is a known issue when it doesn't work: #52699 |
Tween changes so much that the documentation is not yet sufficient to cover all use cases. I found a problem when using it that tween cannot be created outside the function, if this is not a bug, then it should be noted. var tween
func _ready() -> void:
tween = create_tween()
func transition()->void:
tween.tween_property($Sprite, "modulate", Color.red, 1) |
Can confirm I had strange issues when using a tween created outside a function. The error wasn't exactly clear of what was going wrong, either |
The documentation has countless examples where Tween animation is defined right after its creation, discourages re-use and tells you that you should use But maybe the error should tell that too. |
I am using tweens extensively in code. The key lines in the documentation are these:
That means you created an empty tween in ready, the tween begins that frame and continues looping indefinitely, then later on a subsequent frame, you're adding additional properties to change. The docs don't say what is supposed to happen when you add tweens while its running. Create it and set it up in the same frame you want it to run, or stop it upon creation for set up later as KoBeWi wrote. |
How are Tweens supposed to be created when continuously tweening something in _process()? Or is that a bad use case for them? |
Generally, new devs should not create tweens in process. You don't want to create tweens 300 times per second. However, you can do it if you are manually checking the frequency and ensuring you're only creating them one every X seconds where X is less than or equal to your tween time. |
EDIT: Scroll a bit further for interesting stuff.
This is meant to close godotengine/godot-proposals#514
Note that THIS IS HEAVILY WIP. The implementation is incredibly hacked together for now, full of dead and commented out code. Also it's mostly unfinished. I wanted to open the PR early to make sure that this is going in the right direction.In its current state you can do this:
which will result in
So onto details:
Tweens are no longer nodes. They are References, with very similar logic to SceneTreeTimers, designed in a "fire and forget" manner. You create a Tween with
get_tree().create_tween()
, which will result an empty Tween. To do something with it, you then call one of the tweening methods. These are:tween_property
- tweens a property between given values (equivalent of current interpolate property)tween_interval
- waits for given time (equivalent of a timer)tween_callback
- calls a method on an object (equivalent of current interpolate callback)tween_method
- calls a method on an object with a tweening argument that goes from initial to final value (equivalent of current interpolate method)You can call as many tweening methods on the Tween as you want, they will be sequenced one after another, i.e. there's support for chaining out of the box. Parallel tweening will be done explicitly with a dedicated method.
As for the implementation, there two main classes: Tween and Tweener. Tween is basically a collection of Tweeners. When you call tweening method, it actually creates a Tweener. Tweeners are packed in a List. Tween will go through the list and process every Tweener and when it finishes, it goes onto another step in the sequence. The Lists of Tweeners for each step are contained in a Vector.
Internally, SceneTree calls
_step()
on all active Tweens. Each Tween then calls_step()
on each current Tweener._step()
has a return value. When a Tweener returnsfalse
, it's considered finished (but doesn't get freed, in case you want to loop the Tween). If Tween returnsfalse
, it's removed from the list of tweens in SceneTree (and freed if you don't keep a reference). Tweener is also removed when the targeted object gets removed (whether this should happen silently or needs to be handled manually by user is up to discussion).These are the basics. The new Tweens are going to work mostly like in the proposal (see the TweenSequence class linked in there). I yeeted the old Tween code, save for the interpolation (the
_interpolate
method in PropertyTweener, which is going to get cleaned up). The_calculate_interpolation
method defined ineasing_equations.cpp
will stay there and since I need it in another class, there's a chance to expose it (I remember there was a request for it).The thing that I'm the most unsure of is whether Tweens should be handled directly by SceneTree. But I don't have other idea how they could be handled, aside from maybe TweenServer.
EDIT:
Ok, the feature implementation is complete. What's left is error handling and documentation.
Here's a brief summary of the features:
(scroll below for actual usage examples)
Tween
get_tree().create_tween()
orcreate_tween()
inside Node (the latter will automatically bind the Tween, see below)get_tree().get_processed_tweens()
to get the list of all existing Tweensbind_node(node)
. The Tween will not process if the node is not inside tree and will be automatically removed when the node is freedset_loops(loops)
and 0 will make infinite looptween.parallel()
Tween.set_parallel(true)
will make all subsequent Tweeners parallelTween.set_ease()
orTween.set_trans()
to set default easing and transition for a Tweentween.interpolate_value(initial_value, delta_value, time, duration, transition, easing)
which allows you to manually interpolate a value with easing (supersedes AddTween.interpolate()
as a low-level tweening option #36997)There are 4 available Tweeners:
PropertyTweener
tween.tween_property(object, property, final_value, duration)
from()
to set a custom starting valuefrom_current()
to assign it the value it has currently (i.e. when creating the Tween)as_relative()
to make the final value be a relative value (so e.g. tweening position to Vector(100, 0) relatively will move it by 100 pixels instead of moving it to position (100, 0))set_ease(ease)
orset_trans(trans)
set_delay(delay)
IntervalTweener
tween.tween_interval(duration)
CallbackTweener
tween.tween_callback(callback)
, wherecallback
is a CallableCallable.bind(args)
set_delay(delay)
to have a delay before the callbackMethodTweener
tween.tween_method(callback, from, to, duration)
Callable.bind(args)
Ok, so these are features. Now on the actual usage, it basically boils down to:
which will execute the sequence of 2 given tweening operations.
Some samples:
Same as above but without bind
Lambda version of the above:
If you have any questions or feature requests be sure to write them in the comments.