Coding the player

In this lesson, we'll add player movement, animation, and set it up to detect collisions.

To do so, we need to add some functionality that we can't get from a built-in node, so we'll add a script. Click the Player node and click the "Attach Script" button:

../../_images/add_script_button.png

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

備註

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

../../_images/attach_node_window.png

備註

If this is your first time encountering GDScript, please read Scripting languages before continuing.

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

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] (腳本變數) 內看到這個屬性。但請記得,如果在屬性面板中修改了數值,會覆蓋掉腳本內所指定的值。

警告

使用 C# 時,若要看到新匯出的變數或訊號,就需要建置或重新建置專案。要重新建置專案可以手動點擊編輯器視窗底部的 [Mono] 文字來打開 Mono 面板,然後點擊 [建置專案] 按鈕。

../../_images/export_variable.png

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

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

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

  • 檢查輸入。

  • 沿指定方向移動。

  • 播放適當的動畫。

First, we need to check for input - is the player pressing a key? For this game, we have 4 direction inputs to check. Input actions are defined in the Project Settings under "Input Map". Here, you can define custom events and assign different keys, mouse events, or other inputs to them. For this game, we will map the arrow keys to the four directions.

Click on Project -> Project Settings to open the project settings window and click on the Input Map tab at the top. Type "move_right" in the top bar and click the "Add" button to add the move_right action.

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

We need to assign a key to this action. Click the "+" icon on the right, then click the "Key" option in the drop-down menu. A dialog asks you to type in the desired key. Press the right arrow on your keyboard and click "Ok".

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

Repeat these steps to add three more mappings:

  1. move_left mapped to the left arrow key.

  2. move_up mapped to the up arrow key.

  3. And move_down mapped to the down arrow key.

Your input map tab should look like this:

../../_images/input-mapping-completed.png

Click the "Close" button to close the project settings.

備註

We only mapped one key to each input action, but you can map multiple keys, joystick buttons, or mouse buttons to the same input action.

可以用 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
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()

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

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

小訣竅

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

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

小訣竅

$ is shorthand for get_node(). So in the code above, $AnimatedSprite.play() is the same as get_node("AnimatedSprite").play().

In GDScript, $ returns the node at the relative path from the current node, or returns null if the node is not found. Since AnimatedSprite is a child of the current node, we can use $AnimatedSprite.

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

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)

小訣竅

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

Click "Play Scene" (F6, Cmd + R on macOS) and confirm you can move the player around the screen in all directions.

警告

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

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

this likely means you spelled the name of the AnimatedSprite node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

選擇動畫

Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the "walk" animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the "up" animation, which should be flipped vertically with flip_v for downward movement. Let's place this code at the end of the _process() function:

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

備註

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

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

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

小訣竅

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

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

hide()

準備碰撞

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

將下列程式碼加在腳本頂部的 extends Area2D 後:

signal hit

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

../../_images/player_signals.png

可以看到我們自定的「hit」訊號出現在這裡了!由於敵人使用 RigidBody2D 節點,所以我們會需要 body_entered(body: Node) 訊號。這個訊號會在敵人的形體 (Body) 接觸到玩家時送出。點擊 [連接...] 後會出現「連接訊號」視窗。我們先不動這裡的設定,點擊 [連接]。Godot 會自動在玩家腳本內建立一個函式。

../../_images/player_signal_connection.png

可以注意到,出現了一個綠色圖示。這個圖示代表有訊號連到這個函式上。將這段程式碼加到函式中:

func _on_Player_body_entered(body):
    hide() # Player disappears after being hit.
    emit_signal("hit")
    # 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

With the player working, we'll work on the enemy in the next lesson.