From d0d97ba9bfcd5a3bcc97d6dbf2e37e2f7bb10df5 Mon Sep 17 00:00:00 2001 From: bolinfest Date: Mon, 14 Mar 2022 14:11:19 -0700 Subject: [PATCH] feat: add gamepad support to escoria-ui-simplemouse (#518) This commit starts by introducing a new action, `ESC_UI_PRIMARY_ACTION`, that is used to represent a "primary action" from an input device, such as a left-click on a mouse or the press of the primary button on a gamepad. Although this action is not added to `InputMap` by default in `escoria-core`, `_unhandled_input()` in `esc_item.gd` is updated to look for it. The other portion of this commit is an update to `addons/escoria-ui-simplemouse/game.gd` that adds a gamepad mapping to the `InputMap` for both `ESC_UI_PRIMARY_ACTION` as well as another new action `ESC_UI_CHANGE_VERB_ACTION`. These actions are mapped to X and Y on an XBox controller, respectively. Note that `game.gd` is also updated to implement `_process()` such that moving `JOY_AXIS_0` and `JOY_AXIS_1` on "gamepad 0" will move the cursor around on the screen as the mouse would. Overall, this makes the game fairly playable with a gamepad, though admittedly a mouse is still currently required to click the "New game" button on the initial menu because `BaseButton::gui_input()` appears to be hardcoded to check for mouse events exclusively: godotengine/godot@a09814e/scene/gui/base_button.cpp#L55-L81 --- .../game/core-scripts/esc_item.gd | 22 +++- addons/escoria-core/game/inputs_manager.gd | 4 + addons/escoria-ui-simplemouse/game.gd | 112 ++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/addons/escoria-core/game/core-scripts/esc_item.gd b/addons/escoria-core/game/core-scripts/esc_item.gd index adf553022..c11e25dea 100644 --- a/addons/escoria-core/game/core-scripts/esc_item.gd +++ b/addons/escoria-core/game/core-scripts/esc_item.gd @@ -268,8 +268,8 @@ func _ready(): # # #### Parameters # -# - event: Triggered event -func _unhandled_input(event: InputEvent) -> void: +# - input_event: Triggered event +func _unhandled_input(input_event: InputEvent) -> void: # If this is a trigger, then escoria.inputs_manager is not wired up to # receive the signals this function might dispatch. In particular, # calling get_tree().set_input_as_handled() unnecessarily will prevent @@ -278,6 +278,24 @@ func _unhandled_input(event: InputEvent) -> void: if is_trigger: return + var event = input_event + # Note that event could be InputEventMouseButton, InputEventJoypadButton, + # or something else. As such, the value of the `button_index` property + # must be read in the context of the type of input event. + if input_event is InputEventJoypadButton: + if not input_event.is_action_pressed(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION): + return + + # For now, rather than refactor input handling to be more generic + # to accommodate gamepad support, we create a synthetic mouse event + # based on the InputEventJoypadButton. + event = InputEventMouseButton.new() + event.button_index = BUTTON_LEFT + event.doubleclick = false + event.pressed = true + # ESCActionManager expects to read the position off of the event. + event.position = get_global_mouse_position() + if event is InputEventMouseButton and event.is_pressed(): if not escoria.current_state == escoria.GAME_STATE.DEFAULT: escoria.logger.info("Game state doesn't accept interactions") diff --git a/addons/escoria-core/game/inputs_manager.gd b/addons/escoria-core/game/inputs_manager.gd index 7d54c88fc..56d8cc618 100644 --- a/addons/escoria-core/game/inputs_manager.gd +++ b/addons/escoria-core/game/inputs_manager.gd @@ -21,6 +21,10 @@ const SWITCH_ACTION_VERB = "switch_action_verb" # Input action for use by InputMap const ESC_SHOW_DEBUG_PROMPT = "esc_show_debug_prompt" +# Input action for use by InputMap that represents a "primary action" from an +# input device, such as a left-click on a mouse or the X button on an XBox +# controller +const ESC_UI_PRIMARY_ACTION = "esc_ui_primary_action" # The current input mode var input_mode = INPUT_ALL diff --git a/addons/escoria-ui-simplemouse/game.gd b/addons/escoria-ui-simplemouse/game.gd index ec9b086b4..3acc20caa 100644 --- a/addons/escoria-ui-simplemouse/game.gd +++ b/addons/escoria-ui-simplemouse/game.gd @@ -41,6 +41,31 @@ Implement methods to react to inputs. - _on_event_done(event_name: String) """ +# Value to use for `device` argument to various `Input.get_joy` methods. +const JOY_DEVICE = 0 + +# See https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html?#dead-zone +const DEADZONE = 0.2 + +# Multiplier to apply to axis when it exceeds DEADZONE. +const AXIS_WEIGHT = 50.0 + +# JOY_BUTTON_2 corresponds to the "X" button on an XBox controller +# or the Square button on a Playstation controller. These appear to +# map to the "primary action," in practice, so we treat it like a left click. +const PRIMARY_ACTION_BUTTON = JOY_BUTTON_2 + +# JOY_BUTTON_3 corresponds to the "Y" button on an XBox controller +# or the Triangle button on a Playstation controller. These appear to +# map to the "secondary action," in practice, so we treat it like a right click. +const CHANGE_VERB_BUTTON = JOY_BUTTON_3 + +# Input action for use by InputMap +const ESC_UI_CHANGE_VERB_ACTION = "esc_change_verb" + +# true when a gamepad is connected. +var _is_gamepad_connected = false + func _enter_tree(): var room_selector_parent = $CanvasLayer/ui/HBoxContainer/VBoxContainer @@ -53,6 +78,21 @@ func _enter_tree(): ).instance() ) + var input_handler = funcref(self, "_process_input") + escoria.inputs_manager.register_custom_input_handler(input_handler) + + _is_gamepad_connected = Input.is_joy_known(JOY_DEVICE) + if _is_gamepad_connected: + _on_gamepad_connected() + + Input.connect("joy_connection_changed", self, "_on_joy_connection_changed") + + +func _exit_tree(): + escoria.inputs_manager.register_custom_input_handler(null) + Input.disconnect("joy_connection_changed", self, "_on_joy_connection_changed") + _on_gamepad_disconnected() + func _input(event: InputEvent) -> void: if escoria.main.current_scene and escoria.main.current_scene.game: @@ -61,6 +101,78 @@ func _input(event: InputEvent) -> void: update_tooltip_following_mouse_position(event.position) +# https://github.com/godotengine/godot-demo-projects/blob/3.4-585455e/misc/joypads/joypads.gd +# was informative in wiring up the gamepad properly. + +func _on_gamepad_connected(): + set_physics_process(true) + + var primary_event = InputEventJoypadButton.new() + primary_event.button_index = PRIMARY_ACTION_BUTTON + InputMap.add_action(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION) + InputMap.action_add_event(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION, primary_event) + + var verb_event = InputEventJoypadButton.new() + verb_event.button_index = CHANGE_VERB_BUTTON + InputMap.add_action(ESC_UI_CHANGE_VERB_ACTION) + InputMap.action_add_event(ESC_UI_CHANGE_VERB_ACTION, verb_event) + + +func _on_gamepad_disconnected(): + InputMap.action_erase_events(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION) + InputMap.erase_action(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION) + + InputMap.action_erase_events(ESC_UI_CHANGE_VERB_ACTION) + InputMap.erase_action(ESC_UI_CHANGE_VERB_ACTION) + + set_physics_process(false) + _is_gamepad_connected = false + + +func _on_joy_connection_changed(device: int, connected: bool) -> void: + if device != JOY_DEVICE: + return + elif connected: + _on_gamepad_connected() + else: + _on_gamepad_disconnected() + + +func _process(_delta) -> void: + if !_is_gamepad_connected: + return + + var x = Input.get_joy_axis(JOY_DEVICE, JOY_AXIS_0) + var y = Input.get_joy_axis(JOY_DEVICE, JOY_AXIS_1) + var delta_x = int(x * AXIS_WEIGHT) if abs(x) > DEADZONE else 0 + var delta_y = int(y * AXIS_WEIGHT) if abs(y) > DEADZONE else 0 + if delta_x or delta_y: + var direction: Vector2 + direction.x = delta_x + direction.y = delta_y + escoria.logger.trace("gamepad direction:", [direction]) + var viewport = get_viewport() + viewport.warp_mouse(viewport.get_mouse_position() + direction) + + +func _process_input(event: InputEvent, is_default_state: bool) -> bool: + if not is_default_state: + # ESCBackground is not guaranteed to be set, as we may be on + # the "New Game" screen. + return false + elif _is_gamepad_connected and event is InputEventJoypadButton: + escoria.logger.trace("InputEventJoypadButton:", [event.as_text()]) + if event.is_action_pressed(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION): + # Admittedly, this breaks abstraction barriers and is completely + # inappropriate, but it's what works right now. + escoria.inputs_manager._on_left_click_on_bg(get_global_mouse_position()) + return true + elif event.is_action_pressed(ESC_UI_CHANGE_VERB_ACTION): + mousewheel_action(1) + return true + return false + + ## BACKGROUND ## func left_click_on_bg(position: Vector2) -> void: