extends Node signal device_changed(device: String, device_index: int) signal keyboard_input_changed(action: String, input: InputEvent) signal joypad_input_changed(action: String, input: InputEvent) signal joypad_changed(device_index: int, is_connected: bool) const DEVICE_KEYBOARD = "keyboard" const DEVICE_XBOX_CONTROLLER = "xbox" const DEVICE_SWITCH_CONTROLLER = "switch" const DEVICE_PLAYSTATION_CONTROLLER = "playstation" const DEVICE_STEAMDECK_CONTROLLER = "steamdeck" const DEVICE_GENERIC = "generic" const SUB_DEVICE_XBOX_ONE_CONTROLLER = "xbox_one" const SUB_DEVICE_XBOX_SERIES_CONTROLLER = "xbox_series" const SUB_DEVICE_PLAYSTATION3_CONTROLLER = "playstation3" const SUB_DEVICE_PLAYSTATION4_CONTROLLER = "playstation4" const SUB_DEVICE_PLAYSTATION5_CONTROLLER = "playstation5" const SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "switch_left_joycon" const SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "switch_right_joycon" const XBOX_BUTTON_LABELS = ["A", "B", "X", "Y", "Back", "Guide", "Start", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] const XBOX_ONE_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] const XBOX_SERIES_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"] const STEAMDECK_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "?", "Options", "Left Stick", "Right Stick", "L1", "R1", "Up", "Down", "Left", "Right", "", "", "", "", ""] # Note: share and home buttons are not recognized const SWITCH_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "LS", "RS", "Up", "Down", "Left", "Right", "Capture"] # Mapping for left and right joypad connected together (extended gamepad) # Left Stick is Axis 0 and 1 # Right Stick is Axis 2 and 3 # ZL and ZR are Axis 4 and 5 const SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "L", "R", "Up", "Down", "Left", "Right", "Capture"] const PLAYSTATION_3_4_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Share", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"] # Note: Microphone does not work on PC / touchpad is not recognized const PLAYSTATION_5_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Create", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"] const SERIAL_VERSION = 1 ## The deadzone to ignore for joypad motion var deadzone: float = 0.5 ## The mouse distance to ignore before movement is assumed var mouse_motion_threshold: int = 100 ## The last known joypad device name (or "" if no joypad detected) var last_known_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0)) ## The last known joypad index var last_known_joypad_index: int = 0 if Input.get_connected_joypads().size() > 0 else -1 ## Used internally var device_last_changed_at: int = 0 var _last_known_granular_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0), true) @onready var device: String = guess_device_name() @onready var device_index: int = 0 if has_joypad() else -1 func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS if not Engine.has_singleton("InputHelper"): Engine.register_singleton("InputHelper", self) Input.joy_connection_changed.connect(func(device_index, is_connected): joypad_changed.emit(device_index, is_connected)) func _input(event: InputEvent) -> void: var next_device: String = device var next_device_index: int = device_index # Did we just press a key on the keyboard or move the mouse? if event is InputEventKey \ or event is InputEventMouseButton \ or (event is InputEventMouseMotion and (event as InputEventMouseMotion).relative.length_squared() > mouse_motion_threshold): next_device = DEVICE_KEYBOARD next_device_index = -1 # Did we just use a joypad? elif event is InputEventJoypadButton \ or (event is InputEventJoypadMotion and abs(event.axis_value) > deadzone): next_device = get_simplified_device_name(get_joy_name(event.device)) last_known_joypad_device = next_device next_device_index = event.device last_known_joypad_index = next_device_index _last_known_granular_joypad_device = get_simplified_device_name(get_joy_name(event.device), true) # Debounce changes for 1 second because some joypads register twice in Windows for some reason var not_changed_in_last_second = Engine.get_frames_drawn() - device_last_changed_at > Engine.get_frames_per_second() if (next_device != device or next_device_index != device_index) and not_changed_in_last_second: device_last_changed_at = Engine.get_frames_drawn() device = next_device device_index = next_device_index device_changed.emit(device, device_index) ## Get the name of a joypad func get_joy_name(at_device_index: int) -> String: var joy_name: String = Input.get_joy_name(at_device_index) if joy_name == "" and Input.get_joy_info(at_device_index).size() > 0 and "xinput" in Input.get_joy_info(at_device_index).keys()[0]: joy_name = "XInput" return joy_name ## Get the device name for an event func get_device_from_event(event: InputEvent) -> String: if event is InputEventKey or event is InputEventMouseButton or event is InputEventMouseMotion: return DEVICE_KEYBOARD elif event is InputEventJoypadButton or event is InputEventJoypadMotion: return get_simplified_device_name(get_joy_name(event.device)) else: return DEVICE_GENERIC ## Get the device name for an event func get_device_index_from_event(event: InputEvent) -> int: if event is InputEventJoypadButton or event is InputEventJoypadMotion: return event.device else: return -1 ## Convert a Godot device identifier to a simplified string func get_simplified_device_name(raw_name: String, force_granular_identifier: bool = false) -> String: var use_granular_identifier: bool = force_granular_identifier or InputHelperSettings.get_setting(InputHelperSettings.USE_GRANULAR_DEVICE_IDENTIFIERS, false) var keywords: Dictionary = { SUB_DEVICE_XBOX_ONE_CONTROLLER: ["Xbox One Controller"], SUB_DEVICE_XBOX_SERIES_CONTROLLER: ["Xbox Series Controller", "Xbox Wireless Controller"], DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"], SUB_DEVICE_PLAYSTATION3_CONTROLLER: ["PS3"], SUB_DEVICE_PLAYSTATION4_CONTROLLER:["Nacon Revolution Unlimited Pro Controller", "PS4", "DUALSHOCK 4"], SUB_DEVICE_PLAYSTATION5_CONTROLLER:["Sony DualSense", "PS5", "DualSense Wireless Controller"], DEVICE_STEAMDECK_CONTROLLER: ["Steam"], DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con (L/R)", "PowerA Core Controller"], SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)"], SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["joy-Con (R)"], } if use_granular_identifier else { DEVICE_XBOX_CONTROLLER: ["XBox", "XInput"], DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS3", "PS5", "PS4", "DUALSHOCK 4", "DualSense", "Nacon Revolution Unlimited Pro Controller"], DEVICE_STEAMDECK_CONTROLLER: ["Steam"], DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con", "PowerA Core Controller"], } for device_key in keywords: for keyword in keywords[device_key]: if keyword.to_lower() in raw_name.to_lower(): return device_key return DEVICE_GENERIC ## Check if there is a connected joypad func has_joypad() -> bool: return Input.get_connected_joypads().size() > 0 ## Guess the initial input device func guess_device_name() -> String: if has_joypad(): return get_simplified_device_name(get_joy_name(0)) else: return DEVICE_KEYBOARD #region Mapping func reset_all_actions() -> void: InputMap.load_from_project_settings() for action in InputMap.get_actions(): var input: InputEvent = get_joypad_input_for_action(action) if input != null: joypad_input_changed.emit(action, input) input = get_keyboard_input_for_action(action) if input != null: keyboard_input_changed.emit(action, input) ## Set the key or button for an action func set_keyboard_or_joypad_input_for_action(action: String, event: InputEvent, swap_if_taken: bool = true) -> void: if event is InputEventKey or event is InputEventMouse: set_keyboard_input_for_action(action, event, swap_if_taken) elif event is InputEventJoypadButton: set_joypad_input_for_action(action, event, swap_if_taken) ## Get the key or button for a given action depending on the current device func get_keyboard_or_joypad_input_for_action(action: String) -> InputEvent: if device == DEVICE_KEYBOARD: return get_keyboard_input_for_action(action) else: return get_joypad_input_for_action(action) ## Get the key or button for a given action depending on the current device func get_keyboard_or_joypad_inputs_for_action(action: String) -> Array[InputEvent]: if device == DEVICE_KEYBOARD: return get_keyboard_inputs_for_action(action) else: return get_joypad_inputs_for_action(action) ## Get a text label for a given input func get_label_for_input(input: InputEvent) -> String: if input == null: return "" if input is InputEventKey: if input.physical_keycode > 0 : var keycode: Key = DisplayServer.keyboard_get_keycode_from_physical(input.physical_keycode) if DisplayServer.keyboard_get_current_layout() > -1 else input.physical_keycode return OS.get_keycode_string(keycode) elif input.keycode > 0: return OS.get_keycode_string(input.keycode) else: return input.as_text() elif input is InputEventMouseButton: match input.button_index: MOUSE_BUTTON_LEFT: return "Mouse Left Button" MOUSE_BUTTON_MIDDLE: return "Mouse Middle Button" MOUSE_BUTTON_RIGHT: return "Mouse Right Button" return "Mouse Button %d" % input.button_index elif input is InputEventJoypadButton: var labels = [] match _last_known_granular_joypad_device: DEVICE_XBOX_CONTROLLER, DEVICE_GENERIC: labels = XBOX_BUTTON_LABELS SUB_DEVICE_XBOX_ONE_CONTROLLER: labels = XBOX_ONE_BUTTON_LABELS SUB_DEVICE_XBOX_SERIES_CONTROLLER: labels = XBOX_SERIES_BUTTON_LABELS SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER, SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: labels = SWITCH_BUTTON_LABELS DEVICE_SWITCH_CONTROLLER: labels = SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS SUB_DEVICE_PLAYSTATION3_CONTROLLER, SUB_DEVICE_PLAYSTATION4_CONTROLLER: labels = PLAYSTATION_3_4_BUTTON_LABELS DEVICE_PLAYSTATION_CONTROLLER, SUB_DEVICE_PLAYSTATION5_CONTROLLER: labels = PLAYSTATION_5_BUTTON_LABELS DEVICE_STEAMDECK_CONTROLLER: labels = STEAMDECK_BUTTON_LABELS if input.button_index < labels.size(): return "%s Button" % labels[input.button_index] else: return "Button %d" % input.button_index elif input is InputEventJoypadMotion: var motion: InputEventJoypadMotion = input as InputEventJoypadMotion match motion.axis: JOY_AXIS_LEFT_X: return "Left Stick %s" % ("Left" if motion.axis_value < 0 else "Right") JOY_AXIS_LEFT_Y: return "Left Stick %s" % ("Up" if motion.axis_value < 0 else "Down") JOY_AXIS_RIGHT_X: return "Right Stick %s" % ("Left" if motion.axis_value < 0 else "Right") JOY_AXIS_RIGHT_Y: return "Right Stick %s" % ("Up" if motion.axis_value < 0 else "Down") JOY_AXIS_TRIGGER_LEFT: return "Left Trigger" JOY_AXIS_TRIGGER_RIGHT: return "Right Trigger" return input.as_text() ## Serialize a single action's inputs. func serialize_inputs_for_action(action: StringName) -> String: var action_inputs: PackedStringArray = [] var inputs: Array[InputEvent] = InputMap.action_get_events(action) for input in inputs: if input is InputEventKey: var s: String = get_label_for_input(input) var modifiers: Array[String] = [] if input.alt_pressed: modifiers.append("alt") if input.shift_pressed: modifiers.append("shift") if input.ctrl_pressed: modifiers.append("ctrl") if input.meta_pressed: modifiers.append("meta") if not modifiers.is_empty(): s += "|" + ",".join(modifiers) action_inputs.append("key:%s" % s) elif input is InputEventMouseButton: action_inputs.append("mouse:%d" % input.button_index) elif input is InputEventJoypadButton: action_inputs.append("joypad:%d" % input.button_index) elif input is InputEventJoypadMotion: action_inputs.append("joypad:%d|%f" % [input.axis, input.axis_value]) return ";".join(action_inputs) ## Serialize a list of action inputs to string. If actions is empty then it will serialize ## all actions. func serialize_inputs_for_actions(actions: PackedStringArray = []) -> String: if actions == null or actions.is_empty(): actions = InputMap.get_actions() var map: Dictionary = {} for action in actions: map[action] = serialize_inputs_for_action(action) return JSON.stringify({ version = SERIAL_VERSION, map = map }) ## Deserialize a single action's inputs. func deserialize_inputs_for_action(action: String, string: String) -> void: InputMap.action_erase_events(action) var action_inputs: PackedStringArray = string.split(";") for action_input in action_inputs: var bits: PackedStringArray = action_input.split(":") # Ignore any empty actions if bits.size() < 2: continue var input_type: String = bits[0] var input_details: String = bits[1] match input_type: "key": var keyboard_input = InputEventKey.new() if "|" in input_details: var detail_bits = input_details.split("|") keyboard_input.keycode = OS.find_keycode_from_string(detail_bits[0]) detail_bits = detail_bits[1].split(",") if detail_bits.has("alt"): keyboard_input.alt_pressed = true if detail_bits.has("shift"): keyboard_input.shift_pressed = true if detail_bits.has("ctrl"): keyboard_input.ctrl_pressed = true if detail_bits.has("meta"): keyboard_input.meta_pressed = true else: keyboard_input.keycode = OS.find_keycode_from_string(input_details) InputMap.action_add_event(action, keyboard_input) keyboard_input_changed.emit(action, keyboard_input) "mouse": var mouse_input = InputEventMouseButton.new() mouse_input.button_index = int(input_details) InputMap.action_add_event(action, mouse_input) keyboard_input_changed.emit(action, mouse_input) "joypad": if "|" in str(input_details): var joypad_motion_input = InputEventJoypadMotion.new() var joypad_bits = input_details.split("|") joypad_motion_input.axis = int(joypad_bits[0]) joypad_motion_input.axis_value = float(joypad_bits[1]) InputMap.action_add_event(action, joypad_motion_input) joypad_input_changed.emit(action, joypad_motion_input) else: var joypad_input = InputEventJoypadButton.new() joypad_input.button_index = int(input_details) InputMap.action_add_event(action, joypad_input) joypad_input_changed.emit(action, joypad_input) ## Deserialise a list of actions' inputs. func deserialize_inputs_for_actions(string: String) -> void: var data: Dictionary = JSON.parse_string(string) # Use legacy deserialization if not data.has("version"): _deprecated_deserialize_inputs_for_actions(string) return # Version 1 for action in data.map.keys(): deserialize_inputs_for_action(action, data.map[action]) # Load inputs from a serialized string. [deprecated] func _deprecated_deserialize_inputs_for_actions(string: String) -> void: var map: Dictionary = JSON.parse_string(string) for action in map.keys(): InputMap.action_erase_events(action) for key in map[action]["keyboard"]: var keyboard_input = InputEventKey.new() if "|" in key: var bits = key.split("|") keyboard_input.keycode = OS.find_keycode_from_string(bits[0]) bits = bits[1].split(",") if bits.has("alt"): keyboard_input.alt_pressed = true if bits.has("shift"): keyboard_input.shift_pressed = true if bits.has("ctrl"): keyboard_input.ctrl_pressed = true if bits.has("meta"): keyboard_input.meta_pressed = true else: keyboard_input.keycode = OS.find_keycode_from_string(key) InputMap.action_add_event(action, keyboard_input) for button_index in map[action]["mouse"]: var mouse_input = InputEventMouseButton.new() mouse_input.button_index = int(button_index) InputMap.action_add_event(action, mouse_input) for button_index_or_motion in map[action]["joypad"]: if "|" in str(button_index_or_motion): var joypad_motion_input = InputEventJoypadMotion.new() var bits = button_index_or_motion.split("|") joypad_motion_input.axis = int(bits[0]) joypad_motion_input.axis_value = float(bits[1]) InputMap.action_add_event(action, joypad_motion_input) else: var joypad_input = InputEventJoypadButton.new() joypad_input.button_index = int(button_index_or_motion) InputMap.action_add_event(action, joypad_input) #endregion #region Keyboard/mouse input ## Get all of the keys/mouse buttons used for an action. func get_keyboard_inputs_for_action(action: String) -> Array[InputEvent]: return InputMap.action_get_events(action).filter(func(event): return event is InputEventKey or event is InputEventMouseButton ) ## Get the first key for an action func get_keyboard_input_for_action(action: String) -> InputEvent: var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action) return null if inputs.is_empty() else inputs[0] ## Set the key used for an action func set_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error: return _update_keyboard_input_for_action(action, input, swap_if_taken, null) ## Replace a specific key with another key func replace_keyboard_input_for_action(action: String, current_input: InputEvent, input: InputEvent, swap_if_taken: bool = true) -> Error: return _update_keyboard_input_for_action(action, input, swap_if_taken, current_input) ## Replace a specific key, given its index func replace_keyboard_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error: var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action) var replacing_input = InputEventKey.new() if (inputs.is_empty() or inputs.size() <= index) else inputs[index] return _update_keyboard_input_for_action(action, input, swap_if_taken, replacing_input) func _update_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent = null) -> Error: if not (input is InputEventKey or input is InputEventMouseButton): return ERR_INVALID_DATA var is_valid_keyboard_event = func(event): return event is InputEventKey or event is InputEventMouseButton return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, keyboard_input_changed) #endregion #region Joypad input ## Get all buttons used for an action func get_joypad_inputs_for_action(action: String) -> Array[InputEvent]: return InputMap.action_get_events(action).filter(func(event): return event is InputEventJoypadButton or event is InputEventJoypadMotion ) ## Get the first button for an action func get_joypad_input_for_action(action: String) -> InputEvent: var buttons: Array[InputEvent] = get_joypad_inputs_for_action(action) return null if buttons.is_empty() else buttons[0] ## Set the button for an action func set_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error: return _update_joypad_input_for_action(action, input, swap_if_taken, null) ## Replace a specific button for an action func replace_joypad_input_for_action(action: String, current_input: InputEvent, input: InputEventJoypadButton, swap_if_taken: bool = true) -> Error: return _update_joypad_input_for_action(action, input, swap_if_taken, current_input) ## Replace a button, given its index func replace_joypad_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error: var inputs: Array[InputEvent] = get_joypad_inputs_for_action(action) var replacing_input if inputs.is_empty() or inputs.size() <= index: replacing_input = InputEventJoypadButton.new() replacing_input.button_index = JOY_BUTTON_INVALID else: replacing_input = inputs[index] return _update_joypad_input_for_action(action, input, swap_if_taken, replacing_input) ## Set the action used for a button func _update_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true, replacing_input: InputEvent = null) -> Error: var is_valid_keyboard_event = func(event): return event is InputEventJoypadButton or event is InputEventJoypadMotion return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, joypad_input_changed) func _update_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent, check_is_valid: Callable, did_change_signal: Signal) -> Error: # Find any action that is already mapped to this input var clashing_action = "" var clashing_event if swap_if_taken: for other_action in InputMap.get_actions(): if other_action == action: continue for event in InputMap.action_get_events(other_action): if event.is_match(input): clashing_action = other_action clashing_event = event # Find the key based event for the target action var action_events: Array[InputEvent] = InputMap.action_get_events(action) var is_replacing: bool = false for i in range(0, action_events.size()): var event: InputEvent = action_events[i] if check_is_valid.call(event): if replacing_input != null and not event.is_match(replacing_input): continue # Remap the other event if there is a clashing one if clashing_action: _update_input_for_action(clashing_action, event, false, clashing_event, check_is_valid, did_change_signal) # Replace the event action_events[i] = input is_replacing = true break # If we were trying to replace something but didn't find it then just add it to the end if not is_replacing: action_events.append(input) # Apply the changes InputMap.action_erase_events(action) for event in action_events: if event != null: InputMap.action_add_event(action, event) did_change_signal.emit(action, input) return OK #endregion #region Rumbling func rumble_small(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0.4, 0, 0.1) func rumble_medium(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0, 0.7, 0.1) func rumble_large(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0, 1, 0.1) func start_rumble_small(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0.4, 0, 0) func start_rumble_medium(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0, 0.7, 0) func start_rumble_large(target_device: int = 0) -> void: Input.start_joy_vibration(target_device, 0, 1, 0) func stop_rumble(target_device: int = 0) -> void: Input.stop_joy_vibration(target_device) #endregion