On-the-fly animations

While the default Animation editor works great if you know the values that you want to animate beforehand, it is not the right solution if you want animations that depend on the current state of your game. For example:
  • move and zoom the camera to the player figure or a particular enemy;
  • use animations to move a sprite on a grid based on player's input;
  • gradually modify the volume of a song based on the player's health bar;
  • create fade-in and fade-out effects by animating the alpha transparency of a sprite.
For my most recent game I have created a play_animation function which allows me to quickly create such animations from GDScript:

var camera_path = String(camera.get_path())
var duration = 1.0
play_animation(duration, [
        [ camera_path + ":zoom"
        , [ [0, camera.zoom], [duration, Vector2(1.0, 1.0)] ] ],
        [ camera_path + ":position"
        , [ [0, camera.position], [duration, player.position] ] ],
   ])
yield(self, "otf_animation_finished")

The snippet above would create and play a 1-second duration animation in which the camera zooms in or out from its current zoom level to the "default" 1:1 zoom, and moves from its current position to center on the player. It then waits until that animation is finished.

In pseudo code, the signature of the function is:
play_animation( duration
              , List<field_to_animate, List<time_step, field_value>>)
That is:
  • The first argument is the total duration of the animation
  • The second argument is a list of tracks, containing for each field-value that you want to animate:
    • A string describing the path to that field (e.g. "Camera:zoom")
    • A list of key frames (timestep, value) pairs: i.e. what the value of the field should be at that timestep.
If you have taken a closer look at the Animation editor, you will see that this is very much the IDE transformed into code.

The code creating the animation is as follows:
signal otf_animation_finished()
 
func play_animation(duration, tracks):
    var animation = Animation.new()
    animation.length = duration

    var i = 0
    for track in tracks:
        animation.add_track(Animation.TYPE_VALUE)
        animation.track_set_path(i, track[0])
        for keyf in track[1]:
            animation.track_insert_key(i, keyf[0], keyf[1])
        i += 1

    var player = AnimationPlayer.new()
    player.add_animation("animation", animation)
    self.add_child(player)

    player.play("animation")
    yield(player, "animation_finished")

    self.remove_child(player)
    emit_signal("otf_animation_finished")
First, we define a signal that we can use to communicate to the caller of the function that the "on-the-fly" animation has finished. To distinguish it from the regular "animation_finished" signal from the AnimationPlayer I named it slightly differently.

The function creates a new instance of the AnimationPlayer class and adds a single Animation to it, named "animation". For each of the provided tracks (e.g. "Camera:zoom") a new track is added and the key frames are copied into that track. Note that we have to make sure that each track gets its own internal track in the animation. We do this via the "i" counter.

Finally, we add the player to the current scene, play the animation, wait for it to finish, remove the player from the scene and inform the caller we are done.

As an exercise, you could extract this function and place it into some AutoLoad resource. However, in that case the function will require an additional argument to replace "self" in the function body (since the track-paths are all relative to the scene in which you want to play the animation):
func play_animation(scene, duration, tracks):
    ...
    scene.add_child(player)
    ...
    scene.remove_child(player)
    emit_signal("animation_finished")

No comments:

Post a Comment

Most popular posts in the last month: