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

Bring Tween Node back and Merge it with current Tween #7892

Open
henriiquecampos opened this issue Sep 27, 2023 · 94 comments
Open

Bring Tween Node back and Merge it with current Tween #7892

henriiquecampos opened this issue Sep 27, 2023 · 94 comments

Comments

@henriiquecampos
Copy link

henriiquecampos commented Sep 27, 2023

Describe the project you are working on

We want the Tween Node back flyer

I'm working on a Multiplayer Online Adventure game implementing interpolation techniques for lag compensation.

Describe the problem or limitation you are having in your project

The current Tween is way more intricate compared to the previous approach and brings up unnecessary steps to achieve things previously trivial, such as accessing and stopping the Tween. For instance, this doesn't work as it doesn't create a tweener for some reason:

@onready var tween = create_tween()

func interpolate_position(target_position, duration_in_seconds):
    var interpolation = lerp(previous_position, target_position, 1.0)

    tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
    var tweener = tween.tween_property(spaceship, "position", interpolation, duration_in_seconds)
    tweener.from(previous_position)
    previous_position = target_position

But this works:

func interpolate_position(target_position, duration_in_seconds):
    var tween = create_tween()

    var interpolation = lerp(previous_position, target_position, 1.0)

    tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
    var tweener = tween.tween_property(spaceship, "position", interpolation, duration_in_seconds)
    tweener.from(previous_position)
    previous_position = target_position

And since the tween reference "only exists" while the function runs, I can't do this:

func synchronize_position(new_position):
    tween.stop()
    spaceship.position = new_position
    previous_position = new_position

Instead I need to do this:

func synchronize_position(new_position):
    for tween in get_tree().get_processed_tweens():
        tween.stop()
    spaceship.position = new_position
    previous_position = new_position

Which stops other tweens not related to this behavior, so I need to add some filtering to find the actual reference to the tween I want.

On top of that, this new approach breaks the engine's design of providing NODES as building blocks for creating games with Godot Engine, so a Tween node would keep the design consistent, as it was in 3.x.

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

With a Tween node I could just do this instead:

@onready var tween = $Tween

func interpolate_position(target_position, duration_in_seconds):	
    var interpolation = lerp(previous_position, target_position, 1.0)

    tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
    var tweener = tween.tween_property(spaceship, "position", interpolation, duration_in_seconds)
    tweener.from(previous_position)
    previous_position = target_position


func synchronize_position(new_position):
    tween.stop()
    spaceship.position = new_position
    previous_position = new_position

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The idea is to turn the current Tween into a node that would expose some properties in the Inspector, making it easier for us to configure some settings, such as the process mode, duration, ease, transition, etc...

If this enhancement will not be used often, can it be worked around with a few lines of script?

Tweens are fundamental for game development, so this would be used often. Especially because now we have access to Tweeners these two classes would make things very versatile and would be used often.

The workaround for the current approach is to create a whole Node that performs exactly what the previous Tween node did in version 3.x, but is now implemented manually which can cause lots of issues since not every user, me included, knows the proper approach to implement this manually. You guys are the best folks to implement a feature...that you guys already implemented previously.

Is there a reason why this should be core and not an add-on in the asset library?

Yeah, it's part of the engine's core design to use Nodes, not intricate functions as if we were developing in FP. On top of that this feature was in the previous versions of Godot Engine, so technically this is a regression of the core engine's code.

Edit by the production team: added syntax highlighting to code samples and changed tabs to spaces

@henriiquecampos
Copy link
Author

By the way...maybe just port this to core?

https://github.com/akien-mga/threen

@Calinou
Copy link
Member

Calinou commented Sep 27, 2023

I don't think we should have two ways to achieve the same thing in core. This is what add-ons are suited for 🙂

@henriiquecampos
Copy link
Author

henriiquecampos commented Sep 27, 2023

@Calinou

I don't think we should have two ways to achieve the same thing in core. This is what add-ons are suited for 🙂

Isn't that what we have with Timer node and get_tree().create_timer()?

I mean, there are plenty of situations where you can achieve the same thing in multiple, not just two, different ways. I could cite at least five out of my head:

  • Loading with ResourceLoader vs load() keyword
  • Drawing textures with draw_texture_rect() vs TextureRect/Sprite
  • Displaying text with draw_string() vs Label
  • Drawing polygons with draw_polygon() vs Polygon2D
  • Drawing line with draw_line() vs Line2D
  • Accessing nodes with get_node(NodePath) vs $NodePath...

And we can keep going, but the thing is: we used to rather use Nodes than functions. This was the whole Godot Engine design.

But the thing is: in this case specifically we can just merge the two into a Node, we don´t need to keep the current Tween RefCount after porting it as a Node

@ywmaa
Copy link

ywmaa commented Sep 27, 2023

Ok, I am only using tween using create_tween().

but the idea of this proposal I think is valid, due to how Timer Node works.
it is better to keep consistent with other nodes in the engine.

and I think this will satisfy both who use create_tween() and Node approach.

@zinnschlag
Copy link

zinnschlag commented Sep 27, 2023

@henriiquecampos Your first code sample almost certainly fails because the variable declaration is missing a @onready.

And your code with the proposed node will not work, because it is also missing a @onready. So there is literally no difference between these two versions.

Edit: I hope there is no user on GitHub with the name onready

@henriiquecampos
Copy link
Author

@henriiquecampos Your first code sample almost certainly fails because the variable declaration is missing a @onready.

And your code with the proposed node will not work, because it is also missing a @onready. So there is literally no difference between these two versions.

@zinnschlag

Edit: I hope there is no user on GitHub with the name onready

I tried to using onready as well, same issue.

@Calinou
Copy link
Member

Calinou commented Sep 27, 2023

Edit: I hope there is no user on GitHub with the name onready

There is one, otherwise the bold clickable link wouldn't appear 🙂

@zinnschlag
Copy link

@henriiquecampos That is odd. But it makes no difference. You can still use the original version. Declare the variable globally but don't initialise it (or initialise it to null). Then assign the tween to it in the function like in your second version. Still doesn't justify adding another node type IMO.

onready must be really annoyed with Godot by now. Sorry!

@henriiquecampos
Copy link
Author

henriiquecampos commented Sep 27, 2023

@henriiquecampos That is odd. But it makes no difference. You can still use the original version. Declare the variable globally but don't initialise it (or initialise it to null). Then assign the tween to it in the function like in your second version. Still doesn't justify adding another node type IMO.

onready must be really annoyed with Godot by now. Sorry!

Ohh man...I definitely wouldn't be wasting my time if things were working as they used to, look:
https://twitter.com/pigdev/status/1707099594796982731

Note the tween is an object in the reference as seen in the debugger. It just don't process, so the Tweener that it should return from the tween_property() returns null.

Please understand, if it was working nice and smoothly as the Tween node used too, I wouldn't be as frustrated to the point of opening this issue.

@byyiro
Copy link

byyiro commented Sep 27, 2023

I remember that the new Terrains system was considered a bit of a Godot anti-pattern just because it having the layers as inspector variables instead of nodes. I don't understand why we should ignore the nodes system this time.
Anyways, as people are already using the new way, I think we should keep both!

@francoisdlt
Copy link

francoisdlt commented Sep 27, 2023

@henriiquecampos That is odd. But it makes no difference. You can still use the original version. Declare the variable globally but don't initialise it (or initialise it to null). Then assign the tween to it in the function like in your second version. Still doesn't justify adding another node type IMO.

onready must be really annoyed with Godot by now. Sorry!

this is what i've been doing succesfully on my project (keeping references to tweens once they're started, and then stopping them if they're valid and running), and it works fine. It's a little weird indeed compared to the old behavior, but you get accustomed to it (not all tweened animations require to be stopped).

So i'm voting against this proposal.

@henriiquecampos
Copy link
Author

henriiquecampos commented Sep 27, 2023

@francoisdlt would the implementation of this proposal prevent you from keep doing this approach?

@Kakiroi
Copy link

Kakiroi commented Sep 27, 2023

I think having "TweenPlayer" node could be beneficial. A node for managing multiple tween refs for nodes to interact. With this way, both current refcounted Tween and node version of Tween manager can have reason to exist with eachother.

@henriiquecampos
Copy link
Author

Something I just thought about is that to prevent "breaking compatibility" the create_tween() could return a Tween node that was already added to the SceneTree(just like the current one is already added as well) without the need of using add_child()

So.. I think the whole interface of the current class could be ported to a Node. I mean, I'm struggling to understand the resistance to make this switch(not only here but the twitter's threads as well)

@zinnschlag
Copy link

Having multiple ways to do the same thing leads to bloat. It increases the code size and thus maintenance costs. It increases the binary size. It increases the documentation size. It increases the cognitive load on developers who have to choose between multiple APIs. Ideally there should be one way to do it and only one way.

@henriiquecampos
Copy link
Author

henriiquecampos commented Sep 27, 2023

@zinnschlag

Having multiple ways to do the same thing leads to bloat. It increases the code size and thus maintenance costs. It increases the binary size. It increases the documentation size. It increases the cognitive load on developers who have to choose between multiple APIs. Ideally there should be one way to do it and only one way.

As shown in this reply:
#7892 (comment)

I don't think this is a valid reason to not have this proposal implemented, but on top of that, I'm not proposing to have two ways to do the same thing, the title is clear: merge the current Tween into a Tween node. The proposal suggests to have only the Tween node. But if this is not possible, supporting the two approaches shouldn't be such a hardache because, again as shown in the reply, "having two ways to do the same thing" is just the default Godot approach, especially turning one of these ways into a Node-based approach, as suggested in this proposal.

@narredev
Copy link

While I do agree that we need a more reusable way for Tweens, and that the TweenNode accomplished that very well, it's not impossible to replicate it's behavior with the current system.

The docs clearly say that tweens start working as soon as you create them, so doing @onready var tween = create_tween() will make a tween that's "running" with no tweeners.

What I do to replicate the TweenNode behavior is also in the docs, keeping the tween variable and checking if it's empty, then if I want it to reset I use stop, if not, just kill. But you have to always kill the Tween before doing a new one:

var tween: Tween

func do_anim() -> void:
    if tween: tween.stop(); tween.kill()
    tween = create_tween()
    #do animation here

This has more or less always worked for me and it's basically the same as keeping a reference to a TweenNode, I don't get the need to do for loops on the tree to search for all the processed Tweens.

@henriiquecampos
Copy link
Author

@narredev

While I do agree that we need a more reusable way for Tweens, and that the TweenNode accomplished that very well, it's not impossible to replicate it's behavior with the current system.

The docs clearly say that tweens start working as soon as you create them, so doing @onready var tween = create_tween() will make a tween that's "running" with no tweeners.

What I do to replicate the TweenNode behavior is also in the docs, keeping the tween variable and checking if it's empty, then if I want it to reset I use stop, if not, just kill. But you have to always kill the Tween before doing a new one:

var tween: Tween

func do_anim() -> void:
    if tween: tween.stop(); tween.kill()
    tween = create_tween()
    #do animation here

This has more or less always worked for me and it's basically the same as keeping a reference to a TweenNode, I don't get the need to do for loops on the tree to search for all the processed Tweens.

I do understand that there are "work arounds" for this issue. But take a moment and assess: that: is this intuitive? Is this as intuitive as having a node(remember the Godot's design of using nodes as building blocks to create games with Godot).

But on top of that, look at this logic:

"I need to interpolate a property and at some point I can receive a new target value, so I need to stop the current interpolation and update it to aim towards the new target value. For that I need to: create a null reference for a variable type of tween, create a custom function that will stop the tween, even if it didn't even start to begin with, just to maintain a reference to it so I can use it later on, then I can actually play the interpolation but I have to ensure that it NEVER ends, because if it does, I will lose reference to the tween I was using to interpolate the previous value, and if at any point I need to interpolate another property I have to ensure that I stop the tween, even tho I may want to have the previous interpolation going"

Previously the line of thought was: I need to interpolate something, there's this node that is specialized in interpolations, I'll add one as a child and use it, when I receive a new value to interpolate to, I can stop the current track and interpolate to it instead.

Please correct me if I'm wrong, but just because we have workarounds to achieve the same outcome, it doesn't mean we can't improve things, right?

I mean, we can create games with assembly, but we use an engine because it provides easier and higher level approaches to create these games without us having to re-invent the wheel. Besides this code you've shown works, and it probably has been working for you, imagine if instead of having a Sprite node you had to do something like:

extends CanvasItem

@export var texture
@export var position
@export var rotation
@export var scale


func _draw():
    draw_texture(texture, position)

Imagine not having the Sprite node, or any of the nodes mentioned in #7892 (comment)

Imagine not having the SceneTree and having to implement an engine processing loop by hand. Imagine not having a user interface like the Godot Editor and having to do everything via code without visual feedback. Imagine not having a user interface and having to create animations via code.

Do you understand this proposal is not a cry saying "it's impossible to do tweens in Godot Engine 4.0"? But instead it's a proposal to improve the way we do it because as it is it's not as intuitive as it was or as it can potentially be?

@narredev
Copy link

narredev commented Sep 27, 2023

@henriiquecampos It's not a "work around". It's the intended use. It's in the official documentation:

If you want to interrupt and restart an animation, consider assigning the Tween to a variable:

var tween
func animate():
    if tween:
        tween.kill() # Abort the previous animation.
    tween = create_tween()

This is how it would look with your functions:

var tween_pos: Tween

func interpolate_position(target_position, duration_in_seconds):
    if tween_pos: tween_pos.kill()    
    tween_pos = create_tween()

    var interpolation = lerp(previous_position, target_position, 1.0)

    tween_pos.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
    var tweener = tween_pos.tween_property(spaceship, "position", interpolation, duration_in_seconds)
    tweener.from(previous_position)
    previous_position = target_position

func synchronize_position(new_position):
    if tween_pos: tween_pos.kill(); tween_pos = null
    spaceship.position = new_position
    previous_position = new_position

It's not much different from what we did with the TweenNode. You also had to store a reference and check if it was running or active and stop it or pause it. I don't see this argument as a need for adding a TweenNode.

You use the Sprite node as an example that doesn't require code to work, and that makes it practical and in line with Godot, but the Godot 3 Tween node is kinda opposite to that, it does need a script to be used, just like the current Tween

@narredev
Copy link

narredev commented Sep 27, 2023

@henriiquecampos I would like to have a TweenNode for having more ways of doing the same, as you said here: #7892 (comment)

But the new tweens can still accomplish the same as the old ones. I have many scripts making use of the new tween just like the old ones, and they all work as intended. I just want to make that clear. The documentation has a pretty decent tutorial/explanation on how to use them, with common use cases addressed, it's in the tween class page

I also see how they are a bit more confusing if you're coming from Godot 3 (I was also confused when I first transitioned), but they are more intuitive if you think of them as only one animation each, and more in line with other engines. They are also designed for working quicker, since in Godot 3 they needed to have both a Node and a script, now they only need a script.

@narredev
Copy link

Another argument could be allowing TweenNode to work without the need of a script, which is the purpose of the Timer node when I use it or the other nodes alternatives. This also allows for them to be reused when saving scenes instead of copy pasting code

Something like a simplified animation player for only one thing? Or maybe you would need to write an expression? Then you can add that TweenNode to any scene and make the parent do the animation when a signal fires...

Not really sure how it would work but that could be a very good reason to have Tween nodes

@CarpenterBlue
Copy link

CarpenterBlue commented Sep 27, 2023

@henriiquecampos
@narredev
What about this? You can treat it like a node if you want to thanks to the bind_node() that's automatically called by create_tween() in the real usecase, the TweenNode is declared in it's own script with class_name.
image

@narredev
Copy link

narredev commented Sep 28, 2023

@CarpenterBlue That's not really a solution. You would have to do TweenNode.tween to access it, and it still behaves the same. You would still need to kill it and re create it each time in your script. The original Tween node was made to support multiple animations, what you propose only supports one so you would need to make a new node for each.

To make a proper Godot 3 tween node, it would need to store each created tween operation in some kind of dictionary, and properly manage them. Then you would access them by the object and/or property name to stop them.

Like #7892 (comment) said, a Tween node should serve the purpose of managing a group of tween animations in one place

@YuriSizov
Copy link
Contributor

YuriSizov commented Sep 28, 2023

Just to give you guys a starting point as to why this change was made I think it's worth linking to the original proposal: #514

It explains the new features, but it also explains why the node has been removed (short answer: it doesn't serve any practical purpose because nothing is exposed on it and there is no dedicated editor for it, you still need to write code — now more than ever with the advanced sequencing features).

@henriiquecampos
Copy link
Author

@YuriSizov

Just to give you guys a starting point as to why this change was made I think it's worth linking to the original proposal: #514

It explains the new features, but it also explains why the node has been removed (short answer: it doesn't serve any practical purpose because nothing is exposed on it and there is no dedicated editor for it, you still need to write code — now more than ever with the advanced sequencing features).

I think then the solution would be to exposed some properties like the Timer: process mode, easing, transition type, duration, autostart(maybe implement it?), etc...

Would be way easier than revamp the whole thing and break compatibility, no? Maybe creating a cool UI like MultiplayerSynchronizer, AnimationPlayer, etc...

@YuriSizov
Copy link
Contributor

Would be way easier than revamp the whole thing and break compatibility, no?

I'm not sure why you're drawing a comparison like that. The rework was done to add new functionality to the class, none of which can be exposed as properties. And the properties that you are listing — nobody asked for them, so why would anyone consider exposing them? Besides, exposing them is not an alternative to the features added...

easing, transition type, duration, autostart

The first 3 are unique for each tweener, the last one is not how you normally use tweens. They just have different use cases compared to timers, even if sometimes you can use them similarly.

@henriiquecampos
Copy link
Author

henriiquecampos commented Sep 28, 2023

Would be way easier than revamp the whole thing and break compatibility, no?

I'm not sure why you're drawing a comparison like that. The rework was done to add new functionality to the class, none of which can be exposed as properties. And the properties that you are listing — nobody asked for them, so why would anyone consider exposing them? Besides, exposing them is not an alternative to the features added...

easing, transition type, duration, autostart

The first 3 are unique for each tweener, the last one is not how you normally use tweens. They just have different use cases compared to timers, even if sometimes you can use them similarly.

I'm comparing the two because this is the whole point of this proposal. So you're saying the new functionalities couldn't be implemented in a Node? If nobody asked for the functionalities I pointed why they are in the new approach? If nobody uses nodes in autostart mode why the new class literally autostarts and the whole issue is caused precisely because it starts without anyone deliberately saying so, triggering its removal and losing reference to it?

I really didn't get the whole point of your reply. Please help me see your point.

On top of all that, the new approach makes a pseudo Node already. As mentioned in the documentation, the Tween is added to the scene tree, it has process and physics processing like only nodes have in Godot, and supposedely(as mentioned in the replies above) you should create it in the _ready() callback or using @onready keyword...

Why making it so similar to a node yet not making it a node if making it a node wod keep compatibility and prevent the current issues?

@narredev
Copy link

narredev commented Sep 28, 2023

why the new class literally autostarts and the whole issue is caused precisely because it starts without anyone deliberately saying so

you should create it in the _ready() callback or using @onready keyword...

The new Tweens don't "autostart" nor "start without anyone saying so", and you do not have to create them with the onready keyword. They are not meant to be just a code replacement for the old Tweens, please properly read how the new Tweens work before criticism them. The documentation is quite clear, and the original proposal is also there. What you're complaining about in the quote and other parts of this proposal comes from not understanding how they work and forcibly using them as the old ones. I already explained how to get the same result in my other comments, and it's pretty much the same amount of lines and usability as the old ones.

Think of them as a single animation that can have multiple tracks each, meant to be discarded as soon as it finishes. They are only meant to be created in a function, so you always have the code that made them. They only start when you finish creating them, so you just call the function when you want them to start. You store a reference in a variable if you want to check their progress or cancel, pause, reset them, etc.

To compare the old way vs the new, you have to understand how both work.

Do you want to keep working the old way or to have a new Tween node? Those are two different things
If you want to have a Tween node with the new stuff, it will be different than the Godot 3 one, and you will run into the same problems

@mikhaelmartin
Copy link

@henriiquecampos

I mean, there are plenty of situations where you can achieve the same thing in multiple, not just two, different ways. I could cite at least five out of my head:

* Loading with ResourceLoader vs `load()` keyword

* Drawing textures with `draw_texture_rect()` vs `TextureRect`/`Sprite`

* Displaying text with `draw_string()` vs `Label`

* Drawing polygons with `draw_polygon()` vs `Polygon2D`

* Drawing line with `draw_line()` vs `Line2D`

* Accessing nodes with `get_node(NodePath)` vs `$NodePath`...

note that most of the functions are the more low level and simplified version of the same functionality without some overhead. which both alternatives needed in different use cases.

The Tween Node removed because there were no additional usability nor interesting inspector properties as stated in #514. And you still need to write code to set it up anyway.

But the thing is: in this case specifically we can just merge the two into a Node, we don´t need to keep the current Tween RefCount after porting it as a Node

why the new class literally autostarts and the whole issue is caused precisely because it starts without anyone deliberately saying so, triggering its removal and losing reference to it?

cmiiw this is the norm for tween behavior in other engines. DoTween in Unity is also auto freed when tween finished. Otherwise it will gives memory leak.

I think what you want from the old Tween Node is its reusability by not set the tween to be autokilled. And as a node, it will be freed when the node or its parent is freed.

So we need both..

But for the purpose of reusability of Tween, i think add ability to set off autokill would be better solution just like DoTween has Tween.SetAutoKill(false)

@akien-mga
Copy link
Member

As a side note, for those who want the exact same API as Godot 3's Tween, I have a GDExtension that implements this via the Threen node. I just updated it to make sure it works with Godot 4.1+: https://github.com/akien-mga/threen/releases/tag/1.0-stable

The purpose was to help people migrating from Godot 3 to Godot 4 to reuse their existing Godot 3 Tweens, but if some really want to keep using that API going further, this extension should help.

That doesn't mean we can't improve the current Godot 4 Tween API and bring back some of what people liked about Godot 3, I just mention this as a usable workaround for people who want it now.

@henriiquecampos
Copy link
Author

@henriiquecampos

I think this is the relevant part of the documentation that you seem to have overlooked.

Note: Tweens are not designed to be re-used and trying to do so results in an undefined behavior. Create a new Tween for each animation and every time you replay an animation from start. Keep in mind that Tweens start immediately, so only create a Tween when you want to start animating.

In most (every?) example that you have given of the current Tween implementation not working, you have not been following this advice.

I did read that and understand that which doesn't mean I think this is better than having a Node.

This is what is been missing in this whole thread all along:

Just because you use and understand the new system doesn't mean it can't be improved.

@jrassa
Copy link

jrassa commented Sep 29, 2023

@henriiquecampos

I did read that and understand that which doesn't mean I think this is better than having a Node.

Continuing to misuse it in examples doesn't really help your argument. The impression it gives is that you do not understand the current API which is why many of the responses have been to correct your mistakes and show the proper way to do it with the current API.

To better make your argument, you should present 3 sets of code.

  • How it works in 3.x
  • How it works in 4.x
  • How you would like it to work in 4.x

Then you need to express how your proposed changes are an improvement.

@henriiquecampos
Copy link
Author

@jrassa

@henriiquecampos

I did read that and understand that which doesn't mean I think this is better than having a Node.

Continuing to misuse it in examples doesn't really help your argument. The impression it gives is that you do not understand the current API which is why many of the responses have been to correct your mistakes and show the proper way to do it with the current API.

To better make your argument, you should present 3 sets of code.

* How it works in 3.x

* How it works in 4.x

* How you would like it to work in 4.x

Then you need to express how your proposed changes are an improvement.

I sort of did this here #7892 (comment) and in the original proposal(where I showcase how I managed to achieve the desired behavior with the new API)

@jrassa
Copy link

jrassa commented Sep 29, 2023

@henriiquecampos

I sort of did this here #7892 (comment)

Here is your last code snippet from that post. It uses the API incorrectly. You prefaced this by saying Let's try to do the same thing on 4.0. It is not presented as this is how I would like it to work. It is presented as I tried this and it doesn't work.

extends Node2D


var tween


func _ready():
	tween = create_tween()
	tween.stop()


func _unhandled_input(event):
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		var tweener = tween.tween_property($Sprite2D, "modulate", Color.BLUE, 3.0)
		tweener.from(Color.RED)
		tween.play()


func _on_timer_timeout():
	queue_free()

There are two mistakes:

  • You are creating the tween in _ready before you actually need it. While not an invalid use, it goes against the directions/advice from the documentation. It would not have worked without calling tween.stop() as the tween would have run immediately and then become invalid by the time _unhandled_input was called.
  • You are attempting to play the same tween multiple times.

The correct way to do this in 4.0 is below.

extends Node2D


var tween


func _unhandled_input(event):
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		tween = create_tween()
		var tweener = tween.tween_property($Sprite2D, "modulate", Color.BLUE, 3.0)
		tweener.from(Color.RED)


func _on_timer_timeout():
	queue_free()

@jknightdoeswork
Copy link

jknightdoeswork commented Jan 23, 2024

New Tween API is overfitted for making sequential, non interuptable tweens that execute once. Unfortunetly this use case never manifests in our games because "uninteruptable" promotes a user experience that does not live up to my quality standards of reliable instantaneous feedback to user input. ie: Closing a dialog while it is playing it's Open animation. Now again, but this time the Open animation is several tweens, with several layers of paralellism. Old tween API allowed this easily as it naturally allowed 'grouping of tweens', new Tween API simply falls over when challenged with 'interruptibility'. Yes, it is doable, but the resulting code is full of boilerplate and esotericism.

  • Tween Nodes provided a clear mechanism for 'grouping' tweens
  • Tween Node allowed Pause, seek, stop, restart on a logical group of tweens
  • Tween Node allowed simple reuse of tween operations (restart())
  • Tween Nodes allow several modular scripts to reference the same group of tweens, promoting clean code design
  • Tween Nodes are visually present in the Scene Tree promoting rapid visual understanding of control flow in a given scene tree
  • Tween Nodes fit in Godots philosophy of everything is a node (even HTTPRequest is a Node)
  • Tween Node promoted itself to implementing a custom editor to visually design tweens in the Editor (I created something for this, Godot itself should have/could have done something here, but not anymore)
  • Tween Nodes did not invalidate themself, they were always ready to accept new calls
  • New Tween API works great for creating sequential animations that run once, but works poorly for almost everything else, including parallel tweens that are more than just 2 tweens in parallel (Cocos2D action system did this better)
  • New Tween API introduces a new class of bug: "invalid tween"

Godot is praised for keeping a consistent design on 'everything is a Node'. But now, there is a small secret, a leak, a flaw, where there is now a third type of object: the Tween. Now we have Node, Resource and Tweens. Tweens are a new, third thing, something that has it's own lifetime semantics, there label as a 'Reference' is not be trusted, for they can be a valid reference yet an 'invalid tween'. Explaining what an 'invalid tween' is is so obtuse that it leaks it's internal design. One cannot simply use a SceneTreeTween, one must know that it is a handle to some other system and invalidates itself at the end. This core piece of knowledge must be remembered at every usage of the API. New tween's lifetime semantics are so foreign to the rest of the engine that it must introduce a new word: "kill()". Keep in mind that even HTTPRequests are a Node in godot, and it becomes crystal clear that Tween was a mistake. I truly wonder where Juan's vision was on the design of this new thing, for he seemed to have such a strong vision for Godot, yet Tween is so incongruent to it.

I have learnt to bend the new tween API to my will, but it's obtuse, esoteric and repeated in many files:

Group some tweens:

func create_tracked_tween()->SceneTreeTween:
	var t := create_tween()
	tracked_tweens.append(t)
	return t

func stop_tracked_tweens():
	for t in tracked_tweens:
		var scene_tree_tween:SceneTreeTween = t
		if scene_tree_tween and scene_tree_tween.is_valid():
			scene_tree_tween.kill()
	tracked_tweens.clear()

Repeated everywhere boilerplate:

if tween != null and tween.is_valid():
	tween.kill()
tween = create_tween()

Little known hack that prevents tweens from invalidating themselves:

tween.connect("finished", t, "stop")

imo basically every tween that you create, must allow itself to be instantly stopped. ie: Opening/Closing a dialog. If a user presses the close button while the dialog is opening, the open tween must stop, and the close tween must instantly start. How would the designers of the new tween API implement this simple, core use case? The concept 'fire and forget' never manifests because you can never forgot, every frame of animation lives under the game designers control during it's entire duration, because we must allow user input to have instantaneous feedback. One cannot simply 'forget' about a Tween that has been started, and because of this, because the new Tween api overfits itself to an inferiour user experience, I must maintain far more boilerplate bookkeeping throughout our projects in order to use the new tween api.

Listen, I dont want to hurt anyones feelings. Some people have clearly worked hard on the new Tween api, and I respect those people and their efforts. Dont take this personally: new Tween api was deeply mistaken. I know there's probably no going back, but I fear this will cause a fracture in Godot, where libraries like Threen become semi-dominant.

@CarpenterBlue
Copy link

Why do people keep repeating the "everything is node" mantra? I don't think it was ever true and it's slowly being phased out of the engine anyway. In fact the docs have a whole article about how to avoid using Nodes for everything.

@KoBeWi
Copy link
Member

KoBeWi commented Jan 24, 2024

If a user presses the close button while the dialog is opening, the open tween must stop, and the close tween must instantly start. How would the designers of the new tween API implement this simple, core use case?

var anim_tween: Tween

func open():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)

func close():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)

What is the equivalent in Threen?

@jknightdoeswork
Copy link

Thanks for the writeup.

export var anim_tween:Tween # I dont use godot 4 so not sure of syntax here

func open():
	anim_tween.remove_all()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)
        anim_tween.start()

func close():
	anim_tween.remove_all()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)
        anim_tween.start()

@KoBeWi
Copy link
Member

KoBeWi commented Jan 24, 2024

Right, so for this simple use case they are pretty much the same. rewove_all() is replaced with kill() check and create, which can be moved to a single method call. But with the old system you also have a useless node, which you need to setup in the inspector.

I am the person who designed and implemented the new Tweens. The new system is heavily based on DOTween, which is a popular tweening addon for Unity. As much as I hated working in Unity, DOTween was one thing I really liked and it was so much better than what was in Godot. Before rewriting Tweens, I made a GDScript addon which I was using instead of the old system; it had pretty much the same API as we have now in core and it proved really useful. From my experience, almost every animation you want to do with a Tween is sequential, which in the old system was hell to write. Refer to the old proposal, which explains the problems with old Tweens and how new system solves them: #514

There was a great deal of community feedback involved when the new system was made. The reception was overwhelmingly positive (implementation PR for reference: godotengine/godot#41794), while the reactions under this proposal are not really in its favor... Going back to the old system is out of consideration for that reason (and many others), and personally I'm staying out of this discussion (well, except this comment).

The little known hack you mentioned was actually documented in the past as a way to reuse Tweens, but since people were abusing it, the Tweens are officially not reusable now. I actually have an idea how to make them reusable, but I have yet to see a case where it's more convenient and/or fixes a performance bottleneck. Also I don't recall anyone opened a proposal for that.

Custom editor for Tweens is still very much doable and I actually have an idea for an addon that would add it. For now there are no plans to have it in core, because there is no demand.

@jknightdoeswork
Copy link

jknightdoeswork commented Jan 25, 2024

but since people were abusing it, the Tweens are officially not reusable now. I actually have an idea how to make them reusable, but I have yet to see a case where it's more convenient and/or fixes a performance bottleneck.

I want to reuse them so I dont have to re-run the setup code that creates tweens.

I actually have an idea how to make them reusable, but I have yet to see a case where it's more convenient and/or fixes a performance bottleneck.

This is huge. Let me explain:

We have performance problems with basically every single line of GDScript that runs on a per frame basis in our lowest end target platform: Mobile Web. We have 30% of our web users using mobile devices. We're talking tens of thousands of devices every single day.

The web is a massive new burgeoning market for indies, and 30% of the users there are on mobile. It's a bigger market in terms of raw player #s then mobile, console and PC combined. Godot has an advantage here over Unity, and it would be Godot's error to not see and foster that capability. To develop further into this world, Godot must strive for optimal performance. Memory allocation has a large cost in WASM. I have sought out and eliminated even String(), even StringName() allocations in the per frame tick of our products. I have spent literal weeks finding and hunting down basically all GDScript that runs at runtime. Basically every single line of GDScript that runs in a per frame, per object basis is not capable of meeting our standards for performance in the WASM runtime.

If I can re-use tweens, then I can set them up in GDScript. If I can't, then I simply cant write the code for them in GDScript, because its too slow to run on a per frame basis for our use case.

Even the construction of the string to reference the property by name is too slow for our target platform. Not only does creating a String() allocate memory, but it iterates each character, converting it from a raw char to a wchar, one character at a time. Even writing "hello_world" in a per frame basis on a player's renderer is a deal breaker for mobile web, let alone using the scripting system to turn that string into a property reference.

I love GDScript, but for certain projects on certain platforms, namely the web platform, GDScript is way too slow.

To summarize: If I can't reuse the tween setup code, I can't write it in GDScript. If I can re-use the setup code, I can write it in GDScript.

@jknightdoeswork
Copy link

jknightdoeswork commented Jan 25, 2024

From my experience, almost every animation you want to do with a Tween is sequential, which in the old system was hell to write.

You are right about this, and this temptation has led me to use the new Tween api. I think the new Tween API had a good intention. It has a good raw tweening interface, but everything around that tweening api is a step backwards. It should have stayed within the constraints set out by the existing archetypes in Godot, namely Node or even Reference. If you really wanted it to not be a Node, it should not invalidate itself while there still lives a Reference. In my opinion, it should have stayed rooted in a Node. There are so many benefits, which I outlined before. Namely, the removal of 'invalidating itself' and the implict 'grouping of tweens'. I forgot one in my previous post: process_priority. In Godot 3's SceneTreeTweens, they always happen first, before other code, and I have no clue what order they happen in between each other. For things like player and camera movement, you need to be able to control what happens in what order. Tween as a Node allowed this implicitly, the new Tween api has no concept of intra-frame ordering. These issues of what was lost in the redesign pile up over and over and it becomes clear to me that the redesign was too liberal and too experimental.

I haven't even bothered to figure out what order Tweens happen in in the new Tween api, because the 'gain' of using the new abstraction is out weighed by the cost of me having to figure all this stuff out.

EDIT: Just so were clear KobeWi, I respect and value your work. I wish I had commented on the Tween proposal when it was announced, but Godot 4 was not something I could invest my time into until the web platform exceeded Godot 3's capabilities.

@KoBeWi
Copy link
Member

KoBeWi commented Jan 25, 2024

it should not invalidate itself while there still lives a Reference

The invalidation is needed to automatically remove Tweens from processing. Basically you start a Tween and when it's start processing, SceneTree will clean it up. The invalidation happens at the end of animation, it's all documented.

Tween as a Node allowed this implicitly, the new Tween api has no concept of intra-frame ordering.

You can make the Tween process in either physics frame or process frame. But yeah, the processing happens at the end of the frame and there is no way to control that (btw this is the same behavior as SceneTreeTimer). However tween processing is an enum, so adding values for something like EARLY_PROCESS and EARLY_PHYSICS would be easy. Would that solve your problem?

Also I don't get the argument about "per frame" allocations. Are you creating Tweens per frame? Why?
Though I remember someone else brought up the allocation problem once. While it does not make a difference for desktop platforms, maybe in mobile/web it makes a difference, idk. I'll write a proposal how I imagine the Tweens could be reusable.
EDIT: #8966

@CarpenterBlue
Copy link

CarpenterBlue commented Jan 25, 2024

If a user presses the close button while the dialog is opening, the open tween must stop, and the close tween must instantly start. How would the designers of the new tween API implement this simple, core use case?

var anim_tween: Tween

func open():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)

func close():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)

What is the equivalent in Threen?

I often get away with ignoring the kill part since the tween gets invalidated anyway and tweens added later that are tweening same value have priority, This should be enough for something this simple no?

var anim_tween: Tween

func open():
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)

func close():
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)

In regards to the remove_all() issue folks seem to be having.
Does Node know what SceneTreeTweens are bound to it?
That could solved by adding a new method that would work like this:

get_tree().remove_all_tweens(node)

Though, I don't really understand why do people seem to need it.
The new SceneTreeTweens offers much more control if one stops refusing to understand how they work.
They do require bit different mindset.

People, you can just have classic Node with a tween script if you really need the TweenNode so much.

image

@jknightdoeswork
Copy link

Also I don't get the argument about "per frame" allocations. Are you creating Tweens per frame? Why?

Not necessarily every frame, but what I'm saying is that running the tween setup code is expensive and being forced to re-run the setup code is a significant problem to us. A huge factor in that cost is the memory allocations that occur when running the Tween setup code via GDScript.

@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 26, 2024

It would be a good idea to get a few examples and profile/benchmark them. To demonstrate the issue more vividly, if nothing else.

@henriiquecampos
Copy link
Author

henriiquecampos commented Jan 29, 2024

It would be a good idea to get a few examples and profile/benchmark them. To demonstrate the issue more vividly, if nothing else.

It's weird how much attrition we need to request to bring back a feature THAT ALREADY EXISTED while nobody saw any issue completely rewriting the Tween system breaking compatibility and breaking the Godot's design philosophy.

See: godotengine/godot#41794
And: #514

People just thought "yeah nice, we have new features? Parallel execution(something 100% possible with the previous system) JUST DO IT!!"

Meanwhile, here we are debating to bring back a 6 years old feature that had thousands of projects reinforcing its effectiveness.

Did anyone request a profile/benchmark to overhaul the system as well? Honestly would make way more sense. IT WAS AN OVERHAUL AFTERALL

@henriiquecampos
Copy link
Author

Right, so for this simple use case they are pretty much the same. rewove_all() is replaced with kill() check and create, which can be moved to a single method call. But with the old system you also have a useless node, which you need to setup in the inspector.

I am the person who designed and implemented the new Tweens. The new system is heavily based on DOTween, which is a popular tweening addon for Unity. As much as I hated working in Unity, DOTween was one thing I really liked and it was so much better than what was in Godot. Before rewriting Tweens, I made a GDScript addon which I was using instead of the old system; it had pretty much the same API as we have now in core and it proved really useful. From my experience, almost every animation you want to do with a Tween is sequential, which in the old system was hell to write. Refer to the old proposal, which explains the problems with old Tweens and how new system solves them: #514

There was a great deal of community feedback involved when the new system was made. The reception was overwhelmingly positive (implementation PR for reference: godotengine/godot#41794), while the reactions under this proposal are not really in its favor... Going back to the old system is out of consideration for that reason (and many others), and personally I'm staying out of this discussion (well, except this comment).

The little known hack you mentioned was actually documented in the past as a way to reuse Tweens, but since people were abusing it, the Tweens are officially not reusable now. I actually have an idea how to make them reusable, but I have yet to see a case where it's more convenient and/or fixes a performance bottleneck. Also I don't recall anyone opened a proposal for that.

Custom editor for Tweens is still very much doable and I actually have an idea for an addon that would add it. For now there are no plans to have it in core, because there is no demand.

Why being "compliant" with a Unity addon would make anything better? I mean, I'm on Godot, I like using Nodes, it's the engine's design philosophy. I'd much rather set up a node through the inspector than have to ensure every time I interpolate something it will use the physics process.

It wasn't a hell to write sequential animations, you just needed to use yield or the node's signal. Which is pretty much how Godot was designed to work. Now we have this weird system that...how do we make sequential animations on it? I don't know because it's specific to this feature, meaning a parallel knowledge unnecessary before, breaking the intuitive design of using a Tween Node, that works as every other node, so I could build upon the knowledge of Nodes.

How do I make specialized Tweens in this new system? Ohh I'll have to make a Node that instantiates these Tweens, even tho these Tweens behave EXACTLY as if they were nodes, with the drawback of being refcounted so I can't simply wait for the time I would actually use them, I have to somehow keep them in memory with weird workarounds instead of having them visible on the SceneTree, both remotely and in the editor.

@henriiquecampos
Copy link
Author

If a user presses the close button while the dialog is opening, the open tween must stop, and the close tween must instantly start. How would the designers of the new tween API implement this simple, core use case?

var anim_tween: Tween

func open():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)

func close():
	if anim_tween:
		anim_tween.kill()
	
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)

What is the equivalent in Threen?

I often get away with ignoring the kill part since the tween gets invalidated anyway and tweens added later that are tweening same value have priority, This should be enough for something this simple no?

var anim_tween: Tween

func open():
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 100, 0.5)

func close():
	anim_tween = create_tween()
	anim_tween.tween_property(dialog, "size:y", 0, 0.5)

In regards to the remove_all() issue folks seem to be having. Does Node know what SceneTreeTweens are bound to it? That could solved by adding a new method that would work like this:

get_tree().remove_all_tweens(node)

Though, I don't really understand why do people seem to need it. The new SceneTreeTweens offers much more control if one stops refusing to understand how they work. They do require bit different mindset.

People, you can just have classic Node with a tween script if you really need the TweenNode so much.

image

...Honestly, look how much effort you are making to get something that we had effortless before.

Bound a Tween to a node(even tho this Tween already have its own life cycle, that works exactly like a node, with the downside of being removed if we don't manually keep it in memory due to being a refcount) makes the Tween effectively IDENTICAL to the Tween node we had in 3.0.

Literally everything you guys are pointing out just justifies making this approach a node, effectively bringing back the Tween node and merging it with whatever advantage this new system has over it.

I mean, for real, no butthurt, what are the actual advantages of this new system? Anything a custom method wouldn't solve?

Like, for sequential animations, maybe have a Tween.interpolate_sequence()?

@MewPurPur
Copy link

MewPurPur commented Jan 29, 2024

Let's keep it professional and on-topic and not get mad over a software. These arguments feel like they are made in bad faith, you misconstrued much of what you responded to.

It's weird how much attrition we need to request to bring back a feature THAT ALREADY EXISTED while nobody saw any issue completely rewriting the Tween system breaking compatibility and breaking the Godot's design philosophy.

The proposal was unanimously liked. Part of the governance philosophy is that if we can't get near-universal agreement on a big feature, we won't implement it. The tween rework had it, this proposal doesn't. So we're bargaining, maybe you have a valid use case, you need performance of Tween nodes or something? We want to see you justify yourself so maybe we can get to a universal agreement about at least some kind of problem needing to be solved.

Meanwhile, here we are debating to bring back a 6 years old feature that had thousands of projects reinforcing its effectiveness.

People are making thousands of projects with the new tween too. People use tweens when they do what they want, regardless of whether the implementation is good.

Did anyone request a profile/benchmark to overhaul the system as well? Honestly would make way more sense. IT WAS AN OVERHAUL AFTERALL

Nodes tend to have a lot of baggage around them, they are a far more heavyweight object than RefCounted, so in all likelihood the new system is faster in most cases. But anyway, as discussed above, the proposal was focused on usability, so people did not focus on profiling or benchmarking. Performance did become a focus of this discussion, so we're focusing on that. There's no bias, just context.

...Honestly, look how much effort you are making to get something that we had effortless before.

It's a cherrypicked example. When moving my project to 4.0, all of my tweens became way less code than they used to be. I do miss the ability to keep tweens around, but that's not much of a problem either.

@YuriSizov
Copy link
Contributor

It's weird how much attrition we need to request

For a claim that there are performance issues? Yeah, we absolutely need an exhaustive set of data. We need to look into what's going on, if there are suboptimal hotpaths, if there are bugs in the implementation, if there is a wrong assumption by user which can be clear or directed with better API.

@henriiquecampos
Copy link
Author

@MewPurPur right, sorry if this was the tone I went to in the previous messages.

Nodes tend to have a lot of baggage around them, they are a far more heavyweight object than RefCounted, so in all likelihood the new system is faster in most cases.

Correct me if I'm wrong, but the proposed workarounds(making a Node to hold Tween code and bounding it to a node effectively increase the resources load since now there will be a RefCount and a Node instead of just a Node, no?

Performance did become a focus of this discussion, so we're focusing on that. There's no bias, just context.

Why did it suddenly become a focus of discussion? I mean, nobody mentioned it in the overhaul and nobody mentioned issues with Tween performance when it was a Node. It seems to me that it is just a way to put a wall and deny the proposal. Bringing the Tween Node back, for instance as an independent class, wouldn't prevent anyone from using this approach. So...people could choose the approach that better fit their projects, including if performance is an issue. We can have TweenNode and the Tween[RefCount], I can't see why not.

It's a cherrypicked example. When moving my project to 4.0, all of my tweens became way less code than they used to be. I do miss the ability to keep tweens around, but that's not much of a problem either.

Through the discussion, the sum of "cherry-picked" examples build a lot. There are many examples where we need to come out with weird workarounds. The very implementation of the new system presumes it should "work like a node". I mean, I already pointed out: its life cycle, bounding to a Node, etc...it was made like a Node and even has methods to become part of a Node, so we can get the advantages of it being a Node. So why not have a Node already?

Fundamentally, what would change in anyone's life if we had a TweenNode to keep up with the original design and the new Tween system to support those who would like to use Unity but are now on Godot and miss Unity's approach to stuff?

I pretty much think we can have both, as we do have in many other instances, mainly the Timer node.

@henriiquecampos
Copy link
Author

For reference, there were debates about Tween chaining, and back then, people agreed the current tools we had were enough.

godotengine/godot#17543

@henriiquecampos
Copy link
Author

So, look at this:

https://github.com/godotengine/godot/issues?page=3&q=tween+performance

Tween node's performance was never an issue. So why should it block bringing it back?

@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 29, 2024

Tween node's performance was never an issue. So why should it block bringing it back?

Where did you get the idea that performance is blocking this suggestion? A performance issue was brought up before as a downside of the current approach. I asked for a demonstration of the issue so it can be investigated.

PS. Also, please use the edit button.

@CarpenterBlue
Copy link

@henriiquecampos

...Honestly, look how much effort you are making to get something that we had effortless before.

Bound a Tween to a node(even tho this Tween already have its own life cycle, that works exactly like a node, with the downside of being removed if we don't manually keep it in memory due to being a refcount) makes the Tween effectively IDENTICAL to the Tween node we had in 3.0.

Literally everything you guys are pointing out just justifies making this approach a node, effectively bringing back the Tween node and merging it with whatever advantage this new system has over it.

I mean, for real, no butthurt, what are the actual advantages of this new system? Anything a custom method wouldn't solve?

Like, for sequential animations, maybe have a Tween.interpolate_sequence()?

You have to understand, I made these suggestions only to help you alleviate your pains with using the new system and at least partially bring back your workflow.

I treat the tweens as fire and forget thing and I personally don't use any of these suggestions as they are simply not needed.

@Nodragem
Copy link

I think a big part of this debate is about when a feature should be implemented as a Node in Godot. There seems to be no clear rules, so I opened a thread here to discuss about it:
#8998
I would love to hear your take. We might not find clear rules, but we might get a clearer idea of when we want Nodes.

@theraot
Copy link

theraot commented Jan 31, 2024

For people who want Tween Nodes: would Capture in AnimationPlayer be a good alternative for you? #8513 / godotengine/godot#86715

@Zireael07
Copy link

Yes and no. IMHO they have different use cases

@KoBeWi
Copy link
Member

KoBeWi commented Feb 7, 2024

https://github.com/KoBeWi/Godot-Tween-Suite

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

No branches or pull requests