角色动画

这是最后一课,我们会使用 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 向量的那一行之后添加如下代码。

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

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

我们提到 Pivot(轴心)可以在动画之上叠加变换。我们可以用下面这行代码使角色在跳跃时产生弧线。把它加在 _physics_process() 的最后。

func _physics_process(delta):
    #...
    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

为小怪制作动画

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

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

打开 Player 场景,选择 AnimationPlayer 节点,然后点击动画 > 管理动画...。点击 float 动画旁边的 将动画复制到剪贴板 按钮(两个小方块)。点击“确定”关闭窗口。

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

接下来,确保底部动画编辑器面板中的自动播放按钮(自动播放)和循环箭头(动画循环)也已打开。这样就行了;所有的怪物现在就都能播放 float 动画了。

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

func initialize(start_position, player_position):
    #...
    $AnimationPlayer.speed_scale = random_speed / min_speed

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

恭喜

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

这是 Player 脚本。

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal movement being very fast
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction,Vector3.UP)
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

这是 Mob 的脚本。

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.speed_scale = random_speed / min_speed

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node