使用 InputEvent

什麼是 InputEvent?

無論在任何作業系統或平台上,管理輸入通常都很複雜。為了簡化這個過程,Godot 提供了一個特殊的內建型別 InputEvent。這個型別可以被設為包含多種輸入事件。輸入事件會在引擎中流動,並根據用途在多個位置被接收。

以下是一個簡單範例:當按下 Escape 鍵時關閉遊戲:

func _unhandled_input(event):
    if event is InputEventKey:
        if event.pressed and event.keycode == KEY_ESCAPE:
            get_tree().quit()

然而,更乾淨且靈活的做法是使用 Godot 提供的 InputMap 功能,可讓你定義輸入動作並分配多個不同按鍵。如此一來,你可以將多個按鍵(如鍵盤的 ESC 與手把的開始鍵)對應到同一個動作。你也可以在專案設定中輕鬆變更對應,無需修改程式碼,甚至可以擴充成遊戲內的自訂按鍵功能,讓玩家於執行時變更操作按鍵!

你可以在 專案 > 專案設定 > 輸入對應 下設定你的 InputMap,並像這樣使用這些動作:

func _process(delta):
    if Input.is_action_pressed("ui_right"):
        # Move right.

它的運作方式是什麼?

每個輸入事件最初來自使用者/玩家(當然,也可以自行產生 InputEvent 並餵給引擎,這在手勢等應用時很有用)。各平台的 DisplayServer 會從作業系統讀取事件,然後傳遞給根 Window

視窗的 Viewport 會依序對收到的輸入事件執行多項處理:

../../_images/input_event_flow.webp
  1. 如果 Viewport 內嵌了 Window,Viewport 會以視窗管理員的角色解讀事件(例如調整 Window 大小或移動視窗)。

  2. 接著,若有內嵌 Window 取得焦點,事件會送往該 Window 並於其 Viewport 中處理,之後視為已處理。若沒有內嵌 Window 聚焦,事件會依下列順序傳遞給目前視口的節點。

  3. 首先,會呼叫任何有覆寫 Node._input() 的節點(且未用 Node.set_process_input() 停用輸入處理)。若事件在此被消耗,可以呼叫 Viewport.set_input_as_handled() 阻止事件繼續傳播。這確保你可以在 GUI 之前就攔截感興趣的事件。對於遊戲主要輸入,通常建議用 Node._unhandled_input(),因為這讓 GUI 有機會優先處理。

  4. 其次,系統會嘗試將輸入傳遞至 GUI,看有無控制項可接收。若有,將會呼叫 Control 的虛擬方法 Control._gui_input(),並發出 "gui_input" 訊號(此方法可於腳本繼承時覆寫)。若控制項需「消耗」該事件,會呼叫 Control.accept_event(),事件便不再繼續傳遞。你可透過 Control.mouse_filter 屬性,控制 Control 是否透過 Control._gui_input() 回呼接收滑鼠事件,以及事件是否會繼續傳遞。

  5. 如果事件尚未被消耗,且有覆寫 Node._shortcut_input() 停用),則會呼叫該回呼。這僅針對 InputEventKeyInputEventShortcutInputEventJoypadButton。如事件被消耗,可呼叫 Viewport.set_input_as_handled() 阻止事件繼續傳播。此回呼非常適合處理快捷鍵相關事件。

  6. 如果事件尚未被消耗,且有覆寫 Node._unhandled_key_input() 停用),則會呼叫該回呼。這僅當事件為 InputEventKey 時才會發生。如事件被消耗,可呼叫 Viewport.set_input_as_handled() 阻止事件繼續傳播。這個回呼很適合處理鍵盤事件。

  7. 如果事件仍未被消耗,且有覆寫 Node._unhandled_input() 停用),則會呼叫該回呼。若有函式消耗事件,可呼叫 Viewport.set_input_as_handled() 阻止事件繼續傳播。這個回呼很適合處理全螢幕遊戲事件(GUI 元件啟用時不會收到)。

  8. 如果事件到現在都沒被消耗,且已啟用 物件選取,則該事件會用於物件選取。對於根視口,也可在 專案設定 啟用此功能。若是 3D 場景且 Viewport 有指派 Camera3D,則會從點擊方向發射射線到物理世界,命中物件時會呼叫該物件的 CollisionObject3D._input_event()。2D 場景則同理,呼叫 CollisionObject2D._input_event()

將事件傳送給所有子孫節點時,Viewport 會採用反向深度優先順序:從場景樹底層節點到根節點。Window 與 SubViewport 不參與此流程。

../../_images/input_event_scene_flow.webp

備註

這個順序不適用於 Control._gui_input(),GUI 事件的分派會依事件位置或焦點控制元件而異。GUI 滑鼠 事件也會沿著場景樹向上傳遞,受限於前述的 Control.mouse_filter 設定。不過,這類事件只會傳給目標 Control 節點的直接父系。GUI 鍵盤和手把 事件則*不會*沿場景樹傳遞,只能由收到事件的控制元件處理,否則會以非 GUI 事件形式傳遞至 Node._unhandled_input()

由於 Viewport 不會將事件傳遞給其他 SubViewport,你必須使用下列其中一種方法:

  1. 使用 SubViewportContainer,它會在 Node._input()Control._gui_input() 執行後,自動將事件傳送給其子 SubViewports

  2. 根據自身需求實作事件傳遞邏輯。

這符合 Godot 節點導向設計,使專用子節點能處理並消耗特定事件,而父級或場景根節點則可依需要提供更一般性的行為。

InputEvent 結構解析

InputEvent 只是基本的內建類型,不代表任何特定事件,只包含基本資訊,如事件 ID(每個事件遞增)、裝置索引等。

InputEvent 有多種專門型別,說明如下表所示:

事件

說明

InputEvent

空輸入事件。

InputEventKey

包含一個按鍵碼、Unicode 值,以及修飾鍵。

InputEventMouseButton

包含點擊資訊,例如按鈕、修飾鍵等。

InputEventMouseMotion

包含移動資訊,如相對/絕對位置與速度。

InputEventJoypadMotion

包含搖桿/手把類比軸資訊。

InputEventJoypadButton

包含搖桿/手把按鈕資訊。

InputEventScreenTouch

包含多點觸控的按下/釋放資訊。(僅限行動裝置)

InputEventScreenDrag

包含多點觸控的拖曳資訊。(僅限行動裝置)

InputEventMagnifyGesture

包含位置、縮放係數與修飾鍵。

InputEventPanGesture

包含位置、位移量與修飾鍵。

InputEventMIDI

包含 MIDI 相關資訊。

InputEventShortcut

包含一組快捷鍵。

InputEventAction

包含一般動作。這類事件通常由開發者作為回饋手動產生。(詳見下文)

輸入動作

輸入動作是將零個或多個 InputEvent 群組成一個容易理解的名稱(例如預設的「ui_left」可同時代表手把左鍵和鍵盤左方向鍵)。動作本身不一定要對應單一 InputEvent,但這種抽象化設計能讓你在撰寫遊戲邏輯時同時支援多種輸入裝置。

這能帶來下列好處:

  • 相同程式碼適用於不同裝置的不同輸入(如 PC 鍵盤與主機手把)。

  • 可於執行時重新設定輸入對應。

  • 可於程式中在執行時自動觸發動作。

你可以在「專案設定」的 輸入對應 分頁建立動作並分配輸入事件。

任何事件都具備 InputEvent.is_action()InputEvent.is_pressed()InputEvent.is_echo() 這些方法。

你也可以從遊戲程式碼主動送出一個動作(例如用於手勢偵測)。Input 單例物件提供此功能:Input.parse_input_event()。通常你會這樣用:

var ev = InputEventAction.new()
# Set as ui_left, pressed.
ev.action = "ui_left"
ev.pressed = true
# Feedback.
Input.parse_input_event(ev)

也參考

關於如何在專案設定中新增輸入動作,請參閱 建立圖示 教學。

輸入映射表

從程式碼自訂與重映射輸入是常見需求。如果你的系統完全依賴動作,InputMap 單例非常適合於執行時重新分配或建立不同動作。這個單例的狀態不會自動儲存(需自行處理),其預設值來自專案設定(project.godot)。因此,任何這類動態系統都須自行決定設定儲存方式。