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

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_ESCAPEEsc 键
KEY_SHIFTShift 键
KEY_CONTROLCtrl 键
KEY_ALTAlt 键
KEY_TABTab 键
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_leftA / Left左摇杆←
move_rightD / Right左摇杆→
move_upW / Up左摇杆↑
move_downS / Down左摇杆↓
jumpSpaceA按钮
attackJ / 鼠标左键X按钮
dashShiftB按钮
interactEY按钮
pauseEscapeStart

代码中添加输入映射

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

扩展阅读