Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

角色動畫

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

image0

我們會先簡介動畫編輯器的使用方式。

執行編輯器

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

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

動畫」停靠視窗會出現在底部面板。

image1

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

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

image2

將動畫命名為「float」。

image3

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

image4

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

要這麼做,您可以分別點擊動畫工具列中的自動播放按鈕 (自動播放) 和循環箭頭.

image5

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

image6

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

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

image7

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

image8

光照動畫

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

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

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

image9

../../_images/curves.webp

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

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

image10

你可以點擊並拖曳菱形來在時間軸上移動它們。將位置關鍵影格移動到 0.3 秒,並將旋轉關鍵影格移動到 0.1 秒。

image11

在灰色時間軸上點擊並拖曳,或於輸入欄輸入數值,將時間游標移至 0.5 秒。

timeline_05_click

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

如果您在 屬性檢視器 中沒有看到這些屬性,請先在 場景 停靠視窗中再次點擊 Character 節點。

image12

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

second_keys_both

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

image13

備註

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

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

animation_final_keyframes

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

image14

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

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

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

image1

你可以在*屬性檢視器*中同時編輯這兩個關鍵影格的屬性,你可以在那裡看到一個*緩和*屬性。

image1

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

image1

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

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

image1

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

image1

最後場景應該長這樣。

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 場景,選取 AnimationPlayer 節點,然後點擊 動畫 > 管理動畫...。點擊 複製動畫到剪貼簿 按鈕(兩個小方塊),它位於 float 動畫旁邊。點擊「確定」關閉視窗。

接著開啟 mob.tscn,建立一個 AnimationPlayer 子節點並選取它。點擊 Animation > Manage Animations,再選 New Library。你應會看到 Global library will be created. 訊息。將文字欄位留白並按 OK。點擊貼上圖示(剪貼簿),它應會出現在視窗中。最後按 OK 關閉視窗。

接著,請確認底部面板動畫編輯器中的自動播放按鈕 (自動播放) 和循環箭頭 (動畫循環) 也已開啟。這樣就完成了;所有怪物現在都會播放漂浮動畫了。

我們可以根據生物的 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()
        # Setting the basis property will affect the rotation of the node.
        $Pivot.basis = Basis.looking_at(direction)
        $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