Godot-proposals: Add support for code animation

Created on 22 Jun 2020  路  34Comments  路  Source: godotengine/godot-proposals

Describe the project you are working on:
Mobile game
Describe the problem or limitation you are having in your project:

I have a few lines of code that I would like run bit by bit not just at once. I know you are thinking just use yield() but I have a lot of code and I also want to have a bit more control over the code animation like the easing
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
The solution is An animate block 馃く馃く馃く
An animate block???
Yes an animate block

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
The animate block just like if block but used for code animation something like animate (duration = 4s):
#Your code to animate here

Continue code here

If this enhancement will not be used often, can it be worked around with a few lines of script?:
No it can't the only other way to do it is to use yield and it's more tedious and less controllable

Is there a reason why this should be core and not an add-on in the asset library?:
It gdscript based which is core

core

Most helpful comment

I would love to think this through and get back to you with a better Innovation if possible @BeayemX @Xrayez @dalexeev @KoBeWi @Calinou

All 34 comments

You can use the Tween node or AnimationPlayer node for this :slightly_smiling_face:

I do think Godot could benefit from a general-purpose GDScript animate method given that animation is one of the prominent features of Godot.

I've actually done this in my fork as a proof-of-concept: https://github.com/Xrayez/godot/commit/341787415057bed912c85dc26becec685dbc4bf3.

Also see my findings at https://github.com/godotengine/godot/issues/26529#issuecomment-564798612.

As mentioned there, the alternative would be adding get_tree().create_tween() method, similarly to get_tree().create_timer(), to which you would still have to yield or connect, but nonetheless makes it more intuitive, compare:

Timer

# Pause the code execution for 5 seconds.
yield(get_tree().create_timer(5), "timeout")

Tween

# Fade out Godot icon for 5 seconds and remove it afterwards.
var tween = get_tree().create_tween($Godot, "modulate:a", null, 0.0, 5)
tween.connect("completed", $Godot, "queue_free")

# vs

var tween = Tween.new()
add_child(tween)
tween.interpolate_property($Godot, "modulate:a", null, 0.0, 5, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
tween.start()
tween.connect("completed", $Godot, "queue_free")

@KoBeWi may also shed some light on this.

@Xrayez having to use yield every time is somehow a bad practice imagine using yield to get an animation for 200 lines of code that you need to animate it would be horrible and tedious

@Calinou using the tween node was what I thought of before posting this, like:

```gdscript
onready var tween = $Tween
tween.interpolate_method()
But can you point me to a tutorial that uses animation player to animate code or tween because I found none

having to use yield every time is somehow a bad practice

@iapps207 yielding might not be as "bad" or error-prone in future Godot 4.0 with the advent of GDScript "2.0": godotengine/godot#39093 (see await keyword).

But can you point me to a tutorial

The demo project for tweening is here.

I do think Godot could benefit from a general-purpose GDScript animate method given that animation is one of the prominent features of Godot.

I've actually done this in my fork as a proof-of-concept: Xrayez/godot@3417874.

Also see my findings at godotengine/godot#26529 (comment).

As mentioned there, the alternative would be adding get_tree().create_tween() method, similarly to get_tree().create_timer(), to which you would still have to yield or connect, but nonetheless makes it more intuitive, compare:

Timer

# Pause the code execution for 5 seconds.
yield(get_tree().create_timer(5), "timeout")

Tween

# Fade out Godot icon for 5 seconds and remove it afterwards.
var tween = get_tree().create_tween($Godot, "modulate:a", null, 0.0, 5)
tween.connect("completed", $Godot, "queue_free")

# vs

var tween = Tween.new()
add_child(tween)
tween.interpolate_property($Godot, "modulate:a", null, 0.0, 5, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
tween.start()
tween.connect("completed", $Godot, "queue_free")

@KoBeWi may also shed some light on this

Yeah I think this should be more reasonable @Xrayez

@Xrayez I came up with a problem statement to better understand my issue this is the code when I use yield

gdscript $Line2d.add_point(0,20) yield() $Line2d.add_point(0,60) yield() $Line2d.add_point(10, 60) yield() $Line2d.add_point(10,20)...#note that I didn't use vector2 in add_point above it's intended But I had to yield 4 times but let's say there was an animate block: gdscript
animate (2seconds, ease_in):
$Line2d.add_point(0,20)
$Line2d.add_point(0,60)
$Line2d.add_point(10,60)
$Line2d.add_point(10 ,20)
The player would see the same animation of a box being drawn but now I have more control over the code animation and it's also easier than using yield or in the future await

@Xrayez You were talking about animations I could use the animation player to carry out the stuff below

@Xrayez I came up with a problem statement to better understand my issue this is the code when I use yield

$Line2d.add_point(0,20)
yield()
$Line2d.add_point(0,60)
yield()
$Line2d.add_point(10, 60)
yield()
$Line2d.add_point(10,20)...

But I had to yield 4 times but let's say there was an animate block:

animate (2seconds, ease_in):
  $Line2d.add_point(0,20) 
  $Line2d.add_point(0,60)
  $Line2d.add_point(10,60)
  $Line2d.add_point(10 ,20)

The player would see the same animation of a box being drawn but now I have more control over the code animation and it's also easier than using yield or in the future await

But in my case what if I I want to use code that changes on different devices let's say the screen size. Now if I wanted to cover screen size with an animated line being drawn and I decide to use the animation node the animation player would not be able to solve that problem because I can only use specific values.
Well what about the tween node? The tween node can be used in this case but the code is much larger and harder to control the animation flow and it would also be harder if I wanted to animate more lines being drawn.
Hoping for a reply @Xrayez and @Calinou

@iapps207 you can use KoBeWi's TweenSequence script to simplify this perhaps.

Yeah I didn't get at first what you meant as animation block. That somehow reminds me #623, so you could for instance run another expression after each statement, in this case this would be yield as you say.

As of now, this can be implemented as a function, here's what I've come up:

extends Node

func _ready():
    yield(animate(3), "completed")
    yield(animate_start(), "completed")
    print("done!")

func animate(seconds):
    var s = seconds
    while s > 0:
        print(s)
        yield(get_tree().create_timer(1.0), "timeout")
        s -= 1.0

func animate_start():
    return animate(3)

The inherent "issue" with yield itself is that, whenever you call it, the function returns to the caller, so you have to add another awaiting yield yield(do_stuff_with_yeilds(), "completed")

IMO the changes I proposed to Tweens would be enough for any in-code animation. They are inspired by DOTween, which is one of the top and widely used animation plugins for Unity (and is literally the only good thing Unity has to offer xd). I've been using the class I implemented in GDScript successfully in multiple projects.

If you give some example code you'd like to animate I could translate it to TweenSequence code for comparison (something more clear than your example above, because it's hard for me to tell what is it supposed to do).

Maybe I don't understand your usecase correctly, but it seems that you try to force an attempted solution onto Godot instead of re-evaluating your approach.

animate (2seconds, ease_in):
$Line2d.add_point(0,20)
$Line2d.add_point(0,60)
$Line2d.add_point(10,60)
$Line2d.add_point(10 ,20)

This looks like you hardcoded a for-loop.
Wouldn't it be better to store your points in a list

var points = [ [0,20], [0,60], ... ]

and then iterate over the list

for point in points:
    $Line2D.add_point(point)
    yield()

@BeayemX the reason why I am not using this method is lack of control over animations like overall time e.r x

In your animate(2seconds) block, does this mean that the overall time for the whole block would be 2 seconds or that each line will be executed after 2 seconds?

because you could do something like this:

var step_time = overall_time / len(points)
for point in points:
    $Line2D.add_point(point)
    yield(get_tree().create_timer(step_time), "timeout"))

That's the overall time for that animation

The main issue here is getting rid of the yield function to animate the line. Yes Godot 4.0 will change it to await but there are still some limitations of using yield to carry out this process. For example you said using a list , what if I was making calculations to get the point like when I am using it to draw a road map

In 4.0 you can do this:

func add_point(x, y):
    $Line2d.add_point(x, y)
    await get_tree().create_timer(0.5).timeout

func my_func():
    await add_point(0, 20)
    await add_point(0, 60)
    await add_point(10, 60)
    await add_point(10, 20)

In my opinion, there are still too many await, but much better than with yield.

@dalexeev but using yield or await is the problem it feels like bad coding habit and I still insist on getting more control over the animation like ease type and use different types of transitions I mean isn't this cleaner code

```gdscript
animate(duration,transition,ease_type):
#Your code to animate here
And since gdscript is being rewritten this is the best time to get this into the code @dalexeev , @Calinou and @Xrayez

Well, how should the following code work?

animate (2seconds, ease_in):
    print(1)
    if randi() % 2:
        for i in randi() % 5:
            print(i)

I still insist on getting more control over the animation like ease type and use different types of transitions

Why are Tween and AnimationPlayer not suitable?

it feels like bad coding habit

isn't this cleaner code

Code cannot be smarter than a programmer. :smiley: Explain more precisely how the feature should work.

@dalexeev The code works by dividing the duration by the number of lines of codes under the animate block, the transition just gives some additions or subtractions in time. In your code

animate (2seconds, ease_in):
    print(1)
    if randi() % 2:
        for i in randi() % 5:
            print(i)

there are basically only two lines of code because there are only two single tab indented lines and the code below of it consists of two tab indented lines or more so therefore duration/lines of code
basically gives 1 second for each block of code
So the first second runs

print(1)
While the last or second second in this case runs
```gdscript
    if randi() % 2:
        for i in randi() % 5:
            print(i)

Because they are now all considered to be the same line of code

Your alternative would be using

 print(1)
 yield(1sec) or await(1sec) 
    if randi() % 2:
        for i in randi() % 5:
            print(i)

But as your code grows using await gets longer and harder to keep track off just imagine having 20 lines of code you need to animate

Thanks for the explanation. But this way is quite unobvious and significantly narrows the scope of this feature.

For example, the following code will not work, as I would like:

animate (2seconds, ease_in):
    for i in 10:
        $Line2d.add_point(0, 10*i)

And I will have to do this:

animate (2seconds, ease_in):
    $Line2d.add_point(0, 0)
    $Line2d.add_point(0, 10)
    $Line2d.add_point(0, 20)
    $Line2d.add_point(0, 30)
    $Line2d.add_point(0, 40)
    $Line2d.add_point(0, 50)
    $Line2d.add_point(0, 60)
    $Line2d.add_point(0, 70)
    $Line2d.add_point(0, 80)
    $Line2d.add_point(0, 90)

This code no longer seems clean, right?

This code no longer seems clean, right?
Yeah but we only have problems with loops and I wasn't thinking of that problem
But what if it was possible to get the number of loops and run a loop in the time given to the particular for block
```gdscript
animate(2seconds, ease_in):
for i in 10:
$Line2d.add_point(0, 10*i)

We know this code will loop ten times and t has only one second to run then we can run a particular loop in 10th of a second
Or if this doesn't seem feasible we could come up with better ways of making this work
The code for this looks like this as for now without the animate block

```gdscript
for i in 10:
$Line2d.add_point(0, 10*i)
yield(0.1)

So I don't see how this doesn't work with loops necessarily

What if you have an if condition that is not deductible from the code in the animate-block?
something like if the game is in a certain state or if a button is pressed?

animate(2, ease_in):
    if player.is_alive:
        $Line2d.add_point()
        $Line2d.add_point()
    $Line2d.add_point()
    $Line2d.add_point()

There is no way for the animate block to be able to determine how many lines will actually be executed

@dalexeev so what I am saying in my above post is that after every 1 tabbed indented post we add like an animation break off

```gdscript
print(1)

animation break here

for I in x:
do_somerhing()
. #animation_ break here

@BeayemX my above post fixes this problem

What if you have an if condition that is not deductible from the code in the animate-block?
something like if the game is in a certain state or if a button is pressed?

animate(2, ease_in):
    if player.is_alive:
        $Line2d.add_point()
        $Line2d.add_point()
    $Line2d.add_point()
    $Line2d.add_point()

There is no way for the animate block to be able to determine how many lines will actually be executed

I would love to call my above method code animation fragmentation where it breaks
Off parts for animation whether deductible or not

The problem is a little more complicated. yield/await work like return (suspend execution of the current function and resume the function up the stack), see the discussion in https://github.com/godotengine/godot/pull/39093. animate will work as well. In general, asynchronous programming is always difficult.

I see your point but in Gdscript 2.0 we use the await function which is the less buggy than yield so the function could be something like

````gdscript 2.0
for I in x:
do_something()
await_do_something()
await_for_loop_or_any_other_loop_above

I think the main problem is dealing with loops

I would love to think this through and get back to you with a better Innovation if possible @BeayemX @Xrayez @dalexeev @KoBeWi @Calinou

I imagine the best way to implement this feature would not be to make a "frame" for each line of code, but rather have a "frame" block, where each frame block is executed, sequentially, unconditionally (each time the animation requires the next frame). Any other kind of code directly nested inside an "animate" block would be an error.

Perhaps there could also be a "skip" command to skip the current frame...?

animate(2, ease_in):
    frame:
        if player.is_alive:
            $Line2d.add_point()
        else:
            skip
    frame:
        if player.is_alive:
            $Line2d.add_point()
        else:
            skip
    frame: $Line2d.add_point()
    frame: $Line2d.add_point()

Of course this is a lot of syntax for a potentially somewhat niche feature. :thinking:

It's obvious we know how to break the code into frames so I don't see why we can't just create code to break that something like an AI I will kinda create a python snippet to do something like this and probably get back to you

@HoneyPony That means that you create frame-dependent code. Which comes with its own drawbacks and is probably not desired for most games.

Please don't take this the wrong way, I just want to understand what you try to accomplish with this, but the more you guys try to make this proposal work the less I understand why this should be added.

I think the suggestions mentioned in this thread already (tweens, loops using yield, ...) should be enough to cover everything you want. Have you already tried actually implementing these suggestions?

The thing you are proposing, is this implemented in any other game engine or frame work?
Maybe you have experience with another framework/game engine and try to force that workflow onto Godot?

@BeayemX I'm not particularly invested in this proposal--I saw the discussion on how to break up the code into "lines," where I interpreted each line as being essentially a frame of animation (which is why I used the terminology "frame"). I thought I had a suggestion for how this chunking could be done unambiguously, rather than some nebulous metric of "lines."

I'm not really sure how the syntax I suggest creates "frame-dependent" code more than the original suggestion...?

Sooo here are my problems with this proposal. Let's take a look at the example code (because you didn't give any new example):

animate (2seconds, ease_in):
  $Line2d.add_point(0,20) 
  $Line2d.add_point(0,60)
  $Line2d.add_point(10,60)
  $Line2d.add_point(10 ,20)

From what I understand, it will call first $Line2d.add_point(), wait 0.5 seconds, then call another and so on.

Now, what if instead of calling a single function, I want to e.g. animate a property? Doing position = ... twice will set the position, wait and then set it again. That's not useful...
And making it interpolated will only add more magic to already odd feature.

But let's say it does interpolate. Now I want to move between three points, so I do position = ... 3 times. But what if I want each step to take different time? You want more control over time of the whole animation, but didn't even consider single steps. This can be worked around by empty print statements that will granulate your step time, but it's ugly af.

And then, what if I want each step to be interpolated with a different method? Same problem as with time, but this time no workaround.

To sum up, what you presented so far instead of being more flexible and cleaner, it adds a bizarre syntax magic for a questionably useful feature. This can be already achieved with tweens, and using the TweenSequence class I mentioned, my 3-point position example with different times and interpolations would be written as

var seq := TweenSequence.new()
seq.append(self, "position", Vector2(100, 200), 1)
seq.append(self, "position", Vector2(200, 400), 2).set_trans(Tween.TRANS_CUBIC
seq.append(self, "position", Vector2(150, 400), 0.5)

It might take more code to write than your animate() method, but it's extremely flexible and covers pretty much all use cases.

So you need to update your proposal with details that would cover at least as much as I mentioned, otherwise there's no chance for it to be implemented due to vagueness and questionable usability.

EDIT:
I just remembered you said you are going to rethink it. Well, just keep in mind what I wrote.

@HoneyPony If I remember correctly this was from a framework I used when I was 10 so I don't really remember since that was about 6 years ago now so maybe there are new ways of doing this now like tweens as you mentioned, but now I am starting to get confused about Tween Sequncer and how it solves a problem of constantly changing values via code but I actually get the picture......Somehow.....Maybe

But I will put some deep thinking into value and method interpolation via code but I kinda see how code animation breaks code. But I will try to get easier methods to animation in the future

Was this page helpful?
0 / 5 - 0 ratings