角色动画

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

image0

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

动画编辑器的使用

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

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

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

image1

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

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

image2

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

image3

创建完动画后,就会出现一条时间线,上面的数字代表时间,单位为秒。

image4

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

要实现这个需求,可以单击动画工具栏上对应的“A+”图标和循环箭头。

image5

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

image6

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

image7

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

image8

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

image9

漂浮动画

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

让我们来开始插入关键帧吧。这里,我们要为 Character 节点的平移(translation)和旋转(rotation)做动画。

选中 Character 并单击检查器Translation 旁的钥匙图标。Rotation Degrees 也同样如此操作。

image10

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

image11

单击并拖动这些菱形就可以改变它们的时间。将平移(translation)关键帧移动到 0.2 秒的位置,将旋转(rotation)关键帧移动到 0.1 秒的位置。

image12

单击灰色的时间线并拖动,将时间光标移动到 0.5 秒的位置。在检查器中将 TranslationY 轴设为约 0.65 米,将 Rotation DegreesX 轴设为 8

image13

为这两个属性分别创建一个关键帧,然后在时间线上拖动,将平移的关键帧移动到 0.7 秒。

image14

备注

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

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

单击播放按钮或者按 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.playback_speed = 4
  6. else:
  7. $AnimationPlayer.playback_speed = 1
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. // ...
  4. if (direction != Vector3.Zero)
  5. {
  6. // ...
  7. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
  8. }
  9. else
  10. {
  11. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 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(float delta)
  2. {
  3. // ...
  4. var pivot = GetNode<Spatial>("Pivot");
  5. pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
  6. }

为小怪制作动画

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

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

Open the Player scene, select the animation player node and open the “float” animation. Next, click on Animation > Copy. Then open Mob.tscn and open its animation player. Click Animation > Paste. 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.playback_speed = random_speed / min_speed
  1. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  2. {
  3. // ...
  4. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
  5. }

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

恭喜

在下一部分,我们将快速复习已学到的内容,并为你提供一些继续学习的链接。不过现在,这里是完整的 Player.gdMob.gd,你可以用它们来校对你的代码。

这是 Player 脚本。

GDScriptC#

  1. extends KinematicBody
  2. # Emitted when the player was hit by a mob.
  3. signal hit
  4. # How fast the player moves in meters per second.
  5. export var speed = 14
  6. # The downward acceleration when in the air, in meters per second per second.
  7. export var fall_acceleration = 75
  8. # Vertical impulse applied to the character upon jumping in meters per second.
  9. export var jump_impulse = 20
  10. # Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  11. export var bounce_impulse = 16
  12. var velocity = Vector3.ZERO
  13. func _physics_process(delta):
  14. var direction = Vector3.ZERO
  15. if Input.is_action_pressed("move_right"):
  16. direction.x += 1
  17. if Input.is_action_pressed("move_left"):
  18. direction.x -= 1
  19. if Input.is_action_pressed("move_back"):
  20. direction.z += 1
  21. if Input.is_action_pressed("move_forward"):
  22. direction.z -= 1
  23. if direction != Vector3.ZERO:
  24. direction = direction.normalized()
  25. $Pivot.look_at(translation + direction, Vector3.UP)
  26. $AnimationPlayer.playback_speed = 4
  27. else:
  28. $AnimationPlayer.playback_speed = 1
  29. velocity.x = direction.x * speed
  30. velocity.z = direction.z * speed
  31. # Jumping
  32. if is_on_floor() and Input.is_action_just_pressed("jump"):
  33. velocity.y += jump_impulse
  34. velocity.y -= fall_acceleration * delta
  35. velocity = move_and_slide(velocity, Vector3.UP)
  36. for index in range(get_slide_count()):
  37. var collision = get_slide_collision(index)
  38. if collision.collider.is_in_group("mob"):
  39. var mob = collision.collider
  40. if Vector3.UP.dot(collision.normal) > 0.1:
  41. mob.squash()
  42. velocity.y = bounce_impulse
  43. $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
  44. func die():
  45. emit_signal("hit")
  46. queue_free()
  47. func _on_MobDetector_body_entered(_body):
  48. die()
  1. public class Player : KinematicBody
  2. {
  3. // Emitted when the player was hit by a mob.
  4. [Signal]
  5. public delegate void Hit();
  6. // How fast the player moves in meters per second.
  7. [Export]
  8. public int Speed = 14;
  9. // The downward acceleration when in the air, in meters per second squared.
  10. [Export]
  11. public int FallAcceleration = 75;
  12. // Vertical impulse applied to the character upon jumping in meters per second.
  13. [Export]
  14. public int JumpImpulse = 20;
  15. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  16. [Export]
  17. public int BounceImpulse = 16;
  18. private Vector3 _velocity = Vector3.Zero;
  19. public override void _PhysicsProcess(float delta)
  20. {
  21. var direction = Vector3.Zero;
  22. if (Input.IsActionPressed("move_right"))
  23. {
  24. direction.x += 1f;
  25. }
  26. if (Input.IsActionPressed("move_left"))
  27. {
  28. direction.x -= 1f;
  29. }
  30. if (Input.IsActionPressed("move_back"))
  31. {
  32. direction.z += 1f;
  33. }
  34. if (Input.IsActionPressed("move_forward"))
  35. {
  36. direction.z -= 1f;
  37. }
  38. if (direction != Vector3.Zero)
  39. {
  40. direction = direction.Normalized();
  41. GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
  42. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
  43. }
  44. else
  45. {
  46. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
  47. }
  48. _velocity.x = direction.x * Speed;
  49. _velocity.z = direction.z * Speed;
  50. // Jumping.
  51. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  52. {
  53. _velocity.y += JumpImpulse;
  54. }
  55. _velocity.y -= FallAcceleration * delta;
  56. _velocity = MoveAndSlide(_velocity, Vector3.Up);
  57. for (int index = 0; index < GetSlideCount(); index++)
  58. {
  59. KinematicCollision collision = GetSlideCollision(index);
  60. if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
  61. {
  62. if (Vector3.Up.Dot(collision.Normal) > 0.1f)
  63. {
  64. mob.Squash();
  65. _velocity.y = BounceImpulse;
  66. }
  67. }
  68. }
  69. var pivot = GetNode<Spatial>("Pivot");
  70. pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
  71. }
  72. private void Die()
  73. {
  74. EmitSignal(nameof(Hit));
  75. QueueFree();
  76. }
  77. public void OnMobDetectorBodyEntered(Node body)
  78. {
  79. Die();
  80. }
  81. }

这是 Mob 的脚本。

GDScriptC#

  1. extends KinematicBody
  2. # Emitted when the player jumped on the mob.
  3. signal squashed
  4. # Minimum speed of the mob in meters per second.
  5. export var min_speed = 10
  6. # Maximum speed of the mob in meters per second.
  7. export var max_speed = 18
  8. var velocity = Vector3.ZERO
  9. func _physics_process(_delta):
  10. move_and_slide(velocity)
  11. func initialize(start_position, player_position):
  12. look_at_from_position(start_position, player_position, Vector3.UP)
  13. rotate_y(rand_range(-PI / 4, PI / 4))
  14. var random_speed = rand_range(min_speed, max_speed)
  15. velocity = Vector3.FORWARD * random_speed
  16. velocity = velocity.rotated(Vector3.UP, rotation.y)
  17. $AnimationPlayer.playback_speed = random_speed / min_speed
  18. func squash():
  19. emit_signal("squashed")
  20. queue_free()
  21. func _on_VisibilityNotifier_screen_exited():
  22. queue_free()
  1. public class Mob : KinematicBody
  2. {
  3. // Emitted when the played jumped on the mob.
  4. [Signal]
  5. public delegate void Squashed();
  6. // Minimum speed of the mob in meters per second
  7. [Export]
  8. public int MinSpeed = 10;
  9. // Maximum speed of the mob in meters per second
  10. [Export]
  11. public int MaxSpeed = 18;
  12. private Vector3 _velocity = Vector3.Zero;
  13. public override void _PhysicsProcess(float delta)
  14. {
  15. MoveAndSlide(_velocity);
  16. }
  17. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  18. {
  19. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  20. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  21. float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
  22. _velocity = Vector3.Forward * randomSpeed;
  23. _velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
  24. GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
  25. }
  26. public void Squash()
  27. {
  28. EmitSignal(nameof(Squashed));
  29. QueueFree();
  30. }
  31. public void OnVisibilityNotifierScreenExited()
  32. {
  33. QueueFree();
  34. }
  35. }