AnimationPlayer-based scene with @tool running animation in editor but not in the game

Hi everyone, I would appreciate some help with an issue that I’m having. I’ll try to add as much information to this post as possible.

For background context, I’m learning Godot by making a platformer game in Godot 4.4, and I’m trying to create a special scene with exported variables as a way to automate the creation of moving platforms. To accomplish this, I am creating a new scene called PlatformMover that extends AnimationPlayer, with the @tool annotation so that I can generate the required animation in the editor as well. This scene is designed to be added as a child node to Platform scenes in my game.

The issue that I’m facing is that even though my scene seems to work flawlessly in the editor and my platform can move correctly, when I’m running the game my platform remains stationary and does not move at all.

Here is the code that I have written for my exported variables:

@tool
class_name PlatformMover extends AnimationPlayer
# Value will be initialized in _ready
var animation_library: AnimationLibrary

@export var move_to_y: int:
	set(val):
		move_to_y = val
		if Engine.is_editor_hint():
			remake_animation()
@export var move_secs: float = 0:
	set(val):
		move_secs = val
		if Engine.is_editor_hint():
			remake_animation()
@export var wait_secs: float = 0:
	set(val):
		wait_secs = val
		if Engine.is_editor_hint():
			remake_animation()

move_to_y is the final y-position of the platform after moving. move_secs is the number of seconds the platform will use to move in a single direction (up or down). wait_secs is the number of seconds that a platform will wait after reaching its configured start or end position before moving again.

In addition to this, there is a button to reload the animation, however I don’t think that this is relevant to my issue:

# Create a button to reload the animation settings. Because this was created based on the parent
# position, if the parent node is moved after the animation settings were created, this can
# change the parent's location back to the one before it was moved. So with this handy button we
# can reload the start position by regenerating the animation
@export_tool_button("Reload Start Position") var reload = reload_animation_settings.bind()

func reload_animation_settings() -> void:
	remake_animation()

Based on the variables move_to_y, move_secs and wait_secs, the intention of my script is to create an Animation and dynamically generate the animation keys that will move the platform based on the desired parameters by updating the position property of the parent Platform scene. This is the code of the function that creates the animation:

func create_moving_animation() -> Animation:
	var parent = get_parent()
	
	var animation = Animation.new()
#	Create the track for the movement animation
	var track_index = animation.add_track(Animation.TYPE_VALUE)
#	Set the property on the node that the animation will update. In this
#	case, since we are updating the position of the parent node, we need
#	to use the string "<parent_node_path>:position"
	#var track_path = "%s:position" % parent.get_path()
	var track_path = "../:position"
	animation.track_set_path(track_index, track_path)
	
#	Create variables for the various positions and timings that we will be adding
	var start_position = parent.position
	var end_position = start_position
	end_position.y = move_to_y
	var rolling_time_counter = 0.0
	
#	Insert keyframes
	animation.track_insert_key(track_index, 0.0, start_position)
#	If wait, then set a new key that allows the platform to wait at the start position
	if wait_secs > 0:
		rolling_time_counter += wait_secs
		animation.track_insert_key(track_index, rolling_time_counter, start_position)
	rolling_time_counter += move_secs
	animation.track_insert_key(track_index, rolling_time_counter, end_position)
#	If wait, then set a new key that allows the platform to wait at the end position
	if wait_secs > 0:
		rolling_time_counter += wait_secs
		animation.track_insert_key(track_index, rolling_time_counter, end_position)
	rolling_time_counter += move_secs
	animation.track_insert_key(track_index, rolling_time_counter, start_position)

	animation.length = rolling_time_counter
	animation.loop_mode = Animation.LoopMode.LOOP_LINEAR

#     Debugging print statements
	print("move_to_y is %s" % move_to_y)
	print("move_secs is %s" % move_secs)
	print("wait_secs is %s" % wait_secs)
	for i in range(0, animation.track_get_key_count(track_index)):
		print("key time is %s" % animation.track_get_key_time(track_index, i))
		print("Key value is %s" % animation.track_get_key_value(track_index, i))
	return animation

As shown in the code, I have also some print statements for debugging purposes. When I run the game, the following statements are printed:

move_to_y is -65
move_secs is 1.5
wait_secs is 1.0
key time is 0.0
Key value is (2514.0, 136.018)
key time is 1.0
Key value is (2514.0, 136.018)
key time is 2.5
Key value is (2514.0, -65.0)
key time is 3.5
Key value is (2514.0, -65.0)
key time is 5.0
Key value is (2514.0, 136.018)

Based on these statements, it appears to me that the animation keys have been correctly generated and added to the animation.

After the animation is created, it is added to my AnimationLibrary with the following function:

func remake_animation() -> void:
	print("Remaking")
	var animation = create_moving_animation()
	animation_library.remove_animation("move")
	animation_library.add_animation("move", animation)
	#if not Engine.is_editor_hint():
		#play("animation_lib/move")
		#print("Played!")
	print("Remade")

In runtime, the “Remaking” and “Remade” statements are printed once, indicating that this function has run exactly once. The play function is commented out here because it will be called in the _ready() function instead.

This remake_animation() function is automatically called in the setters for my exported variables. When I’m using the editor, I can see an animation generated under the Animation tool at the bottom of the screen, and I can play the animation and see it move my platform, as expected.

Finally, in order to run this code in both the editor and during the game’s runtime, I have the following code in the _ready() function here:

func _ready() -> void:
	animation_library = AnimationLibrary.new()
	if not has_animation_library("animation_lib"):
		add_animation_library("animation_lib", animation_library)
	remake_animation()
	
	if not Engine.is_editor_hint():
		remake_animation()
		if not is_playing():
			play("animation_lib/move")
			print("Played")

When i run the game, I see the “Played” statement printed, indicating that my if branch has been entered and executed as expected.

An additional debugging step I performed is to implement the _process() function to print debugging statements:

func _process(delta: float) -> void:
	var animation = animation_library.get_animation("move")
	var parent = get_parent()
	print("Parent position: %s" % parent.position)
	print("is playing: %s" % is_playing())
	print("Current animation: %s" % current_animation)
	print("Assigned animation: %s" % assigned_animation)
	print("Animation position %s" % current_animation_position)

Here is a sample of the print statements generated during runtime:

Parent position: (2514.0, 172.0)
is playing: true
Current animation: animation_lib/move
Assigned animation: animation_lib/move
Animation position 0.41515233333326

Parent position: (2514.0, 172.0)
is playing: true
Current animation: animation_lib/move
Assigned animation: animation_lib/move
Animation position 0.42654066666659

Parent position: (2514.0, 172.0)
is playing: true
Current animation: animation_lib/move
Assigned animation: animation_lib/move
Animation position 0.42728666666659

Parent position: (2514.0, 172.0)
is playing: true
Current animation: animation_lib/move
Assigned animation: animation_lib/move
Animation position 0.42798654166979

The animation seems to be playing correctly because of the following reasons:

  1. The is_playing() function returns true
  2. The current and assigned animation names seem to match the animation I generated
  3. The animation position is continually increasing, and even loops back to 0 after 5 seconds, which is the length of this generated animation

However, even though the animation is playing, the parent position vector is still remaining the same.

Finally, for convenience, here is the entire script code:

@tool
class_name PlatformMover extends AnimationPlayer
var animation_library: AnimationLibrary

@export var move_to_y: int:
	set(val):
		move_to_y = val
		if Engine.is_editor_hint():
			remake_animation()
@export var move_secs: float = 0:
	set(val):
		move_secs = val
		if Engine.is_editor_hint():
			remake_animation()
@export var wait_secs: float = 0:
	set(val):
		wait_secs = val
		if Engine.is_editor_hint():
			remake_animation()

# Create a button to reload the animation settings. Because this was created based on the parent
# position, if the parent node is moved after the animation settings were created, this can
# change the parent's location back to the one before it was moved. So with this handy button we
# can reload the start position by regenerating the animation
@export_tool_button("Reload Start Position") var reload = reload_animation_settings.bind()

func reload_animation_settings() -> void:
	remake_animation()

func _process(delta: float) -> void:
	var animation = animation_library.get_animation("move")
	var parent = get_parent()
	print("Parent position: %s" % parent.position)
	print("is playing: %s" % is_playing())
	print("Current animation: %s" % current_animation)
	print("Assigned animation: %s" % assigned_animation)
	print("Animation position %s\n" % current_animation_position)

func _ready() -> void:
	animation_library = AnimationLibrary.new()
	if not has_animation_library("animation_lib"):
		add_animation_library("animation_lib", animation_library)
	remake_animation()
	
	if not Engine.is_editor_hint():
		remake_animation()
		if not is_playing():
			play("animation_lib/move")
			print("Played")

func remake_animation() -> void:
	print("Remaking")
	var animation = create_moving_animation()
	animation_library.remove_animation("move")
	animation_library.add_animation("move", animation)
	#if not Engine.is_editor_hint():
		#play("animation_lib/move")
		#print("Played!")
	print("Remade")

func create_moving_animation() -> Animation:
	var parent = get_parent()
	
	var animation = Animation.new()
#	Create the track for the movement animation
	var track_index = animation.add_track(Animation.TYPE_VALUE)
#	Set the property on the node that the animation will update. In this
#	case, since we are updating the position of the parent node, we need
#	to use the string "<parent_node_path>:position"
	#var track_path = "%s:position" % parent.get_path()
	var track_path = "../:position"
	animation.track_set_path(track_index, track_path)
	
#	Create variables for the various positions and timings that we will be adding
	var start_position = parent.position
	var end_position = start_position
	end_position.y = move_to_y
	var rolling_time_counter = 0.0
	
#	Insert keyframes
	animation.track_insert_key(track_index, 0.0, start_position)
#	If wait, then set a new key that allows the platform to wait at the start position
	if wait_secs > 0:
		rolling_time_counter += wait_secs
		animation.track_insert_key(track_index, rolling_time_counter, start_position)
	rolling_time_counter += move_secs
	animation.track_insert_key(track_index, rolling_time_counter, end_position)
#	If wait, then set a new key that allows the platform to wait at the end position
	if wait_secs > 0:
		rolling_time_counter += wait_secs
		animation.track_insert_key(track_index, rolling_time_counter, end_position)
	rolling_time_counter += move_secs
	animation.track_insert_key(track_index, rolling_time_counter, start_position)

	animation.length = rolling_time_counter
	animation.loop_mode = Animation.LoopMode.LOOP_LINEAR

#     Debugging print statements
	print("move_to_y is %s" % move_to_y)
	print("move_secs is %s" % move_secs)
	print("wait_secs is %s" % wait_secs)
	for i in range(0, animation.track_get_key_count(track_index)):
		print("key time is %s" % animation.track_get_key_time(track_index, i))
		print("Key value is %s" % animation.track_get_key_value(track_index, i))
	return animation

At this point, I have debugged this problem to the best of my ability and I do not know where else in my code to look for issues. I have also tried searching for similar issues but I could not find anything similar to mine. Most of the issues I found were due to multiple play calls overwriting and restarting the current animation, making it seem like the animation is not playing, but it does not seem to apply to me as I have the play function only once in my code, and it is executed in _ready() so it should be executed only once.

I would appreciate any help on resolving this issue. Thank you!

When I run your code I get:

E 0:00:00:326   animation_player.gd:55 @ remake_animation(): Animation not found: move.
  <C++ Error>   Condition "!animations.has(p_name)" is true.
  <C++ Source>  scene/resources/animation_library.cpp:70 @ remove_animation()
  <Stack Trace> animation_player.gd:55 @ remake_animation()
                animation_player.gd:43 @ _ready()

Errors are shown in the debug panel at the bottom of the editor: Debugger panel — Godot Engine (stable) documentation in English

This can be fixed by first checking if the animation exists before deleting it:

if (animation_library.has_animation("move")):
	animation_library.remove_animation("move")

It might be worth looking at looping tweens instead of creating animations at runtime.

1 Like

When you use a @tool script, any change you do to the nodes with it will be as if you did it from the editor itself.

This piece of code will not work as you expect at runtime because the AnimationPlayer already has a "animation_lib" AnimationLibrary added:

It was added when you opened the scene in the editor. After that, anything you do with your animation_library variable at runtime has no effect because it’s using the AnimationLibrary added when it ran in the editor.

So that’s probably one issue but I do not know why the animation runs fine in the editor and not at runtime.

Maybe it’s because you are using an StaticBody2D and you need to set the AnimationMixer.callback_mode_process in your AnimationPlayer to Physics as explained in the StaticBody2D description.

You should probably use an AnimatableBody2D instead.

Thank you, it turns out that it was indeed the animation_library issue that you pointed out. Fixing this issue allowed the animation to run correctly at runtime.

For reference, this is what I changed my code to:

func _ready() -> void:
	set_process_callback(AnimationPlayer.ANIMATION_PROCESS_PHYSICS)
	if not has_animation_library("animation_lib"):
		animation_library = AnimationLibrary.new()
		add_animation_library("animation_lib", animation_library)
	else:
		animation_library = get_animation_library("animation_lib")
	remake_animation()
	
	if not Engine.is_editor_hint():
		if not is_playing():
			play("animation_lib/move")
			print("Played")

func remake_animation() -> void:
	print("Remaking")
	var animation = create_moving_animation()
	animation_library = get_animation_library("animation_lib")
	if animation_library.has_animation("move"):
		animation_library.remove_animation("move")
	animation_library.add_animation("move", animation)
	print("Remade")

I’ll look into using AnimatableBody2D as well

Thanks for the tip, I was not aware of this panel.

I’ll also look into your suggestion of using Tween, it does seem like a good choice as the docs say that it’s more lightweight than an AnimationPlayer.