| name | input-system |
| description | 输入系统技能。管理游戏输入映射、支持键盘/手柄/触屏多种输入方式, 提供输入缓冲、连招检测等高级功能。
|
| version | 1.0.0 |
| dependencies | ["godot-core"] |
| triggers | [{"pattern":"输入|按键|手柄|控制器|Input|键盘映射"}] |
| inputs | [{"name":"input_type","type":"string","enum":["basic","advanced","fighting_game"],"required":false}] |
| outputs | [{"name":"input_manager","type":"file","path_pattern":"res://scripts/autoload/input_manager.gd"}] |
输入系统技能
管理游戏输入,支持多种输入设备和高级输入处理。
标准输入映射
动作游戏输入
[input]
move_left = [A, D-Pad Left, Left Stick Left]
move_right = [D, D-Pad Right, Left Stick Right]
move_up = [W, D-Pad Up, Left Stick Up]
move_down = [S, D-Pad Down, Left Stick Down]
jump = [Space, A Button (Xbox), X Button (PlayStation)]
attack = [Left Mouse, X Button (Xbox), Square (PlayStation)]
special = [Right Mouse, Y Button (Xbox), Triangle (PlayStation)]
dash = [Shift, LB/L1]
interact = [E, B Button (Xbox), Circle (PlayStation)]
look_left = [Right Stick Left]
look_right = [Right Stick Right]
look_up = [Right Stick Up]
look_down = [Right Stick Down]
pause = [Escape, Start]
inventory = [Tab, Back/Select]
按键码速查
| 按键 | Godot 常量 |
|---|
| A-Z | KEY_A - KEY_Z |
| 0-9 | KEY_0 - KEY_9 |
| F1-F12 | KEY_F1 - KEY_F12 |
| 空格 | KEY_SPACE |
| Enter | KEY_ENTER |
| Escape | KEY_ESCAPE |
| Shift | KEY_SHIFT |
| Ctrl | KEY_CTRL |
| Alt | KEY_ALT |
| Tab | KEY_TAB |
| 方向键 | KEY_LEFT/RIGHT/UP/DOWN |
手柄按钮
| 按钮 | Xbox | PlayStation | Godot |
|---|
| 南 | A | × | JOY_BUTTON_A |
| 东 | B | ○ | JOY_BUTTON_B |
| 西 | X | □ | JOY_BUTTON_X |
| 北 | Y | △ | JOY_BUTTON_Y |
| LB | LB | L1 | JOY_BUTTON_LEFT_SHOULDER |
| RB | RB | R1 | JOY_BUTTON_RIGHT_SHOULDER |
| LT | LT | L2 | JOY_AXIS_TRIGGER_LEFT |
| RT | RT | R2 | JOY_AXIS_TRIGGER_RIGHT |
输入管理器
input_manager.gd (Autoload)
## 输入管理器
##
## 统一处理所有输入,支持输入缓冲、按键重映射
## @author AI Generated
## @version 1.0.0
class_name InputManager
extends Node
# region 信号
signal input_device_changed(device_type: DeviceType)
signal action_pressed(action: String)
signal action_released(action: String)
# endregion
# region 枚举
enum DeviceType { KEYBOARD_MOUSE, CONTROLLER, TOUCH }
# endregion
# region 常量
const INPUT_BUFFER_TIME: float = 0.15
const COMBO_WINDOW: float = 0.3
# endregion
# region 导出变量
@export var vibration_enabled: bool = true
@export var vibration_strength: float = 1.0
# endregion
# region 状态变量
var current_device: DeviceType = DeviceType.KEYBOARD_MOUSE
var _input_buffer: Dictionary = {} # action -> timestamp
var _combo_buffer: Array = []
var _last_input_time: float = 0.0
# endregion
# region 生命周期
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
func _process(delta: float) -> void:
_clean_expired_buffer()
func _input(event: InputEvent) -> void:
_detect_device_change(event)
_buffer_inputs(event)
# endregion
# region 设备检测
func _detect_device_change(event: InputEvent) -> void:
var new_device = current_device
if event is InputEventKey or event is InputEventMouse:
new_device = DeviceType.KEYBOARD_MOUSE
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
new_device = DeviceType.CONTROLLER
elif event is InputEventScreenTouch:
new_device = DeviceType.TOUCH
if new_device != current_device:
current_device = new_device
input_device_changed.emit(current_device)
# endregion
# region 输入缓冲
func _buffer_inputs(event: InputEvent) -> void:
for action in InputMap.get_actions():
if event.is_action_pressed(action):
_input_buffer[action] = Time.get_ticks_msec() / 1000.0
action_pressed.emit(action)
_add_to_combo(action)
elif event.is_action_released(action):
action_released.emit(action)
func _clean_expired_buffer() -> void:
var current_time = Time.get_ticks_msec() / 1000.0
var expired_actions = []
for action in _input_buffer:
if current_time - _input_buffer[action] > INPUT_BUFFER_TIME:
expired_actions.append(action)
for action in expired_actions:
_input_buffer.erase(action)
## 检查动作是否在缓冲时间内被按下(消耗缓冲)
func consume_buffered_action(action: String) -> bool:
if _input_buffer.has(action):
_input_buffer.erase(action)
return true
return false
## 检查动作是否在缓冲时间内被按下(不消耗)
func is_action_buffered(action: String) -> bool:
return _input_buffer.has(action)
# endregion
# region 连招系统
func _add_to_combo(action: String) -> void:
var current_time = Time.get_ticks_msec() / 1000.0
# 超时则清空连招
if current_time - _last_input_time > COMBO_WINDOW:
_combo_buffer.clear()
_combo_buffer.append(action)
_last_input_time = current_time
# 限制缓冲长度
if _combo_buffer.size() > 10:
_combo_buffer.pop_front()
## 检查是否匹配连招序列
func check_combo(sequence: Array) -> bool:
if sequence.size() > _combo_buffer.size():
return false
var start_index = _combo_buffer.size() - sequence.size()
for i in range(sequence.size()):
if _combo_buffer[start_index + i] != sequence[i]:
return false
return true
## 消耗并清空连招缓冲
func consume_combo() -> void:
_combo_buffer.clear()
## 获取当前连招序列
func get_combo_sequence() -> Array:
return _combo_buffer.duplicate()
# endregion
# region 手柄震动
func vibrate(weak_magnitude: float = 0.5, strong_magnitude: float = 0.5, duration: float = 0.2) -> void:
if not vibration_enabled:
return
weak_magnitude *= vibration_strength
strong_magnitude *= vibration_strength
for device_id in Input.get_connected_joypads():
Input.start_joy_vibration(device_id, weak_magnitude, strong_magnitude, duration)
func stop_vibration() -> void:
for device_id in Input.get_connected_joypads():
Input.stop_joy_vibration(device_id)
# endregion
# region 工具方法
## 获取当前设备的动作图标路径
func get_action_icon(action: String) -> String:
var base_path = "res://assets/textures/ui/input_icons/"
match current_device:
DeviceType.KEYBOARD_MOUSE:
return base_path + "keyboard/" + action + ".png"
DeviceType.CONTROLLER:
return base_path + "controller/" + action + ".png"
DeviceType.TOUCH:
return base_path + "touch/" + action + ".png"
return ""
## 获取移动输入向量
func get_movement_vector() -> Vector2:
return Input.get_vector("move_left", "move_right", "move_up", "move_down")
## 获取视角输入向量
func get_look_vector() -> Vector2:
return Input.get_vector("look_left", "look_right", "look_up", "look_down")
## 检查是否有任意移动输入
func has_movement_input() -> bool:
return get_movement_vector() != Vector2.ZERO
## 获取当前设备类型名称
func get_device_name() -> String:
match current_device:
DeviceType.KEYBOARD_MOUSE:
return "Keyboard & Mouse"
DeviceType.CONTROLLER:
return "Controller"
DeviceType.TOUCH:
return "Touch"
return "Unknown"
# endregion
# region 按键重映射
var _custom_mappings: Dictionary = {}
func remap_action(action: String, event: InputEvent) -> void:
# 清除现有映射
for existing in InputMap.action_get_events(action):
InputMap.action_erase_event(action, existing)
# 添加新映射
InputMap.action_add_event(action, event)
_custom_mappings[action] = event
_save_custom_mappings()
func reset_to_defaults() -> void:
# 重新加载默认输入映射
InputMap.load_from_project_settings()
_custom_mappings.clear()
_save_custom_mappings()
func _save_custom_mappings() -> void:
var config = ConfigFile.new()
for action in _custom_mappings:
var event = _custom_mappings[action]
config.set_value("input", action, var_to_str(event))
config.save("user://input_config.cfg")
func _load_custom_mappings() -> void:
var config = ConfigFile.new()
if config.load("user://input_config.cfg") == OK:
for action in config.get_section_keys("input"):
var event = str_to_var(config.get_value("input", action))
if event:
remap_action(action, event)
# endregion
高级输入处理
输入缓冲器(单独组件)
## 输入缓冲器
##
## 为单个实体提供输入缓冲功能
class_name InputBuffer
extends Node
@export var buffer_time: float = 0.15
var _buffer: Dictionary = {}
func buffer_action(action: String) -> void:
_buffer[action] = buffer_time
func _process(delta: float) -> void:
var to_remove = []
for action in _buffer:
_buffer[action] -= delta
if _buffer[action] <= 0:
to_remove.append(action)
for action in to_remove:
_buffer.erase(action)
func consume(action: String) -> bool:
if _buffer.has(action):
_buffer.erase(action)
return true
return false
func is_buffered(action: String) -> bool:
return _buffer.has(action)
func clear() -> void:
_buffer.clear()
连招定义系统
## 连招定义
class_name ComboDefinition
extends Resource
@export var name: String
@export var sequence: Array[String]
@export var time_window: float = 0.3
@export var result_action: String
func matches(input_sequence: Array) -> bool:
if sequence.size() > input_sequence.size():
return false
var start = input_sequence.size() - sequence.size()
for i in range(sequence.size()):
if input_sequence[start + i] != sequence[i]:
return false
return true
连招检测器
## 连招检测器
class_name ComboDetector
extends Node
signal combo_detected(combo_name: String)
@export var combos: Array[ComboDefinition]
@export var input_window: float = 0.3
var _input_history: Array = []
var _last_input_time: float = 0.0
func _ready() -> void:
InputManager.action_pressed.connect(_on_action_pressed)
func _on_action_pressed(action: String) -> void:
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - _last_input_time > input_window:
_input_history.clear()
_input_history.append(action)
_last_input_time = current_time
# 检查所有连招
for combo in combos:
if combo.matches(_input_history):
combo_detected.emit(combo.name)
_input_history.clear()
break
# 限制历史长度
if _input_history.size() > 20:
_input_history.pop_front()
触屏输入支持
虚拟摇杆
## 虚拟摇杆
class_name VirtualJoystick
extends Control
signal joystick_input(direction: Vector2)
@export var max_distance: float = 50.0
@export var dead_zone: float = 0.2
var _is_pressed: bool = false
var _touch_index: int = -1
var _center: Vector2
var _current_pos: Vector2
@onready var base: TextureRect = $Base
@onready var knob: TextureRect = $Knob
func _ready() -> void:
_center = size / 2
knob.position = _center - knob.size / 2
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed and _is_point_inside(event.position):
_is_pressed = true
_touch_index = event.index
_update_knob(event.position)
elif not event.pressed and event.index == _touch_index:
_release()
elif event is InputEventScreenDrag:
if event.index == _touch_index:
_update_knob(event.position)
func _is_point_inside(point: Vector2) -> bool:
var local_point = point - global_position
return local_point.length() <= size.x / 2
func _update_knob(touch_pos: Vector2) -> void:
var local_pos = touch_pos - global_position - _center
var clamped = local_pos.limit_length(max_distance)
knob.position = _center + clamped - knob.size / 2
var direction = clamped / max_distance
if direction.length() < dead_zone:
direction = Vector2.ZERO
joystick_input.emit(direction)
func _release() -> void:
_is_pressed = false
_touch_index = -1
knob.position = _center - knob.size / 2
joystick_input.emit(Vector2.ZERO)
触屏按钮
## 触屏动作按钮
class_name TouchActionButton
extends TextureButton
@export var action_name: String
func _ready() -> void:
pressed.connect(_on_pressed)
button_up.connect(_on_released)
func _on_pressed() -> void:
Input.action_press(action_name)
func _on_released() -> void:
Input.action_release(action_name)
输入设置 UI
## 输入设置界面
class_name InputSettingsUI
extends Control
@onready var action_list: VBoxContainer = $ActionList
@onready var rebind_popup: PopupPanel = $RebindPopup
var _current_rebind_action: String = ""
func _ready() -> void:
_populate_action_list()
func _populate_action_list() -> void:
for action in InputMap.get_actions():
if action.begins_with("ui_"):
continue # 跳过系统动作
var row = _create_action_row(action)
action_list.add_child(row)
func _create_action_row(action: String) -> HBoxContainer:
var row = HBoxContainer.new()
var label = Label.new()
label.text = action.capitalize().replace("_", " ")
label.custom_minimum_size.x = 200
row.add_child(label)
var button = Button.new()
button.text = _get_action_key_name(action)
button.pressed.connect(func(): _start_rebind(action))
row.add_child(button)
return row
func _get_action_key_name(action: String) -> String:
var events = InputMap.action_get_events(action)
if events.size() > 0:
return events[0].as_text()
return "Unbound"
func _start_rebind(action: String) -> void:
_current_rebind_action = action
rebind_popup.popup_centered()
func _input(event: InputEvent) -> void:
if _current_rebind_action.is_empty():
return
if event is InputEventKey or event is InputEventJoypadButton:
InputManager.remap_action(_current_rebind_action, event)
rebind_popup.hide()
_current_rebind_action = ""
_refresh_ui()
func _refresh_ui() -> void:
for child in action_list.get_children():
child.queue_free()
_populate_action_list()
使用示例
基本使用
# 在玩家脚本中
func _physics_process(delta: float) -> void:
var direction = InputManager.get_movement_vector()
velocity = direction * speed
# 使用输入缓冲处理跳跃
if InputManager.consume_buffered_action("jump") and is_on_floor():
velocity.y = jump_velocity
连招检测
# 设置连招
var hadouken_combo = ComboDefinition.new()
hadouken_combo.name = "hadouken"
hadouken_combo.sequence = ["move_down", "move_down_right", "move_right", "attack"]
# 监听连招
combo_detector.combo_detected.connect(func(name):
if name == "hadouken":
perform_hadouken()
)