Godot 3 GDScript 教程 / 输入处理(Input)
输入处理(Input)
输入系统概述
Godot 的输入系统将各种输入设备(键盘、鼠标、手柄、触摸屏)统一抽象为输入动作(Action)。开发者定义动作名称,然后在代码中检测动作状态。
输入处理流程
硬件输入事件
│
├── _input(event) # 最先接收,可拦截
├── _unhandled_input(event) # 未被 UI 消费的事件
├── Input.is_action_pressed() # 轮询方式检查
│
└── 处理完成
输入事件(InputEvent)
InputEvent 类型
| 事件类型 | 说明 | 继承自 |
|---|---|---|
InputEventKey | 键盘事件 | InputEvent |
InputEventMouseButton | 鼠标按钮事件 | InputEventMouse |
InputEventMouseMotion | 鼠标移动事件 | InputEventMouse |
InputEventJoypadButton | 手柄按钮事件 | InputEvent |
InputEventJoypadMotion | 手柄摇杆事件 | InputEvent |
InputEventScreenTouch | 触摸事件 | InputEvent |
InputEventScreenDrag | 触摸拖拽事件 | InputEvent |
InputEventAction | 自定义动作事件 | InputEvent |
在 _input 中处理事件
extends Node2D
func _input(event: InputEvent) -> void:
# 键盘事件
if event is InputEventKey:
if event.pressed and event.scancode == KEY_ESCAPE:
get_tree().quit()
if event.pressed and event.scancode == KEY_F11:
OS.window_fullscreen = not OS.window_fullscreen
# 鼠标按钮事件
if event is InputEventMouseButton:
if event.button_index == BUTTON_LEFT and event.pressed:
_shoot(event.position)
if event.button_index == BUTTON_RIGHT:
_aim(event.position)
# 鼠标移动事件
if event is InputEventMouseMotion:
_update_cursor(event.position)
# 使用动作检测(推荐)
if event.is_action_pressed("jump"):
_jump()
if event.is_action_released("shoot"):
_release_shot()
⚠️ 注意:_input() 在每一帧中可能被多次调用(如果有多于一个输入事件)。
键盘输入
轮询方式(推荐用于持续性操作)
extends KinematicBody2D
export var speed: float = 300.0
var velocity: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
velocity = Vector2.ZERO
# 持续按住检测
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
velocity = velocity.normalized() * speed
velocity = move_and_slide(velocity)
瞬时检测(用于一次性操作)
func _process(delta: float) -> void:
# 刚按下瞬间(只触发一次)
if Input.is_action_just_pressed("jump"):
_jump()
# 刚释放瞬间(只触发一次)
if Input.is_action_just_released("jump"):
_release_jump()
# 直接按键检测(不使用动作映射)
if Input.is_key_pressed(KEY_SHIFT):
_sprint()
if Input.is_physical_key_pressed(KEY_SPACE):
_fire()
按键常量
| 常量 | 说明 |
|---|---|
KEY_A ~ KEY_Z | 字母键 |
KEY_0 ~ KEY_9 | 数字键 |
KEY_SPACE | 空格键 |
KEY_ENTER | 回车键 |
KEY_ESCAPE | Esc 键 |
KEY_SHIFT | Shift 键 |
KEY_CONTROL | Ctrl 键 |
KEY_ALT | Alt 键 |
KEY_TAB | Tab 键 |
KEY_F1 ~ KEY_F12 | 功能键 |
KEY_UP / DOWN / LEFT / RIGHT | 方向键 |
Input.is_action_pressed vs _input 事件
| 方式 | 特点 | 适用场景 |
|---|---|---|
Input.is_action_pressed() | 每帧轮询,只返回状态 | 移动、持续按住 |
Input.is_action_just_pressed() | 只在按下瞬间为 true | 跳跃、攻击、菜单选择 |
_input(event) | 事件驱动,包含详细信息 | 文本输入、鼠标精确位置 |
鼠标输入
鼠标位置
extends Node2D
func _process(delta: float) -> void:
# 获取鼠标位置(相对于视口)
var mouse_pos = get_viewport().get_mouse_position()
# 获取全局鼠标位置
var global_mouse = get_global_mouse_position()
# 角色朝向鼠标
var direction = (global_mouse - global_position).normalized()
$Sprite.rotation = direction.angle()
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
# 鼠标移动
print("鼠标移动到: ", event.position)
print("相对移动: ", event.relative) # 鼠标移动增量
if event is InputEventMouseButton:
if event.pressed:
match event.button_index:
BUTTON_LEFT:
print("左键点击")
BUTTON_RIGHT:
print("右键点击")
BUTTON_MIDDLE:
print("中键点击")
BUTTON_WHEEL_UP:
_zoom_in()
BUTTON_WHEEL_DOWN:
_zoom_out()
鼠标光标控制
extends Node
func _ready() -> void:
# 隐藏系统光标
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
# 捕获鼠标(FPS 游戏)
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# 恢复
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
自定义光标
extends Node
func _ready() -> void:
var cursor_texture = preload("res://assets/ui/cursor.png")
var hotspot = Vector2(16, 16) # 光标热点(点击位置)
Input.set_custom_mouse_cursor(cursor_texture, Input.CURSOR_ARROW, hotspot)
手柄输入
手柄按钮
func _input(event: InputEvent) -> void:
if event is InputEventJoypadButton:
if event.pressed:
match event.button_index:
JOY_BUTTON_0: # A / X
print("A 按钮按下")
JOY_BUTTON_1: # B / ○
print("B 按钮按下")
JOY_BUTTON_2: # X / □
print("X 按钮按下")
JOY_BUTTON_3: # Y / △
print("Y 按钮按下")
手柄摇杆
func _input(event: InputEvent) -> void:
if event is InputEventJoypadMotion:
# 左摇杆水平轴 (-1 到 1)
if event.axis == JOY_AXIS_0:
print("左摇杆 X: ", event.axis_value)
# 左摇杆垂直轴
if event.axis == JOY_AXIS_1:
print("左摇杆 Y: ", event.axis_value)
# 右摇杆
if event.axis == JOY_AXIS_2:
print("右摇杆 X: ", event.axis_value)
if event.axis == JOY_AXIS_3:
print("右摇杆 Y: ", event.axis_value)
# 扳机键 (LT/RT)
if event.axis == JOY_AXIS_6: # LT
print("LT: ", event.axis_value)
if event.axis == JOY_AXIS_7: # RT
print("RT: ", event.axis_value)
# 使用动作映射的摇杆输入
func _physics_process(delta: float) -> void:
var stick = Vector2(
Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
)
# 应用死区
if stick.length() < 0.2:
stick = Vector2.ZERO
move_and_slide(stick * 300)
手柄震动
# 开启震动 (手柄ID, 弱电机, 强电机, 持续时间)
Input.start_joy_vibration(0, 0.5, 0.8, 0.5)
# 停止震动
Input.stop_joy_vibration(0)
# 检测手柄连接
func _ready() -> void:
var joypads = Input.get_connected_joypads()
print("已连接手柄数: ", joypads.size())
for joypad in joypads:
print("手柄名称: ", Input.get_joy_name(joypad))
输入映射(InputMap)
在编辑器中配置
- Project → Project Settings → Input Map
- 添加动作名称(如 “jump”、“shoot”)
- 为每个动作添加按键绑定
推荐的输入映射
| 动作名称 | 键盘 | 手柄 |
|---|---|---|
| move_left | A / Left | 左摇杆← |
| move_right | D / Right | 左摇杆→ |
| move_up | W / Up | 左摇杆↑ |
| move_down | S / Down | 左摇杆↓ |
| jump | Space | A按钮 |
| attack | J / 鼠标左键 | X按钮 |
| dash | Shift | B按钮 |
| interact | E | Y按钮 |
| pause | Escape | Start |
代码中添加输入映射
extends Node
func _ready() -> void:
# 添加动作
if not InputMap.has_action("jump"):
InputMap.add_action("jump")
# 添加按键事件
var key_event = InputEventKey.new()
key_event.scancode = KEY_SPACE
InputMap.action_add_event("jump", key_event)
# 添加手柄按钮事件
var joy_event = InputEventJoypadButton.new()
joy_event.button_index = JOY_BUTTON_0
InputMap.action_add_event("jump", joy_event)
# 设置死区
InputMap.action_set_deadzone("move_left", 0.2)
_input 与 _unhandled_input
事件传播顺序
Viewport
├── _input(event) # 第一步
│ ├── Control._gui_input() # UI 节点优先处理
│ │
│ ├── 若未被消费 ↓
│ │
│ └── _unhandled_input(event) # 第二步
│ └── 最终处理
实际应用
# 在 UI 层处理菜单输入
extends Control
func _input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
_toggle_pause()
get_tree().set_input_as_handled() # 标记为已处理,阻止传播
# 在游戏世界中处理游戏输入
extends KinematicBody2D
func _unhandled_input(event: InputEvent) -> void:
# 这里只处理未被 UI 消费的输入
if event.is_action_pressed("attack"):
_perform_attack()
if event is InputEventMouseButton and event.pressed:
_shoot(event.position)
💡 提示:
- UI 节点使用
_input()拦截事件 - 游戏逻辑使用
_unhandled_input()处理游戏输入 - 使用
set_input_as_handled()阻止事件继续传播
触摸输入
基本触摸
extends Node2D
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed:
print("触摸按下: ", event.position, " 手指: ", event.index)
else:
print("触摸释放: ", event.position, " 手指: ", event.index)
if event is InputEventScreenDrag:
print("触摸拖拽: ", event.position, " 速度: ", event.speed)
虚拟摇杆实现
extends Node2D
var is_touching: bool = false
var touch_index: int = -1
var joystick_center: Vector2 = Vector2(200, 400)
var joystick_radius: float = 80.0
var joystick_direction: Vector2 = Vector2.ZERO
onready var knob = $Knob
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed and event.position.x < 540: # 左半屏
is_touching = true
touch_index = event.index
joystick_center = event.position
elif not event.pressed and event.index == touch_index:
is_touching = false
touch_index = -1
joystick_direction = Vector2.ZERO
knob.position = joystick_center
if event is InputEventScreenDrag and event.index == touch_index:
var delta = event.position - joystick_center
if delta.length() > joystick_radius:
delta = delta.normalized() * joystick_radius
knob.position = joystick_center + delta
joystick_direction = delta / joystick_radius
func get_direction() -> Vector2:
return joystick_direction
自定义输入动作
动态切换输入方案
extends Node
enum InputScheme { KEYBOARD, GAMEPAD }
var current_scheme: int = InputScheme.KEYBOARD
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
current_scheme = InputScheme.KEYBOARD
_update_ui_hints()
elif event is InputEventJoypadButton and event.pressed:
current_scheme = InputScheme.GAMEPAD
_update_ui_hints()
func _update_ui_hints() -> void:
match current_scheme:
InputScheme.KEYBOARD:
$PressELabel.text = "按 E 交互"
InputScheme.GAMEPAD:
$PressELabel.text = "按 Y 交互"
获取动作强度(模拟输入)
func _physics_process(delta: float) -> void:
# get_action_strength 返回 0.0 到 1.0
# 对于摇杆,它返回实际的偏移量
var move_x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
var move_y = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
var direction = Vector2(move_x, move_y)
if direction.length() > 1.0:
direction = direction.normalized()
move_and_slide(direction * speed)
输入缓冲区
输入缓冲区(Input Buffer)
在动作游戏中,玩家经常在动画结束前就按下下一个操作。输入缓冲区可以记住玩家最近的输入。
extends KinematicBody2D
var input_buffer: Array = []
var buffer_time: float = 0.15 # 缓冲时间窗口(秒)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
_buffer_input("jump")
if event.is_action_pressed("attack"):
_buffer_input("attack")
func _buffer_input(action: String) -> void:
input_buffer.append({
"action": action,
"time": OS.get_ticks_msec() / 1000.0
})
func _get_buffered_input(action: String) -> bool:
var current_time = OS.get_ticks_msec() / 1000.0
for i in range(input_buffer.size() - 1, -1, -1):
var entry = input_buffer[i]
if current_time - entry["time"] > buffer_time:
input_buffer.remove(i) # 移除过期输入
elif entry["action"] == action:
input_buffer.remove(i)
return true
return false
func _physics_process(delta: float) -> void:
# 使用缓冲输入
if _get_buffered_input("jump") and is_on_floor():
_jump()
if _get_buffered_input("attack") and can_attack:
_attack()
土狼时间(Coyote Time)
玩家离开平台后的一小段时间内仍可跳跃,提升游戏手感。
extends KinematicBody2D
var coyote_time: float = 0.1
var coyote_timer: float = 0.0
var was_on_floor: bool = false
func _physics_process(delta: float) -> void:
var on_floor = is_on_floor()
if on_floor:
coyote_timer = coyote_time
was_on_floor = true
elif was_on_floor:
coyote_timer -= delta
if coyote_timer <= 0:
was_on_floor = false
# 可以在土狼时间内跳跃
var can_jump = on_floor or coyote_timer > 0
if Input.is_action_just_pressed("jump") and can_jump:
_jump()
coyote_timer = 0 # 用完土狼时间
游戏开发场景
场景:完整的角色输入控制器
extends KinematicBody2D
# 移动参数
export var speed: float = 250.0
export var jump_force: float = -500.0
export var gravity: float = 1200.0
export var max_fall_speed: float = 800.0
export var dash_speed: float = 600.0
export var dash_duration: float = 0.15
# 状态
var velocity: Vector2 = Vector2.ZERO
var is_dashing: bool = false
var dash_timer: float = 0.0
var can_dash: bool = true
var facing: int = 1 # 1=右, -1=左
# 土狼时间 & 输入缓冲
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
onready var sprite = $Sprite
onready var dash_cooldown = $DashCooldown
func _physics_process(delta: float) -> void:
_update_timers(delta)
var input_x = _get_input_x()
if is_dashing:
_process_dash(delta)
else:
_process_movement(delta, input_x)
_process_jump()
_process_dash_input()
velocity = move_and_slide(velocity, Vector2.UP)
_update_animation(input_x)
func _get_input_x() -> float:
return Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
func _process_movement(delta: float, input_x: float) -> void:
# 水平移动
velocity.x = input_x * speed
# 重力
if not is_on_floor():
velocity.y += gravity * delta
velocity.y = min(velocity.y, max_fall_speed)
else:
velocity.y = 0
func _process_jump() -> void:
var can_jump = is_on_floor() or coyote_timer > 0
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = 0.1
if jump_buffer_timer > 0 and can_jump:
velocity.y = jump_force
jump_buffer_timer = 0
coyote_timer = 0
func _process_dash_input() -> void:
if Input.is_action_just_pressed("dash") and can_dash and not is_on_floor():
is_dashing = true
can_dash = false
dash_timer = dash_duration
velocity = Vector2(facing * dash_speed, 0)
dash_cooldown.start()
func _process_dash(delta: float) -> void:
dash_timer -= delta
if dash_timer <= 0:
is_dashing = false
func _update_timers(delta: float) -> void:
if is_on_floor():
coyote_timer = 0.1
can_dash = true
else:
coyote_timer -= delta
jump_buffer_timer -= delta
func _update_animation(input_x: float) -> void:
if input_x != 0:
facing = 1 if input_x > 0 else -1
sprite.flip_h = facing == -1
if is_dashing:
$AnimatedSprite.play("dash")
elif not is_on_floor():
$AnimatedSprite.play("jump" if velocity.y < 0 else "fall")
elif abs(velocity.x) > 10:
$AnimatedSprite.play("run")
else:
$AnimatedSprite.play("idle")
func _on_DashCooldown_timeout() -> void:
can_dash = true