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

Overhaul the Tween system #514

Closed
KoBeWi opened this issue Feb 23, 2020 · 11 comments · Fixed by godotengine/godot#41794
Closed

Overhaul the Tween system #514

KoBeWi opened this issue Feb 23, 2020 · 11 comments · Fixed by godotengine/godot#41794
Milestone

Comments

@KoBeWi
Copy link
Member

KoBeWi commented Feb 23, 2020

Describe the project you are working on:
A very big game™, although it's loosely relevant.

Describe the problem or limitation you are having in your project:
Godot's tweens have problems. You need to add them to scene tree, then interpolate desired properties using extremely long method calls and then you need to remember to start them. This is maybe acceptable, but real fun starts when you want to chain multiple tweens. You end up with a yield mess like this

$Tween.interpolate_property($Sprite, "position", Vector2(300, 100), Vector2(100, 100), 3, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
$Tween.start()
yield($Tween, "tween_all_completed")
yield(get_tree().create_timer(1),"timeout")
$Tween.interpolate_callback($Sprite, 0, "set_texture", load("res://icon2.png"))
$Tween.start()
yield($Tween, "tween_all_completed")
$Tween.interpolate_property($Sprite, "position", Vector2(300, 100), Vector2(100, 100), 3, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
$Tween.start()

Absolutely horrible. Lots of lines, long lines. All to get this effect

Godot's tweens weren't designed with chaining in mind and from my experience you want to chain them more than not.

Describe the feature / enhancement and how it helps to overcome the problem or limitation:

My inspiration for this proposal was DOTween. It's a very productive implementation of tweens that make you want create animations from code instead of e.g. AnimationPlayer (if it was in Godot).

The aim is to reduce code amount required to make an animation. The most basic thing would be making chaining easier. The Tween commands could just run sequentially instead of simultaneously like now and the parallel execution could be done explicitly. Also requiring to set transition and ease type each time is counter-productive, we should have defaults (probably linear + in/out). Also Tweens could start automatically, e.g. by connecting to SceneTree's idle frame signal.

Another thing inspired by DOTeen are Tweeners. Imagine if interpolate_property returned an object that could be used to tweak the command it's created for, so instead of

$Tween.interpolate_property($Sprite, "position", Vector2(300, 100), Vector2(100, 100), 3, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)

we could do

$Tween.interpolate_property($Sprite, "position", Vector2(100, 100), 3).set_trans(Tween.TRANS_CUBIC)

We omit the initial argument and by default it uses current value. Also we omit ease/trans and set them only when non-defaults are needed. I'll give more examples later, but the last thing that could be changed is making Tweens Reference instead of Node. They don't have associated editor and don't have interesting inspector properties, so there's no need to keep them in tree. They could just be created on the fly and removed automatically when not needed, similar to SceneTreeTimers.

I actually implemented a system like this in GDScript and the above code changes to

var seq := TweenSequence.new(get_tree()).set_loops(1)
seq.append($Sprite, "position", Vector2(100, 100), 3).set_trans(Tween.TRANS_CUBIC).from_current()
seq.append_interval(1)
seq.append_callback($Sprite, "set_texture", [load("res://icon2.png")])

It's much easier to create both simple and complex animations and you write only what you need.

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
Literally this: godot-extended-libraries/godot-next#50
but in C++. The Tweens could get a total overhaul that functionally make them work like my custom class (which is basically GDScript DOTween).

So here's a breakdown of my code.

First, creating Tweens would be dynamic: var tween = Tween.new()
When you have a Tween, you can adjust it's properties. In my case you can do

tween.set_loops(int) #sets the number of times a whole animation is executed
tween.set_speed(float) #speed of Tween
tween.set_autostart(bool) #whether the Tween should start automatically, true by default

Then, to add operations to Tween, you can do

tween.append(object, property, target_value, duration) #equivalent of interpolate property
tween.append_advance(object, property, advance, duration) #same as above, but the target value is start value + advance
tween.append_interval(time) #waits given time
tween.append_callback(object, method, args) #current interpolate_callback
tween.append_method(object, method, from, to, duration) #current interpolate_method

These operations should return Tweener objects (e.g. PropertyTweener etc.) that can be used to tweak the calls further. Calling anything on the Tweener returns the Tweener again, for easy chaining of multiple settings.

For property it would be

tween.append(...).from(value) #overrides the default starting value
tween.append(...).from_current() #overrides the default starting value with the current value (normally it fetches it at the beginning of given command)
property_tweener.set_ease(ease) #sets the ease
property_tweener.set_trans(trans) #sets the transition type
property_tweener.set_delay(time) #like the optional last argument of interpolate_property

Example chained usage:

tween.append(...).set_ease(Tween.EASE_IN).set_delay(2)

For callback, you can set the delay and for method you can set ease/trans/delay.

Finally, to allow parallel execution, you can do:

tween.append(...)
tween.parallel().append(...)

^ these two properties would be tweened simultanously.

Alternatively, tween.join() could be a version of parallel() that throws an error when the tween doesn't have preceding commands.

That sums up the functionality I think (at least from my class). I see that current Tween have more methods, but they could be adopted to the new system too. Also we could have some way to get a list of all running tweens, tweak more properties like per-tween or global default transition/ease type etc. My proposal is more like base for the new Tweens.

If this enhancement will not be used often, can it be worked around with a few lines of script?:
IMO it would be used often. More often that current Tweens, because the new system would be easier.

Is there a reason why this should be core and not an add-on in the asset library?:
The current Tweens are meh, so they should be replaced by this.

@nicktgn
Copy link

nicktgn commented Apr 25, 2020

All up for parallel execution! Would love to see that implemented.

@lukostello
Copy link

lukostello commented May 25, 2020

Maybe the array of tweens should be in a singleton? I don't understand how one property of the same object could be tweened from 2 different tween objects from 2 different scenes. Having them all in one place seems like a good way to ensure you arn't accidentally overlapping tweens and keeps them nice and organized.

P.S. I'm not sure you need append.advance I think doing destination - start isn't so difficult that we need to make another function to simulate it.

@KoBeWi
Copy link
Member Author

KoBeWi commented May 25, 2020

P.S. I'm not sure you need append.advance I think doing destination - start isn't so difficult that we need to make another function to simulate it.

It's useful for relative movement, i.e. when you don't necessarily know the start and destination points. This way you can tween jumping movements which don't depend on starting height etc.

@lukostello
Copy link

but if the default if the default starting point is the current one then you've already found the start. Maybe you could have a start() method that returns it then the destination would be start() + advance

@KoBeWi
Copy link
Member Author

KoBeWi commented May 25, 2020

Here's an example from my project:


	var i := randi()  % 3
	for j in 3:
		match (i + j) % 3:
			0:
				seq.append_advance(self, "position:x", 15, 0.5).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE)
				seq.parallel().append_advance(self, "position:y", -15, 0.5)
			1:
				seq.append_advance(self, "position:x", -30, 1).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
				seq.parallel().append_advance(self, "position:y", -30, 1)
			2:
				seq.append_advance(self, "position:x", 15, 0.5).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_SINE)
				seq.parallel().append_advance(self, "position:y", -15, 0.5)

It defines the bubble movement as seen here:
ezgif-3-c937b30d9e97

It moves the bubble up in a sinusoidal pattern. If I wanted to do it on absolute values, I'd have to either use two tweens (one for vertical movement and one for horizontal) or calculate the vertical position after each sine period. Relative values make this much more simple to define.

EDIT:
Also append_advance adds literally 9 lines of code. It's very little for how much convenience it adds (and compared to whole tween code).

@lukostello
Copy link

Here's an idea: do the same thing for animations. You would have play() only require 1 argument (the name of the animation) then once you've created it you can specify custom blend, play speed (I don't see the need for having a from end boolean, can't we just assume that is the case for any negative value?), delay, and perhaps even easetype such that you could interpolate through the animation as you would a tween.

@Exoow
Copy link

Exoow commented Jun 11, 2020

So, would the following still work then, if you create a temporary Tween every time?

I have a counter that flashes whenever it's being increased (tween that interpolates both scale from 1.5 to 1, and modulates color).
However, if the counter increases rapidly, the first interpolation is still going (say, scale is halfway at 1.25) but it's overruled by the second interpolation (scale set back to 1.5 to interpolate). So you get a nice 'hit' effect when the value changes.

@KoBeWi
Copy link
Member Author

KoBeWi commented Jun 11, 2020

Theoretically, when you interpolate the same thing with 2 Tweens, the one added later takes precedence (AnimationPlayers work this way for example). Or you can just store reference to the Tween and remove/restart it manually.

@MarcusElg
Copy link

This would be an improvement. +1 support!

@javidcf
Copy link

javidcf commented Aug 17, 2020

This looks very good. I was just thinking if this could benefit from a sort of "chainable" API, which I think could make it very readable. Unfortunately, for this to be really nice we would need to have godotengine/godot#35415 fixed, otherwise you need to put backslashes at the end of every line and would not be able to use comments, but the idea would be something like this:

var tween = (TweenBuilder.new()
	.begin()  # begin() gives you an object with the API to add a tween
		.interpolate(object, property, target_value, duration)
                # Tween methods give you an object with the API to set properties
		.with_ease(ease)
		.with_trans(trans)
	.then()  # .then() is sequential concatenation
		.advance(object, property, advance, duration)
		.with_delay(delay)
	.and_()  # and_() is parallel execution wrt latest .then() or .begin()
		.interpolate_method(object, method, val1, val2, duration)
	# You can make "sub-tweeners" (or whatever you want to call them)
	# to have a sequence of tweens to execute in parallel
	.and_()
		.begin()  # Begin "sub-tweener"
			.interpolate(...)
			.with_this_property(...)
		.then()
			.follow_method(...)
			.with_that_property(...)
		.done()  # End of "sub-tweener"
	# Could have a parameter in .then() or and_() to add delay
	# (in theory even negative delays could be made to work I think?)
	.then(2.0)
		.targeting_property(...)
	.done()  # Finish chain
)
# Set general tween properties as usual
tween.set_loops(...)
tween.set_speed(...)
tween.set_autostart(...)

@Calinou Calinou changed the title Tween overhaul Overhaul the Tween system Sep 7, 2020
@altered-dev
Copy link

One more suggestion could be supporting custom easing functions (which must return a float from 0 to 1), but yet I can't think of any actual use; maybe when you don't like both of the default functions.

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

Successfully merging a pull request may close this issue.

8 participants