角色动画

这是最后一课,我们会使用 Godot 的内置动画工具制作角色的浮动和拍打动画。你会学到如何在编辑器中设计动画,以及如何使用代码让游戏变得活灵活现。

image0

我们将会开始介绍动画编辑器的使用。

动画编辑器的使用

该引擎自带的工具可以在编辑器中编写动画。然后你可以在运行时使用代码来播放和控制它们。

打开玩家场景,选中 Player 节点,然后添加一个 AnimationPlayer 节点。

动画停靠面板就会出现在底部面板中。

image1

它的特点是顶部有一个工具栏和动画下拉菜单,中间有一个轨道编辑器,目前是空的,底部有过滤、捕捉和缩放选项。

让我们来创建一个动画。请点击动画 -> 新建

image2

将动画命名为“float”(漂浮)。

image3

创建动画后,将显示时间轴,其中数字表示以秒为单位的时间。

image4

我们希望让这个动画在游戏开始时自动开始播放,而且还应该循环播放。

要执行此操作,你可以单击动画工具栏上的自动播放按钮(自动播放)和循环箭头。

image5

你还可以单击右上角的图钉图标,将动画编辑器进行固定。这样它就不会在你点击视口取消选择节点时折叠。

image6

在面板右上角将动画的时长设为 1.2 秒。

image7

你应该看到灰色带子变宽了一点。它显示动画的开始和结束,垂直蓝线是你的时间光标。

image8

单击并拖拽右下角的滑动条,即可将时间线进行缩放。

image9

漂浮动画

使用动画播放器节点,你可以对所需任意数量的节点的大多数属性做动画。请注意检查器中属性旁的钥匙图标。在上面单击就可以创建一个关键帧,即对应属性的一对时间与值。关键帧会被插入到时间线上的时间光标处。

让我们来开始插入帧吧。这里,我们要为 Character 节点的位置(position)和旋转(rotation)做动画。

选中 Character 并在检查器中展开 Transform 栏。单击 PositionRotation 旁的钥匙图标。

image10

../../_images/curves.webp

对于本教程,我们只创建默认选择 RESET(重置)轨道

编辑器中会出现两个轨道,各有一个代表关键帧的菱形图标。

image11

你可以在菱形滑块上单击并拖动,以移动它们的时间。将位置(position )帧移动到 0.3 秒处,将旋转(rotation )帧移动到 0.1 秒处。

image12

在灰色的时间线上单击并拖拽,将时间光标移动至 0.5 秒位置。

timeline_05_click

检查器 中,将 PositionY 轴设置为 0.65 米,将 RotationX 轴设置为 8

如果你在检查器面板中没有看到属性,请在场景面板中再次点击 Character 节点。

image13

为这两个属性分别创建一个关键帧

second_keys_both

现在开始在时间线上拖动,将位置(position)的关键帧移动到 0.7 秒。

image14

备注

关于动画原理的讲解已经超出了本教程的范围。请注意,你不想均匀地分配时间和空间。取而代之的是,动画师使用时间和间隔,这两个核心动画原则。你希望让它们存在一定的偏移,在角色的运动中产生对比,以使他们感觉生动。

将时间光标移动到动画结尾,即 1.2 秒。将 Y 平移量设为约 0.35、X 旋转量设为 -9 度。再次为这两个属性添加帧。

animation_final_keyframes

单击播放按钮或者按 Shift + D 即可预览结果。单击停止按钮或者按 S 即可停止播放。

image15

你可以看到引擎通过在关键帧之间插值来生成连续动画。不过目前,生成的动作非常机械。这是因为默认的插值是线性的,会导致持续的过渡,并且与现实世界中生物的移动方式不同。

我们可以使用缓动曲线来控制关键帧之间的过渡。

单击并拖拽,框选时间线上的前两个帧。

image16

可以在检查器中同时编辑这两个帧的属性,其中就有一个属性叫做 Easing(缓动)。

image17

单击并拖动曲线,把它往左拉。这样就会让他实现缓出,也就是说,一开始变得快,然后时间光标越接近下一个关键帧就变得越慢。

image18

再次播放动画以查看差异。前半部分应该已经感觉有点弹性了。

将缓动效果应用于旋转轨迹中的第二个关键帧。

image19

对第二个平移关键帧执行相反操作,将其拖动到右侧。

image20

你的动画应该类似这样。

image21

备注

每一帧,动画都会去更新被动画的节点的属性,覆盖掉初始值。如果我们直接对 Player 节点做动画,就没法使用代码来移动它了。这就是 Pivot 节点的用处:尽管我们为 Character 做了动画,我们还是可以在此动画之上,再通过代码来移动并旋转 Pivot

如果你运行游戏,玩家的生物就会漂浮起来!

如果这个生物离地面太近了,你可以将 Pivot 向上移动,达成偏移的目的。

使用代码控制动画

我们可以使用代码来根据玩家的输入控制动画的播放。让我们在角色移动时修改动画的速度吧。

点击 Player 旁的脚本图标打开其脚本。

image22

_physics_process() 中检查 direction 向量的那一行之后添加如下代码。

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. }

这段代码的作用是让玩家在移动时将播放速度乘以 4。在停止移动时将其恢复原状。

我们提到 Pivot(轴心)可以在动画之上叠加变换。我们可以用下面这行代码使角色在跳跃时产生弧线。把它加在 _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. }

为小怪制作动画

在 Godot 中还有一个很好的动画技巧:只要你使用类似的节点结构,你就可以把它们复制到不同的场景中。

例如,MobPlayer 场景都有 PivotCharacter 节点,所以我们可以在它们之间复用动画。

这个应用程序是一个开源项目。要报告bug或者做出贡献,请前往该应用程序的源代码仓库:`GitHub repository`_.

然后打开 mob.tscn ,创建一个 AnimationPlayer 子节点并且选中它。点击 动画 > 管理动画 ,然后 新建库 ,你应该看到信息 “将创建全局库”。文本处留白然后点击OK。点击 粘贴 图标(剪贴板)然后它应当出现在窗口上。点击OK来关闭窗口。

Next, make sure that the autoplay button (自动播放) 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.

我们可以根据生物的 random_speed 来更改播放速度。打开 Mob 的脚本,在 initialize() 函数的末尾添加下面这行代码。

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. }

这样,你就完成了你第一个完整 3D 游戏的编码。

恭喜

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.

这是 Player 脚本。

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. }

这是 Mob 的脚本。

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. }