Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

角色動畫

這是最後一課,我們會使用 Godot 的內建動畫工具製作角色的浮動和拍打動畫。你會學到如何在編輯器中設計動畫,以及如何使用程式碼讓遊戲變得活靈活現。

image0

建置系統簡介

執行編輯器

該引擎自帶的工具可以在編輯器中編寫動畫。然後你可以在運作時使用程式碼來播放和控制它們。

打開玩家場景,選中 Player 節點,然後新增一個 AnimationPlayer 節點。

*動畫*停靠面板就會出現在底部面板中。

image1

它的特點是頂部有一個工具列和動畫下拉式功能表,中間有一個軌道編輯器,目前是空的,底部有篩選、捕捉和縮放選項。

讓我們來建立一個動畫。請點擊*動畫 -> 新建*。

image2

建立動畫

image3

建立完動畫後,就會出現一條時間線,上面的數位代表時間,單位為秒。

image4

我們希望讓這個動畫在遊戲開始時自動開始播放,而且還應該迴圈播放。

要實作這個需求,可以按一下動畫工具列上對應的“A+”圖示和迴圈箭頭。

image5

你還可以按一下右上角的圖釘圖示,將動畫編輯器進行固定。這樣它就不會在你點擊視口取消選擇節點時折疊。

image6

在面板右上角將動畫的時長設為 1.2 秒。

image7

您應該看到灰色帶子變寬了一點。它顯示動畫的開始和結束,垂直藍線是您的時間游標。

image8

按一下並拖拽右下角的滑動條,即可將時間線進行縮放。

image9

光照動畫

使用動畫播放機節點,你可以對所需任意數量的節點的大多數屬性做動畫。請注意*屬性面板*中屬性旁的鑰匙圖示。在上面按一下就可以建立一個關鍵影格,即對應屬性的一對時間與值。關鍵影格會被插入到時間線上的時間游標處。

讓我們來開始插入關鍵影格吧。這裡,我們要為 Character 節點的平移(translation)和旋轉(rotation)做動畫。

選中 Character 並在*屬性面板*中展開 Transform 欄。按一下 PositionRotation 旁的鑰匙圖示。

image10

../../_images/curves.webp

對於本教學,我們只建立預設選擇 RESET(重設)軌道

編輯器中會出現兩個軌道,各有一個代表關鍵影格的菱形圖示。

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

在灰色的時間線上按一下並拖拽,將時間游標移動至 0.5 秒位置。

../../_images/timeline_05_click.webp

按一下灰色的時間線並拖動,將時間游標移動到 0.5 秒的位置。在*屬性面板*中將 TranslationY 軸設為約 0.65 米,將 Rotation DegreesX 軸設為 8

image13

為這兩個屬性分別建立一個關鍵影格

../../_images/second_keys_both.webp

現在開始在時間線上拖動,將平移的關鍵影格移動到 0.7 秒。

image14

備註

關於動畫原理的講解已經超出了本教學的範圍。請注意,您不想均勻地分配時間和空間。取而代之的是,動畫師使用時間和間隔,這兩個核心動畫原則。您希望讓它們存在一定的偏移,在角色的運動中產生對比,以使他們感覺生動。

將時間游標移動到動畫結尾,即 1.2 秒。將 Y 平移量設為約 0.35、X 旋轉量設為 -9 度。再次為這兩個屬性新增關鍵影格。

../../_images/animation_final_keyframes.webp

按一下播放按鈕或者按 Shift + D 即可預覽結果。按一下停止按鈕或者按 S 即可停止播放。

image1

您可以看到引擎在關鍵影格之間插值以生成連續動畫。不過目前,這個動作感覺非常機器人化。這是因為預設插值是線性的,導致持續的過渡,這與現實世界中生物的移動方式不同。

我們可以使用緩動曲線來控制關鍵影格之間的過渡。

按一下並拖拽,框選時間線上的前兩個關鍵影格。

image1

可以在*屬性面板*中同時編輯這兩個關鍵影格的屬性,其中就有一個屬性叫做 *Easing*(緩動)。

image1

按一下並拖動曲線,把它往左拉。這樣就會讓他實作緩出,也就是說,一開始變得快,然後時間游標越接近下一個關鍵影格就變得越慢。

image1

再次播放動畫以查看差異。前半部分應該已經感覺有點彈性了。

將緩動效果應用於旋轉軌跡中的第二個關鍵影格。

image1

對第二個平移關鍵影格執行相反操作,將其拖動到右側。

image2

最後場景應該長這樣。

image2

備註

每一影格,動畫都會去更新被動畫的節點的屬性,覆蓋掉初始值。如果我們直接對 Player 節點做動畫,就沒法使用程式碼來移動它了。這就是 Pivot 節點的用處:儘管我們為 Character 做了動畫,我們還是可以在此動畫之上,再通過程式碼來移動並旋轉 Pivot

如果你運作遊戲,玩家的生物就會漂浮起來!

如果這個生物離地面太近了,你可以將 Pivot 向上移動,達成偏移的目的。

建立內容

我們可以使用程式碼來根據玩家的輸入控制動畫的播放。讓我們在角色移動時修改動畫的速度吧。

點擊 Player 旁的腳本圖示打開其腳本。

image2

_physics_process() 中檢查 direction 向量的那一行之後新增如下程式碼。

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

這段程式碼的作用是讓玩家在移動時將播放速度乘以 4。在停止移動時將其恢復原狀。

我們提到樞紐(Pivot)可以在動畫之上疊加變換(transforms)。我們可以用下面這行程式碼使角色在跳躍時產生弧線。將其新增到 _physics_process() 的最後。

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

動畫

在 Godot 中還有一個很好的動畫技巧:只要你使用類似的節點結構,你就可以把它們複製到不同的場景中。

例如,MobPlayer 場景都有 PivotCharacter 節點,所以我們可以在它們之間複用動畫。

打開 Player 場景,選中動畫播放機節點,打開浮動動畫。然後點擊*動畫 -> 複製*。然後打開 Mob.tscn 並打開其中的動畫播放機。點擊*動畫 -> 貼上*。這樣就行了;所有的怪物現在就都能播放浮動動畫了。

我們可以根據生物的 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 -90 and +90 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