Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

移動玩家

在這一課中,我們將新增玩家的動作、動畫,並將其設定為偵測碰撞。

現在我們需要一個內建節點沒有提供的功能,所以需要自己寫腳本了。點擊 Player 節點,然後點擊 [附加腳本] 按鈕:

../../_images/add_script_button.webp

腳本設定視窗內的設定可以保持預設值。直接點擊 [建立] 就好:

備註

如果要建立 C# 腳本或其他語言的腳本,在點擊建立前記得在 [語言] 下拉選單內選擇語言。

../../_images/attach_node_window.webp

備註

如果這是你第一次接觸 GDScript,請在繼續之前閱讀 為場景編寫腳本

首先我們先宣告這個物件所需要的成員變數:

extends Area2D

@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

在第一個變數 speed 上使用 export 關鍵字可以讓我們稍後在屬性面板中指定 speed 的值。當希望讓成員變數與節點內建屬性一樣可以在屬性面板中調整的話,就可以用這個功能。點擊 Player 節點,就可以在 [Script Variables] (腳本變數) 內看到這個屬性。但請記得,如果在屬性面板中修改了數值,會覆蓋掉腳本內所指定的值。

警告

If you're using C#, you need to (re)build the project assemblies whenever you want to see new export variables or signals. This build can be manually triggered by clicking the Build button at the top right of the editor.

../../_images/build_dotnet.webp
../../_images/export_variable.webp

Your player.gd script should already contain a _ready() and a _process() function. If you didn't select the default template shown above, create these functions while following the lesson.

_ready() 函式是當節點進入場景樹後會呼叫的函式,這裡正是取得遊戲視窗大小的好時機:

func _ready():
    screen_size = get_viewport_rect().size

接著我們可以使用 _process() 函式來定義玩家要做的事。 _process() 在每一影格都會被呼叫,所以我們會用 _process() 來更新遊戲內需要常常變化的元素。而對於 Player,我們需要做這些事:

  • 檢查輸入。

  • 沿指定方向移動。

  • 播放適當的動畫。

首先,我們需要檢查輸入——玩家有按按鍵嗎?在這個遊戲中,我們需要檢查四個方向的輸入。輸入操作是在專案設定中的 [輸入對應] 裡設定的。可以在輸入對應中定義自定事件,並給事件指定不同的按鍵、滑鼠事件或其他輸入。在這個示範遊戲中,我們會使用預設事件,這些事件是被分配到鍵盤上的方向鍵。

點擊 專案 -> 專案設定 打開專案設定視窗,然後按一下頂部的 輸入對應 分頁。在頂部欄中鍵入“move_right”,然後按一下“新增”按鈕以新增該 move_right 動作。

../../_images/input-mapping-add-action.webp

我們需要為這個操作分配一個按鍵。按一下右側的“+”圖示,打開事件管理器視窗。

../../_images/input-mapping-add-key.webp

會自動選中“監聽輸入...”區域。按下鍵盤上的“右方向”鍵,選單應該像這樣。

../../_images/input-mapping-event-configuration.webp

選擇“確定”按鈕。現在“右方向”鍵與 move_right 動作關聯了。

重複這些步驟以再新增三個對應:

  1. move_left 對應到左方向鍵。

  2. move_up 對應到向上方向鍵。

  3. move_down 對應到向下方向鍵。

現在場景看起來會這樣:

../../_images/input-mapping-completed.webp

按一下“關閉”按鈕關閉專案設定。

備註

我們只將一個鍵對應到每個輸入動作,但你可以將多個鍵、操縱桿按鈕或滑鼠按鈕對應到同一個輸入動作。

可以用 Input.is_action_pressed() 來偵測按鍵是否被按下。這個函式會在按鍵被按下時回傳 true ,若沒按下則會回傳 false

func _process(delta):
    var velocity = Vector2.ZERO # The player's movement vector.
    if Input.is_action_pressed("move_right"):
        velocity.x += 1
    if Input.is_action_pressed("move_left"):
        velocity.x -= 1
    if Input.is_action_pressed("move_down"):
        velocity.y += 1
    if Input.is_action_pressed("move_up"):
        velocity.y -= 1

    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite2D.play()
    else:
        $AnimatedSprite2D.stop()

我們先從設定 velocity (速率) 開始,設定成 (0, 0) ——這表示在預設情況下,玩家應該不會移動。接著依據不同的輸入來增加/減少 velocity 來往不同方向移動。例如,如果同時按住 方向鍵,最終的 velocity 向量值應該是 (1, 1) 。因為這個情況下我們同時增加了水平與垂直方向的移動距離,所以玩家的移動速度應該比只在水平方向上移動 還要快

我們可以通過 正規化 (Normalize) 速率來防止這種情況,也就是要把移動的 距離 設定為 1 ,接著乘以需要的速度。這表示在對角線上移動不會比水平移動還要快了。

小訣竅

如果你從來沒學習過向量數學運算或是需要複習的話,可以參考這篇 向量數學 瞭解 Godot 中是如何使用向量的。雖然對於這篇本篇教學剩下的部分來說不必要,但若能瞭解 Godot 中的向量是最好不過的了。

同時也需要檢查玩家有沒有在移動,這樣才能呼叫 AnimatedSprite 上的 play()stop()

小訣竅

$get_node() 的簡寫。所以上面的程式碼中 $AnimatedSprite.play() 等同於 get_node("AnimatedSprite").play()

在 GDScript 中, $ 會回傳與目前節點相對路徑上的節點,若找不到則會回傳 null 。由於 AnimatedSprite 是目前節點的子節點,所以可以寫成 $AnimatedSprite

現在玩家會面向不同方向了,可以接著來更新玩家的位置。還可以使用 clamp() (箝制) 來防止 Player 物件跑出畫面外。箝制 一個值,就代表把值限制在一個給定的範圍內。將下列程式碼放到 _process 函式的末尾 (記得確定不要放到 else 縮排下了):

position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)

小訣竅

_process() 函式的 delta 參數代表了 影格時長 ——即是自從上一個影格到現在這個影格所花費的時間。通過這個數值可以確保即使影格率發生變化,移動的距離也能保持不變。

點擊 [執行場景] (F6) 並測試玩家能否夠移動到任何一個方向。

警告

若「除錯工具」中出現了這樣的錯誤

Attempt to call function 'play' in base 'null instance' on a null instance (嘗試在基礎型別為「null 實體」的 null 實體上呼叫「play」函式)

通常這代表程式中的 AnimatedSprite 節點名稱拼錯了。節點名稱有區分大小寫,而且 $NodeName 這種語法中的名稱必須與場景樹裡能看到的節點名稱一致。

選擇動畫

現在玩家可以移動了,現在讓我們來根據移動的方向改變播放的 AnimatedSprite。「walk」動畫會讓玩家看起來朝右邊行走,朝左邊行走時使用 flip_h 屬性來水平翻轉動畫。另一個動畫是「up」,朝下行走時使用 flip_v 來垂直翻轉。來把程式碼放到 _process() 函式的尾端:

if velocity.x != 0:
    $AnimatedSprite2D.animation = "walk"
    $AnimatedSprite2D.flip_v = false
    # See the note below about boolean assignment.
    $AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite2D.animation = "up"
    $AnimatedSprite2D.flip_v = velocity.y > 0

備註

在這段程式碼中的布林賦值是程式設計師常用的一種簡寫方式。因為做了數值比較 (會得到布林值) 後也需要將這個布林值 賦值 到變數上,所以我們可以把這兩件事合在一起做。可以比較看看下面這段程式碼跟上方範例中單行布林賦值的程式:

if velocity.x < 0:
    $AnimatedSprite2D.flip_h = true
else:
    $AnimatedSprite2D.flip_h = false

再執行一次場景來看看各個方向的動畫是否正確。

小訣竅

很多人會在這裡打錯動畫名稱。SpriteFrames 面板上的動畫名稱必須與程式碼中輸入的名稱一致。如果動畫名稱為 「Walk」 ,則程式碼中也必須使用大寫的「W」。

確保能正常移動後,來把下面這行程式碼加到 _ready() 裡。如此一來在遊戲開始的時候玩家會隱藏起來:

hide()

準備碰撞

我們希望 Player 被敵人撞到時能偵測到,但我們還沒做好敵人!沒關係,因為我們會用 Godot 的 訊號 來做這個功能。

在腳本頂部新增以下內容。如果你使用的是 GDScript,請將其新增到 extends Area2D 之後。如果你使用 C#,請將其新增到 public partial class Player : Area2D 之後:

signal hit

這段程式碼定義了一個叫做「hit」的自定訊號,稍後我們會讓玩家在碰撞到敵人的時候送出這個訊號。我們會使用 Area2D 來偵測碰撞。先選擇 Player 節點,然後點擊屬性面板旁邊的 [節點] 分頁來看看所有 Player 能送出的訊號:

../../_images/player_signals.webp

Notice our custom "hit" signal is there as well! Since our enemies are going to be RigidBody2D nodes, we want the body_entered(body: Node2D) signal. This signal will be emitted when a body contacts the player. Click "Connect.." and the "Connect a Signal" window appears.

Godot will create a function with that exact name directly in script for you. You don't need to change the default settings right now.

警告

If you're using an external text editor (for example, Visual Studio Code), a bug currently prevents Godot from doing so. You'll be sent to your external editor, but the new function won't be there.

In this case, you'll need to write the function yourself into the Player's script file.

../../_images/player_signal_connection.webp

Note the green icon indicating that a signal is connected to this function; this does not mean the function exists, only that the signal will attempt to connect to a function with that name, so double-check that the spelling of the function matches exactly!

Next, add this code to the function:

func _on_body_entered(body):
    hide() # Player disappears after being hit.
    hit.emit()
    # Must be deferred as we can't change physics properties on a physics callback.
    $CollisionShape2D.set_deferred("disabled", true)

每當敵人碰撞到玩家後都會送出這個訊號。這時我們需要關閉玩家的碰撞偵測,這樣才不會觸發超過一次的 hit 訊號。

備註

如果在引擎處理碰撞過程中間禁用碰撞區域可能會發生錯誤。所以我們需要使用 set_deferred() 來讓 Godot 等到能安全禁用碰撞區域後再禁用。

最後我們要做的,就是新增一個函式,用來在開始新遊戲時重設玩家。

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

在玩家部分的工作完成後,我們將在下一課中研究敵人。