Godot 4 GDScript 教程 / 输入系统(InputMap)
输入系统(InputMap)
Godot 的输入系统通过 InputMap 将物理输入(键盘、鼠标、手柄)抽象为逻辑动作,实现跨平台输入处理。本章详细介绍 InputMap 配置、Input 类 API、输入事件层次以及高级输入处理模式。
1. 输入映射 InputMap
1.1 编辑器中配置 InputMap
通过 项目 > 项目设置 > 输入映射 配置输入动作:
| 步骤 | 操作 |
|---|---|
| 1 | 在"添加新动作"输入框中输入动作名称 |
| 2 | 点击"添加"按钮 |
| 3 | 点击动作右侧的"+“按钮添加按键 |
| 4 | 按下要绑定的键或按钮 |
| 5 | 点击"确定"完成绑定 |
1.2 推荐的输入动作配置
| 动作名称 | 键盘 | 鼠标 | 手柄 | 说明 |
|---|---|---|---|---|
move_left | A, ← | — | 左摇杆左 | 向左移动 |
move_right | D, → | — | 左摇杆右 | 向右移动 |
move_up | W, ↑ | — | 左摇杆上 | 向上移动 |
move_down | S, ↓ | — | 左摇杆下 | 向下移动 |
jump | Space | — | 按钮A | 跳跃 |
attack | — | 鼠标左键 | 按钮X | 攻击 |
interact | E | — | 按钮Y | 交互 |
dash | Shift | — | 左摇杆按下 | 冲刺 |
pause | Escape | — | Start | 暂停 |
inventory | I, Tab | — | 按钮B | 背包 |
1.3 代码中管理 InputMap
extends Node
func _ready() -> void:
# 检查动作是否存在
if not InputMap.has_action("jump"):
# 创建新动作
InputMap.add_action("jump")
# 添加键盘映射
var key_event := InputEventKey.new()
key_event.keycode = KEY_SPACE
InputMap.action_add_event("jump", key_event)
# 添加手柄映射
var joy_event := InputEventJoypadButton.new()
joy_event.button_index = JOY_BUTTON_A
InputMap.action_add_event("jump", joy_event)
# 删除动作
if InputMap.has_action("old_action"):
InputMap.action_erase("old_action")
# 获取动作列表
var actions := InputMap.get_actions()
print("已注册动作: %s" % str(actions))
# 获取动作的事件列表
var jump_events := InputMap.action_get_events("jump")
print("Jump 绑定: %d 个事件" % jump_events.size())
# 清除动作的所有映射
InputMap.action_erase_events("jump")
# 修改死区
InputMap.action_set_deadzone("move_left", 0.2)
2. Input 类 API
2.1 查询输入状态
extends CharacterBody2D
@export var speed: float = 300.0
func _physics_process(delta: float) -> void:
# ── 持续按住 ──────────────────────────
# is_action_pressed: 按住时每帧返回 true
if Input.is_action_pressed("move_right"):
velocity.x = speed
elif Input.is_action_pressed("move_left"):
velocity.x = -speed
else:
velocity.x = 0
# ── 刚按下 ──────────────────────────
# is_action_just_pressed: 按下的那一帧返回 true
if Input.is_action_just_pressed("jump"):
velocity.y = -400.0
# ── 刚释放 ──────────────────────────
# is_action_just_released: 释放的那一帧返回 true
if Input.is_action_just_released("jump"):
if velocity.y < 0:
velocity.y *= 0.5 # 短按跳跃低一点
move_and_slide()
2.2 获取输入轴
extends CharacterBody2D
func _physics_process(delta: float) -> void:
# get_axis: 返回 -1 到 1 的浮点值
# get_axis("负方向", "正方向")
var horizontal := Input.get_axis("move_left", "move_right")
var vertical := Input.get_axis("move_up", "move_down")
velocity = Vector2(horizontal, vertical) * 300.0
move_and_slide()
# 更简洁的写法
func get_movement_vector() -> Vector2:
return Vector2(
Input.get_axis("move_left", "move_right"),
Input.get_axis("move_up", "move_down")
).limit_length(1.0) # 限制对角线移动速度
2.3 鼠标输入
extends Node2D
func _unhandled_input(event: InputEvent) -> void:
# 鼠标按键
if event is InputEventMouseButton:
var mouse_event := event as InputEventMouseButton
match mouse_event.button_index:
MOUSE_BUTTON_LEFT:
if mouse_event.pressed:
_on_left_click(mouse_event.position)
MOUSE_BUTTON_RIGHT:
if mouse_event.pressed:
_on_right_click(mouse_event.position)
MOUSE_BUTTON_WHEEL_UP:
_zoom_in()
MOUSE_BUTTON_WHEEL_DOWN:
_zoom_out()
# 鼠标移动
if event is InputEventMouseMotion:
var motion := event as InputEventMouseMotion
_on_mouse_move(motion.position, motion.relative)
# 触摸输入(移动端)
if event is InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed:
_on_touch(touch.position, touch.index)
func _on_left_click(pos: Vector2) -> void:
print("左键点击: %s" % str(pos))
func _on_right_click(pos: Vector2) -> void:
print("右键点击: %s" % str(pos))
func _on_mouse_move(pos: Vector2, relative: Vector2) -> void:
# relative 是鼠标移动的相对距离
pass
func _zoom_in() -> void:
print("放大")
func _zoom_out() -> void:
print("缩小")
2.4 键盘输入
extends Node
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey:
var key_event := event as InputEventKey
if key_event.pressed and not key_event.echo:
# 获取按键
var key := key_event.keycode
var physical_key := key_event.physical_keycode
var key_string := OS.get_keycode_string(key)
match key:
KEY_F1:
_toggle_debug()
KEY_F5:
_quick_save()
KEY_F9:
_quick_load()
KEY_ESCAPE:
_toggle_pause()
KEY_F11:
_toggle_fullscreen()
_:
print("按键: %s" % key_string)
func _toggle_fullscreen() -> void:
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
func _toggle_debug() -> void:
# 切换调试信息显示
var debug_overlay := get_node_or_null("/root/DebugOverlay")
if debug_overlay:
debug_overlay.visible = !debug_overlay.visible
3. 手柄输入
3.1 手柄配置
extends Node
func _ready() -> void:
# 连接手柄连接/断开信号
Input.joy_connection_changed.connect(_on_joy_connection_changed)
# 列出已连接的手柄
for joy_id in Input.get_connected_joypads():
var joy_name := Input.get_joy_name(joy_id)
var joy_guid := Input.get_joy_guid(joy_id)
print("手柄 %d: %s (%s)" % [joy_id, joy_name, joy_guid])
func _on_joy_connection_changed(device_id: int, connected: bool) -> void:
if connected:
print("手柄已连接: %s" % Input.get_joy_name(device_id))
else:
print("手柄已断开: 设备 %d" % device_id)
3.2 手柄震动
extends Node
# 手柄震动反馈
func trigger_vibration(
joy_id: int = 0,
weak_magnitude: float = 0.5,
strong_magnitude: float = 0.5,
duration: float = 0.2
) -> void:
Input.start_joy_vibration(joy_id, weak_magnitude, strong_magnitude, duration)
# 受伤震动
func _on_damage_taken() -> void:
trigger_vibration(0, 0.3, 0.7, 0.15)
# 爆炸震动
func _on_explosion() -> void:
trigger_vibration(0, 0.5, 1.0, 0.3)
# 停止震动
func stop_vibration(joy_id: int = 0) -> void:
Input.stop_joy_vibration(joy_id)
3.3 手柄轴输入
extends CharacterBody2D
@export var move_speed: float = 300.0
@export var deadzone: float = 0.2
func _physics_process(delta: float) -> void:
# 左摇杆
var left_stick := Vector2(
Input.get_joy_axis(0, JOY_AXIS_LEFT_X),
Input.get_joy_axis(0, JOY_AXIS_LEFT_Y)
)
# 应用死区
if left_stick.length() < deadzone:
left_stick = Vector2.ZERO
else:
left_stick = left_stick.normalized() * ((left_stick.length() - deadzone) / (1.0 - deadzone))
# 右摇杆(通常用于瞄准)
var right_stick := Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
)
# 扳机键(LT/RT)
var left_trigger := (Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT) + 1.0) / 2.0
var right_trigger := (Input.get_joy_axis(0, JOY_AXIS_TRIGGER_RIGHT) + 1.0) / 2.0
velocity = left_stick * move_speed
move_and_slide()
4. _input 事件层次
4.1 事件传播顺序
输入事件传播路径:
1. _input() → 场景树根节点开始,逐级向下传播
2. _unhandled_input() → 未被 _input 处理的事件
3. _unhandled_key_input() → 未处理的键盘事件
GUI 事件优先级更高:
- Control 节点先处理输入
- 如果 GUI 消费了事件,不会传播到 _input
4.2 完整的输入处理
extends Node
# 1. _input: 最先处理,可以过滤/修改事件
func _input(event: InputEvent) -> void:
# 可以在这里阻止事件传播
if event.is_action_pressed("ui_accept"):
# 如果是 UI 事件,标记为已处理
get_viewport().set_input_as_handled()
# 全局输入处理(如调试快捷键)
if event is InputEventKey:
var key_event := event as InputEventKey
if key_event.pressed and key_event.keycode == KEY_F1:
_toggle_debug()
get_viewport().set_input_as_handled()
# 2. _unhandled_input: 未被 GUI 或 _input 处理的事件
func _unhandled_input(event: InputEvent) -> void:
# 游戏逻辑输入处理
if event.is_action_pressed("attack"):
_perform_attack()
elif event.is_action_pressed("interact"):
_interact()
# 3. _unhandled_key_input: 专门处理键盘
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
match event.keycode:
KEY_TAB:
_toggle_inventory()
KEY_M:
_toggle_map()
# 4. 处理鼠标输入(放在 _unhandled_input 中避免被 UI 拦截)
# func _unhandled_input(event: InputEvent) -> void:
# if event is InputEventMouseButton:
# pass
# ⚠️ 不要同时使用 _input 和 _unhandled_input 处理同一个动作
⚠️ 注意: 游戏逻辑输入应使用 _unhandled_input,这样当 UI 打开时输入会被 UI 消费,不会触发游戏操作。
5. 触摸输入
5.1 基本触摸
extends Node2D
var touch_points: Dictionary = {}
var is_touching: bool = false
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed:
# 触摸按下
touch_points[touch.index] = touch.position
_on_touch_start(touch.index, touch.position)
else:
# 触摸释放
touch_points.erase(touch.index)
_on_touch_end(touch.index, touch.position)
elif event is InputEventScreenDrag:
var drag := event as InputEventScreenDrag
if touch_points.has(drag.index):
var old_pos: Vector2 = touch_points[drag.index]
touch_points[drag.index] = drag.position
_on_touch_drag(drag.index, drag.position, drag.position - old_pos)
func _on_touch_start(finger: int, pos: Vector2) -> void:
print("触摸开始: 手指 %d, 位置 %s" % [finger, str(pos)])
func _on_touch_end(finger: int, pos: Vector2) -> void:
print("触摸结束: 手指 %d" % finger)
func _on_touch_drag(finger: int, pos: Vector2, delta: Vector2) -> void:
print("触摸拖动: 手指 %d, 位移 %s" % [finger, str(delta)])
5.2 手势识别
extends Node
## 简单手势识别器
signal swipe_detected(direction: Vector2)
signal pinch_detected(scale_factor: float)
signal tap_detected(pos: Vector2)
signal long_press_detected(pos: Vector2)
@export var swipe_threshold: float = 50.0
@export var long_press_time: float = 0.5
var _touch_start: Dictionary = {}
var _touch_time: float = 0.0
var _is_touching: bool = false
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed:
_touch_start[touch.index] = touch.position
_touch_time = Time.get_ticks_msec() / 1000.0
_is_touching = true
else:
if _is_touching and _touch_start.has(touch.index):
var start_pos: Vector2 = _touch_start[touch.index]
var delta: Vector2 = touch.position - start_pos
var elapsed := Time.get_ticks_msec() / 1000.0 - _touch_time
if delta.length() < 10 and elapsed < long_press_time:
tap_detected.emit(touch.position)
elif delta.length() >= swipe_threshold:
swipe_detected.emit(delta.normalized())
elif elapsed >= long_press_time:
long_press_detected.emit(touch.position)
_touch_start.erase(touch.index)
if _touch_start.is_empty():
_is_touching = false
# 使用手势
func _ready() -> void:
swipe_detected.connect(_on_swipe)
tap_detected.connect(_on_tap)
func _on_swipe(direction: Vector2) -> void:
if direction.x > 0.7:
print("向右滑动")
elif direction.x < -0.7:
print("向左滑动")
elif direction.y < -0.7:
print("向上滑动")
elif direction.y > 0.7:
print("向下滑动")
func _on_tap(pos: Vector2) -> void:
print("点击: %s" % str(pos))
6. InputEventAction
6.1 创建自定义输入事件
extends Node
# 动态创建并发送输入事件
func simulate_action(action_name: String, pressed: bool = true) -> void:
var event := InputEventAction.new()
event.action = action_name
event.pressed = pressed
event.strength = 1.0 if pressed else 0.0
Input.parse_input_event(event)
# 模拟按键
func simulate_key(keycode: Key, pressed: bool = true) -> void:
var event := InputEventKey.new()
event.keycode = keycode
event.pressed = pressed
event.physical_keycode = keycode
Input.parse_input_event(event)
# 模拟鼠标点击
func simulate_mouse_click(pos: Vector2, button: MouseButton = MOUSE_BUTTON_LEFT) -> void:
var event := InputEventMouseButton.new()
event.button_index = button
event.position = pos
event.pressed = true
Input.parse_input_event(event)
# 释放
await get_tree().create_timer(0.05).timeout
event.pressed = false
Input.parse_input_event(event)
# 使用示例:AI 控制器
func _ai_control() -> void:
# AI 模拟玩家输入
simulate_action("move_right", true)
await get_tree().create_timer(1.0).timeout
simulate_action("move_right", false)
simulate_action("jump", true)
await get_tree().create_timer(0.1).timeout
simulate_action("jump", false)
7. 虚拟摇杆
7.1 虚拟摇杆实现
# virtual_joystick.gd
extends Control
signal joystick_input(direction: Vector2)
@export var radius: float = 80.0
@export var dead_zone: float = 10.0
@export var clamp_zone: float = 70.0
@export var use_input_actions: bool = true
@export var action_left: String = "move_left"
@export var action_right: String = "move_right"
@export var action_up: String = "move_up"
@export var action_down: String = "move_down"
var _is_pressed: bool = false
var _touch_index: int = -1
var _output: Vector2 = Vector2.ZERO
@onready var background: TextureRect = $Background
@onready var handle: TextureRect = $Background/Handle
func _ready() -> void:
# 确保可以接收输入
mouse_filter = Control.MOUSE_FILTER_STOP
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed and _is_point_inside(touch.position):
_is_pressed = true
_touch_index = touch.index
_update_handle(touch.position)
get_viewport().set_input_as_handled()
elif not touch.pressed and touch.index == _touch_index:
_reset()
get_viewport().set_input_as_handled()
elif event is InputEventScreenDrag:
var drag := event as InputEventScreenDrag
if drag.index == _touch_index:
_update_handle(drag.position)
get_viewport().set_input_as_handled()
func _is_point_inside(point: Vector2) -> bool:
var center := global_position + size / 2
return point.distance_to(center) <= radius * 2
func _update_handle(touch_pos: Vector2) -> void:
var center := global_position + size / 2
var delta: Vector2 = touch_pos - center
# 限制在半径内
if delta.length() > clamp_zone:
delta = delta.normalized() * clamp_zone
# 更新手柄位置
handle.position = Vector2(radius, radius) + delta - handle.size / 2
# 计算输出
if delta.length() > dead_zone:
_output = delta / clamp_zone
else:
_output = Vector2.ZERO
# 发送信号
joystick_input.emit(_output)
# 模拟输入动作
if use_input_actions:
_simulate_actions()
func _reset() -> void:
_is_pressed = false
_touch_index = -1
_output = Vector2.ZERO
handle.position = Vector2(radius - handle.size.x / 2, radius - handle.size.y / 2)
joystick_input.emit(Vector2.ZERO)
if use_input_actions:
_release_actions()
func _simulate_actions() -> void:
_set_action(action_left, _output.x < -0.3)
_set_action(action_right, _output.x > 0.3)
_set_action(action_up, _output.y < -0.3)
_set_action(action_down, _output.y > 0.3)
func _release_actions() -> void:
_set_action(action_left, false)
_set_action(action_right, false)
_set_action(action_up, false)
_set_action(action_down, false)
func _set_action(action: String, pressed: bool) -> void:
var event := InputEventAction.new()
event.action = action
event.pressed = pressed
event.strength = _output.length() if pressed else 0.0
Input.parse_input_event(event)
func get_output() -> Vector2:
return _output
func get_output_angle() -> float:
return _output.angle() if _output.length() > 0 else 0.0
8. 自定义输入管理器
# autoload/input_manager.gd
extends Node
## 全局输入管理器
signal input_scheme_changed(scheme: String)
enum InputScheme { KEYBOARD_MOUSE, GAMEPAD, TOUCH }
var current_scheme: InputScheme = InputScheme.KEYBOARD_MOUSE
var is_using_gamepad: bool = false
var vibration_enabled: bool = true
func _ready() -> void:
# 监听输入设备切换
Input.joy_connection_changed.connect(_on_joy_connection_changed)
func _input(event: InputEvent) -> void:
# 自动检测输入设备
if event is InputEventKey or event is InputEventMouse:
if current_scheme != InputScheme.KEYBOARD_MOUSE:
current_scheme = InputScheme.KEYBOARD_MOUSE
is_using_gamepad = false
input_scheme_changed.emit("keyboard_mouse")
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
if not is_using_gamepad:
current_scheme = InputScheme.GAMEPAD
is_using_gamepad = true
input_scheme_changed.emit("gamepad")
elif event is InputEventScreenTouch or event is InputEventScreenDrag:
if current_scheme != InputScheme.TOUCH:
current_scheme = InputScheme.TOUCH
input_scheme_changed.emit("touch")
func get_input_icon(action: String) -> Texture2D:
"""根据当前输入方案返回对应的图标"""
match current_scheme:
InputScheme.KEYBOARD_MOUSE:
return _get_keyboard_icon(action)
InputScheme.GAMEPAD:
return _get_gamepad_icon(action)
InputScheme.TOUCH:
return _get_touch_icon(action)
return null
func _get_keyboard_icon(action: String) -> Texture2D:
var events := InputMap.action_get_events(action)
for event in events:
if event is InputEventKey:
# 返回键盘按键图标
return load("res://assets/ui/keys/%s.png" % OS.get_keycode_string(event.keycode))
return null
func _get_gamepad_icon(action: String) -> Texture2D:
# 返回手柄按钮图标
return null
func _get_touch_icon(action: String) -> Texture2D:
# 返回触摸图标
return null
func trigger_vibration(weak: float, strong: float, duration: float) -> void:
if vibration_enabled and is_using_gamepad:
Input.start_joy_vibration(0, weak, strong, duration)
func _on_joy_connection_changed(device: int, connected: bool) -> void:
if connected:
print("手柄已连接: %s" % Input.get_joy_name(device))
9. 无障碍输入适配
# 无障碍功能实现
extends Node
## 输入辅助设置
var auto_run: bool = false
var hold_to_toggle: Dictionary = {} # 将"按住"改为"切换"
var input_remap: Dictionary = {}
var colorblind_mode: int = 0 # 0=正常, 1=红色盲, 2=绿色盲, 3=蓝色盲
func toggle_auto_run() -> void:
auto_run = !auto_run
func toggle_action(action: String) -> bool:
"""切换模式:按一次开启,再按一次关闭"""
if not hold_to_toggle.has(action):
hold_to_toggle[action] = false
hold_to_toggle[action] = !hold_to_toggle[action]
return hold_to_toggle[action]
func is_action_active(action: String) -> bool:
"""检查动作是否激活(考虑切换模式)"""
if hold_to_toggle.has(action):
return hold_to_toggle[action]
return Input.is_action_pressed(action)
# 按键重映射
func remap_action(action: String, new_key: Key) -> void:
InputMap.action_erase_events(action)
var event := InputEventKey.new()
event.keycode = new_key
InputMap.action_add_event(action, event)
input_remap[action] = new_key
_save_remap()
func _save_remap() -> void:
var config := ConfigFile.new()
for action in input_remap:
config.set_value("input", action, input_remap[action])
config.save("user://input.cfg")
func _load_remap() -> void:
var config := ConfigFile.new()
if config.load("user://input.cfg") == OK:
for action in config.get_section_keys("input"):
var key: Key = config.get_value("input", action)
remap_action(action, key)
10. 游戏开发场景
场景:双人本地对战输入
extends Node
## 双人输入配置
var player1_device: int = -1 # -1 = 键盘, 0+ = 手柄ID
var player2_device: int = 0
func get_player_input(player: int, action: String) -> float:
var device: int = player1_device if player == 1 else player2_device
if device == -1:
# 键盘玩家
return Input.get_axis(
action + "_keyboard_negative",
action + "_keyboard_positive"
)
else:
# 手柄玩家
match action:
"move_horizontal":
return Input.get_joy_axis(device, JOY_AXIS_LEFT_X)
"move_vertical":
return Input.get_joy_axis(device, JOY_AXIS_LEFT_Y)
"aim_horizontal":
return Input.get_joy_axis(device, JOY_AXIS_RIGHT_X)
"aim_vertical":
return Input.get_joy_axis(device, JOY_AXIS_RIGHT_Y)
return 0.0
func is_player_action_pressed(player: int, action: String) -> bool:
var device: int = player1_device if player == 1 else player2_device
if device == -1:
return Input.is_action_pressed(action + "_p1")
else:
return Input.is_action_pressed(action + "_p2")
func get_player_movement(player: int) -> Vector2:
return Vector2(
get_player_input(player, "move_horizontal"),
get_player_input(player, "move_vertical")
)
场景:输入缓冲系统
extends Node
## 输入缓冲 - 改善游戏手感
@export var buffer_time: float = 0.15 # 缓冲时间(秒)
var _input_buffer: Dictionary = {}
func buffer_action(action: String) -> void:
"""记录动作按下时间"""
_input_buffer[action] = Time.get_ticks_msec() / 1000.0
func consume_action(action: String) -> bool:
"""消费缓冲中的动作,返回是否有缓冲"""
if _input_buffer.has(action):
var timestamp: float = _input_buffer[action]
var current_time := Time.get_ticks_msec() / 1000.0
if current_time - timestamp <= buffer_time:
_input_buffer.erase(action)
return true
return false
func _input(event: InputEvent) -> void:
# 记录所有按下事件
if event.is_pressed():
for action in InputMap.get_actions():
if event.is_action(action):
buffer_action(action)
# 使用示例:在角色着陆时检查跳跃缓冲
func check_jump_buffer() -> bool:
if is_on_floor() and consume_action("jump"):
return true
return false
11. 扩展阅读
上一章: 09 - 2D 渲染与 Sprite 下一章: 11 - 角色控制器(CharacterBody) (即将发布)