强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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)

在编辑器中配置

  1. Project → Project Settings → Input Map
  2. 添加动作名称(如 “jump”、“shoot”)
  3. 为每个动作添加按键绑定

推荐的输入映射

动作名称 键盘 手柄
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

扩展阅读