Character animation

In this final lesson, we’ll use Godot’s built-in animation tools to make our characters float and flap. You’ll learn to design animations in the editor and use code to make your game feel alive.

image0

We’ll start with an introduction to using the animation editor.

Using the animation editor

The engine comes with tools to author animations in the editor. You can then use the code to play and control them at runtime.

Open the player scene, select the Player node, and add an AnimationPlayer node.

The Animation dock appears in the bottom panel.

image1

It features a toolbar and the animation drop-down menu at the top, a track editor in the middle that’s currently empty, and filter, snap, and zoom options at the bottom.

Let’s create an animation. Click on Animation -> New.

image2

Name the animation “float”.

image3

Once you’ve created the animation, the timeline appears with numbers representing time in seconds.

image4

We want the animation to start playback automatically at the start of the game. Also, it should loop.

To do so, you can click the autoplay button (Autoplay) in the animation toolbar and the looping arrows, respectively.

image5

You can also pin the animation editor by clicking the pin icon in the top-right. This prevents it from folding when you click on the viewport and deselect the nodes.

image6

Set the animation duration to 1.2 seconds in the top-right of the dock.

image7

You should see the gray ribbon widen a bit. It shows you the start and end of your animation and the vertical blue line is your time cursor.

image8

You can click and drag the slider in the bottom-right to zoom in and out of the timeline.

image9

The float animation

With the animation player node, you can animate most properties on as many nodes as you need. Notice the key icon next to properties in the Inspector. You can click any of them to create a keyframe, a time and value pair for the corresponding property. The keyframe gets inserted where your time cursor is in the timeline.

Let’s insert our first keys. Here, we will animate both the position and the rotation of the Character node.

Select the Character and in the Inspector expand the Transform section. Click the key icon next to Position, and Rotation.

image10

../../_images/curves.webp

For this tutorial, just create RESET Track(s) which is the default choice

Two tracks appear in the editor with a diamond icon representing each keyframe.

image11

You can click and drag on the diamonds to move them in time. Move the position key to 0.3 seconds and the rotation key to 0.1 seconds.

image12

Move the time cursor to 0.5 seconds by clicking and dragging on the gray timeline.

timeline_05_click

In the Inspector, set the Position‘s Y axis to 0.65 meters and the RotationX axis to 8.

If you don’t see the properties in the Inspector panel, first click on the Character node again in the Scene dock.

image13

Create a keyframe for both properties

second_keys_both

Now, move the position keyframe to 0.7 seconds by dragging it on the timeline.

image14

Note

A lecture on the principles of animation is beyond the scope of this tutorial. Just note that you don’t want to time and space everything evenly. Instead, animators play with timing and spacing, two core animation principles. You want to offset and contrast in your character’s motion to make them feel alive.

Move the time cursor to the end of the animation, at 1.2 seconds. Set the Y position to about 0.35 and the X rotation to -9 degrees. Once again, create a key for both properties.

animation_final_keyframes

You can preview the result by clicking the play button or pressing Shift + D. Click the stop button or press S to stop playback.

image15

You can see that the engine interpolates between your keyframes to produce a continuous animation. At the moment, though, the motion feels very robotic. This is because the default interpolation is linear, causing constant transitions, unlike how living things move in the real world.

We can control the transition between keyframes using easing curves.

Click and drag around the first two keys in the timeline to box select them.

image16

You can edit the properties of both keys simultaneously in the Inspector, where you can see an Easing property.

image17

Click and drag on the curve, pulling it towards the left. This will make it ease-out, that is to say, transition fast initially and slow down as the time cursor reaches the next keyframe.

image18

Play the animation again to see the difference. The first half should already feel a bit bouncier.

Apply an ease-out to the second keyframe in the rotation track.

image19

Do the opposite for the second position keyframe, dragging it to the right.

image20

Your animation should look something like this.

image21

Note

Animations update the properties of the animated nodes every frame, overriding initial values. If we directly animated the Player node, it would prevent us from moving it in code. This is where the Pivot node comes in handy: even though we animated the Character, we can still move and rotate the Pivot and layer changes on top of the animation in a script.

If you play the game, the player’s creature will now float!

If the creature is a little too close to the floor, you can move the Pivot up to offset it.

Controlling the animation in code

We can use code to control the animation playback based on the player’s input. Let’s change the animation speed when the character is moving.

Open the Player‘s script by clicking the script icon next to it.

image22

In _physics_process(), after the line where we check the direction vector, add the following code.

GDScriptC#

  1. func _physics_process(delta):
  2. #...
  3. if direction != Vector3.ZERO:
  4. #...
  5. $AnimationPlayer.speed_scale = 4
  6. else:
  7. $AnimationPlayer.speed_scale = 1
  1. public override void _PhysicsProcess(double delta)
  2. {
  3. // ...
  4. if (direction != Vector3.Zero)
  5. {
  6. // ...
  7. GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 4;
  8. }
  9. else
  10. {
  11. GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 1;
  12. }
  13. }

This code makes it so when the player moves, we multiply the playback speed by 4. When they stop, we reset it to normal.

We mentioned that the Pivot could layer transforms on top of the animation. We can make the character arc when jumping using the following line of code. Add it at the end of _physics_process().

GDScriptC#

  1. func _physics_process(delta):
  2. #...
  3. $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
  1. public override void _PhysicsProcess(double delta)
  2. {
  3. // ...
  4. var pivot = GetNode<Node3D>("Pivot");
  5. pivot.Rotation = new Vector3(Mathf.Pi / 6.0f * Velocity.Y / JumpImpulse, pivot.Rotation.Y, pivot.Rotation.Z);
  6. }

Animating the mobs

Here’s another nice trick with animations in Godot: as long as you use a similar node structure, you can copy them to different scenes.

For example, both the Mob and the Player scenes have a Pivot and a Character node, so we can reuse animations between them.

Open the Player scene, select the AnimationPlayer node and then click on Animation > Manage Animations…. Click the Copy animation to clipboard button (two small squares) alongside the float animation. Click OK to close the window.

Then open mob.tscn, create an AnimationPlayer child node and select it. Click Animation > Manage Animations, then Add Library. You should see the message “Global library will be created.” Leave the text field blank and click OK. Click the Paste icon (clipboard) and it should appear in the window. Click OK to close the window.

Next, make sure that the autoplay button (Autoplay) and the looping arrows (Animation looping) are also turned on in the animation editor in the bottom panel. That’s it; all monsters will now play the float animation.

We can change the playback speed based on the creature’s random_speed. Open the Mob‘s script and at the end of the initialize() function, add the following line.

GDScriptC#

  1. func initialize(start_position, player_position):
  2. #...
  3. $AnimationPlayer.speed_scale = random_speed / min_speed
  1. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  2. {
  3. // ...
  4. GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = randomSpeed / MinSpeed;
  5. }

And with that, you finished coding your first complete 3D game.

Congratulations!

In the next part, we’ll quickly recap what you learned and give you some links to keep learning more. But for now, here are the complete player.gd and mob.gd so you can check your code against them.

Here’s the Player script.

GDScriptC#

  1. extends CharacterBody3D
  2. signal hit
  3. # How fast the player moves in meters per second.
  4. @export var speed = 14
  5. # The downward acceleration while in the air, in meters per second squared.
  6. @export var fall_acceleration = 75
  7. # Vertical impulse applied to the character upon jumping in meters per second.
  8. @export var jump_impulse = 20
  9. # Vertical impulse applied to the character upon bouncing over a mob
  10. # in meters per second.
  11. @export var bounce_impulse = 16
  12. var target_velocity = Vector3.ZERO
  13. func _physics_process(delta):
  14. # We create a local variable to store the input direction
  15. var direction = Vector3.ZERO
  16. # We check for each move input and update the direction accordingly
  17. if Input.is_action_pressed("move_right"):
  18. direction.x = direction.x + 1
  19. if Input.is_action_pressed("move_left"):
  20. direction.x = direction.x - 1
  21. if Input.is_action_pressed("move_back"):
  22. # Notice how we are working with the vector's x and z axes.
  23. # In 3D, the XZ plane is the ground plane.
  24. direction.z = direction.z + 1
  25. if Input.is_action_pressed("move_forward"):
  26. direction.z = direction.z - 1
  27. # Prevent diagonal movement being very fast
  28. if direction != Vector3.ZERO:
  29. direction = direction.normalized()
  30. $Pivot.look_at(position + direction,Vector3.UP)
  31. $AnimationPlayer.speed_scale = 4
  32. else:
  33. $AnimationPlayer.speed_scale = 1
  34. # Ground Velocity
  35. target_velocity.x = direction.x * speed
  36. target_velocity.z = direction.z * speed
  37. # Vertical Velocity
  38. if not is_on_floor(): # If in the air, fall towards the floor
  39. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  40. # Jumping.
  41. if is_on_floor() and Input.is_action_just_pressed("jump"):
  42. target_velocity.y = jump_impulse
  43. # Iterate through all collisions that occurred this frame
  44. # in C this would be for(int i = 0; i < collisions.Count; i++)
  45. for index in range(get_slide_collision_count()):
  46. # We get one of the collisions with the player
  47. var collision = get_slide_collision(index)
  48. # If the collision is with ground
  49. if collision.get_collider() == null:
  50. continue
  51. # If the collider is with a mob
  52. if collision.get_collider().is_in_group("mob"):
  53. var mob = collision.get_collider()
  54. # we check that we are hitting it from above.
  55. if Vector3.UP.dot(collision.get_normal()) > 0.1:
  56. # If so, we squash it and bounce.
  57. mob.squash()
  58. target_velocity.y = bounce_impulse
  59. # Prevent further duplicate calls.
  60. break
  61. # Moving the Character
  62. velocity = target_velocity
  63. move_and_slide()
  64. $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
  65. # And this function at the bottom.
  66. func die():
  67. hit.emit()
  68. queue_free()
  69. func _on_mob_detector_body_entered(body):
  70. die()
  1. using Godot;
  2. public partial class Player : CharacterBody3D
  3. {
  4. // Emitted when the player was hit by a mob.
  5. [Signal]
  6. public delegate void HitEventHandler();
  7. // How fast the player moves in meters per second.
  8. [Export]
  9. public int Speed { get; set; } = 14;
  10. // The downward acceleration when in the air, in meters per second squared.
  11. [Export]
  12. public int FallAcceleration { get; set; } = 75;
  13. // Vertical impulse applied to the character upon jumping in meters per second.
  14. [Export]
  15. public int JumpImpulse { get; set; } = 20;
  16. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  17. [Export]
  18. public int BounceImpulse { get; set; } = 16;
  19. private Vector3 _targetVelocity = Vector3.Zero;
  20. public override void _PhysicsProcess(double delta)
  21. {
  22. // We create a local variable to store the input direction.
  23. var direction = Vector3.Zero;
  24. // We check for each move input and update the direction accordingly.
  25. if (Input.IsActionPressed("move_right"))
  26. {
  27. direction.X += 1.0f;
  28. }
  29. if (Input.IsActionPressed("move_left"))
  30. {
  31. direction.X -= 1.0f;
  32. }
  33. if (Input.IsActionPressed("move_back"))
  34. {
  35. // Notice how we are working with the vector's X and Z axes.
  36. // In 3D, the XZ plane is the ground plane.
  37. direction.Z += 1.0f;
  38. }
  39. if (Input.IsActionPressed("move_forward"))
  40. {
  41. direction.Z -= 1.0f;
  42. }
  43. // Prevent diagonal movement being very fast.
  44. if (direction != Vector3.Zero)
  45. {
  46. direction = direction.Normalized();
  47. GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
  48. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
  49. }
  50. else
  51. {
  52. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
  53. }
  54. // Ground velocity
  55. _targetVelocity.X = direction.X * Speed;
  56. _targetVelocity.Z = direction.Z * Speed;
  57. // Vertical velocity
  58. if (!IsOnFloor())
  59. {
  60. _targetVelocity.Y -= FallAcceleration * (float)delta;
  61. }
  62. // Jumping.
  63. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  64. {
  65. _targetVelocity.Y += JumpImpulse;
  66. }
  67. // Iterate through all collisions that occurred this frame.
  68. for (int index = 0; index < GetSlideCollisionCount(); index++)
  69. {
  70. // We get one of the collisions with the player.
  71. KinematicCollision3D collision = GetSlideCollision(index);
  72. // If the collision is with a mob.
  73. if (collision.GetCollider() is Mob mob)
  74. {
  75. // We check that we are hitting it from above.
  76. if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
  77. {
  78. // If so, we squash it and bounce.
  79. mob.Squash();
  80. _targetVelocity.Y = BounceImpulse;
  81. // Prevent further duplicate calls.
  82. break;
  83. }
  84. }
  85. }
  86. // Moving the character
  87. Velocity = _targetVelocity;
  88. MoveAndSlide();
  89. var pivot = GetNode<Node3D>("Pivot");
  90. pivot.Rotation = new Vector3(Mathf.Pi / 6.0f * Velocity.Y / JumpImpulse, pivot.Rotation.Y, pivot.Rotation.Z);
  91. }
  92. private void Die()
  93. {
  94. EmitSignal(SignalName.Hit);
  95. QueueFree();
  96. }
  97. private void OnMobDetectorBodyEntered(Node body)
  98. {
  99. Die();
  100. }
  101. }

And the Mob‘s script.

GDScriptC#

  1. extends CharacterBody3D
  2. # Minimum speed of the mob in meters per second.
  3. @export var min_speed = 10
  4. # Maximum speed of the mob in meters per second.
  5. @export var max_speed = 18
  6. # Emitted when the player jumped on the mob
  7. signal squashed
  8. func _physics_process(_delta):
  9. move_and_slide()
  10. # This function will be called from the Main scene.
  11. func initialize(start_position, player_position):
  12. # We position the mob by placing it at start_position
  13. # and rotate it towards player_position, so it looks at the player.
  14. look_at_from_position(start_position, player_position, Vector3.UP)
  15. # Rotate this mob randomly within range of -45 and +45 degrees,
  16. # so that it doesn't move directly towards the player.
  17. rotate_y(randf_range(-PI / 4, PI / 4))
  18. # We calculate a random speed (integer)
  19. var random_speed = randi_range(min_speed, max_speed)
  20. # We calculate a forward velocity that represents the speed.
  21. velocity = Vector3.FORWARD * random_speed
  22. # We then rotate the velocity vector based on the mob's Y rotation
  23. # in order to move in the direction the mob is looking.
  24. velocity = velocity.rotated(Vector3.UP, rotation.y)
  25. $AnimationPlayer.speed_scale = random_speed / min_speed
  26. func _on_visible_on_screen_notifier_3d_screen_exited():
  27. queue_free()
  28. func squash():
  29. squashed.emit()
  30. queue_free() # Destroy this node
  1. using Godot;
  2. public partial class Mob : CharacterBody3D
  3. {
  4. // Emitted when the played jumped on the mob.
  5. [Signal]
  6. public delegate void SquashedEventHandler();
  7. // Minimum speed of the mob in meters per second
  8. [Export]
  9. public int MinSpeed { get; set; } = 10;
  10. // Maximum speed of the mob in meters per second
  11. [Export]
  12. public int MaxSpeed { get; set; } = 18;
  13. public override void _PhysicsProcess(double delta)
  14. {
  15. MoveAndSlide();
  16. }
  17. // This function will be called from the Main scene.
  18. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  19. {
  20. // We position the mob by placing it at startPosition
  21. // and rotate it towards playerPosition, so it looks at the player.
  22. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  23. // Rotate this mob randomly within range of -45 and +45 degrees,
  24. // so that it doesn't move directly towards the player.
  25. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  26. // We calculate a random speed (integer).
  27. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  28. // We calculate a forward velocity that represents the speed.
  29. Velocity = Vector3.Forward * randomSpeed;
  30. // We then rotate the velocity vector based on the mob's Y rotation
  31. // in order to move in the direction the mob is looking.
  32. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  33. GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = randomSpeed / MinSpeed;
  34. }
  35. public void Squash()
  36. {
  37. EmitSignal(SignalName.Squashed);
  38. QueueFree(); // Destroy this node
  39. }
  40. private void OnVisibilityNotifierScreenExited()
  41. {
  42. QueueFree();
  43. }
  44. }

User-contributed notes

Please read the User-contributed notes policy before submitting a comment.

Previous Next