└── addons └── input_controller ├── icon.png ├── plugin.gd ├── plugin.cfg ├── action_state.gd ├── icon.png.import ├── icon.svg.import ├── action_handler_map.gd ├── LICENSE ├── icon.svg ├── README.md └── input_controller.gd /addons/input_controller/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sscovil/godot-input-controller-addon/HEAD/addons/input_controller/icon.png -------------------------------------------------------------------------------- /addons/input_controller/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | pass 7 | 8 | 9 | func _exit_tree(): 10 | pass 11 | -------------------------------------------------------------------------------- /addons/input_controller/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="InputController" 4 | description="Improved detection of action input events (e.g. tap, double tap, press, long press)." 5 | author="Shaun Scovil" 6 | version="1.0.2" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/input_controller/action_state.gd: -------------------------------------------------------------------------------- 1 | class_name ActionState 2 | extends Node 3 | ## This class is used in the InputController._actions dictionary to hold the state of each action. 4 | 5 | var last_activated_at: float = 0 6 | var prev_activated_at: float = 0 7 | 8 | var is_active: bool: 9 | get: return last_activated_at > 0 10 | 11 | var is_possible_double_tap: bool: 12 | get: return prev_activated_at > 0 13 | -------------------------------------------------------------------------------- /addons/input_controller/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://n6kfossq1mag" 6 | path="res://.godot/imported/icon.png-e493b81bb0ba181dbfc40e5a9318df18.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/input_controller/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-e493b81bb0ba181dbfc40e5a9318df18.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/input_controller/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://by7vmbi3eboqq" 6 | path="res://.godot/imported/icon.svg-9221645567d14f15f308539de65abefb.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/input_controller/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-9221645567d14f15f308539de65abefb.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/input_controller/action_handler_map.gd: -------------------------------------------------------------------------------- 1 | class_name ActionHandlerMap 2 | extends Object 3 | ## This class is used to store a list of input actions for each InputController handler method. 4 | 5 | var _input: Array[StringName] = [] 6 | var _unhandled_shortcuts: Array[StringName] = [] 7 | var _unhandled_key_input: Array[StringName] = [] 8 | var _unhandled_input: Array[StringName] = [] 9 | 10 | 11 | func add_action(method: String, action: StringName) -> void: 12 | self.get(method).push_back(action) 13 | 14 | 15 | func clear(method: String = "") -> void: 16 | if method: 17 | self.get(method).clear() 18 | else: 19 | _input.clear() 20 | _unhandled_shortcuts.clear() 21 | _unhandled_key_input.clear() 22 | _unhandled_input.clear() 23 | 24 | 25 | func has_actions(method: String) -> bool: 26 | return self.get(method).size() > 0 27 | 28 | 29 | func get_actions(method: String) -> Array[StringName]: 30 | return self.get(method) 31 | 32 | 33 | func remove_action(method: String, action: StringName) -> void: 34 | self.get(method).pop_at(self.get(method).find(action)) 35 | -------------------------------------------------------------------------------- /addons/input_controller/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024-present Shaun Scovil 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 9 | the Software. 10 | 11 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 12 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 14 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 15 | SOFTWARE. 16 | -------------------------------------------------------------------------------- /addons/input_controller/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/input_controller/README.md: -------------------------------------------------------------------------------- 1 |

2 | InputController 3 |

4 | 5 |

6 | Easily differentiate between a button tap, double tap, press, long press, and hold for all of your input actions in Godot. 7 |

8 | 9 |

10 | Godot v4.2+ 11 | Latest InputController Release 12 | GitHub Repo Stars 13 |

14 | 15 |

16 | InputController Icon 17 |

18 | 19 | ## Table of Contents 20 | 21 | - [Version](#version) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [Configuration](#configuration) 25 | - [Methods](#methods) 26 | - [Signals](#signals) 27 | - [Troubleshooting](#troubleshooting) 28 | - [License](#license) 29 | 30 | ## Version 31 | 32 | InputController **requires at least Godot 4.2**. It may work with earlier versions, but they have not been tested. 33 | 34 | ## Installation 35 | 36 | Let's install InputController into your Godot project: 37 | 38 | - Download the `.zip` or `tar.gz` file for your desired InputController version [here](https://github.com/sscovil/godot-input-controller-addon/releases). 39 | - Extract the `addons` folder from this file. 40 | - Move the `addons` folder to your Godot project folder. 41 | 42 | Now, let's verify you have correctly installed InputController: 43 | 44 | - You have this folder path `res://addons/input_controller`. 45 | - Head to `Project > Project Settings`. 46 | - Click the `Plugins` tab. 47 | - Tick the `enabled` button next to InputController. 48 | - Restart Godot. 49 | 50 | ## Usage 51 | 52 | To get started, simply add an `InputController` node to your scene tree. This node will automatically start listening 53 | for input events and emitting a signal when it detects a tap, double tap, press, long press, or hold for any action. 54 | 55 | Once you've added the `InputController` to your scene tree, simply connect an event handler function in your script to 56 | the `input_detected` signal: 57 | 58 | ```gdscript 59 | const InputType = InputController.InputType 60 | @onready var input_controller = $InputController 61 | 62 | func _ready(): 63 | input_controller.input_detected.connect(_on_input_detected) 64 | 65 | func _on_input_detected(event: InputEvent, action: String, input_type: InputType): 66 | match input_type: 67 | InputType.TAP: 68 | prints(action, "tapped") 69 | InputType.DOUBLE_TAP: 70 | prints(action, "double tapped") 71 | InputType.PRESS: 72 | prints(action, "pressed") 73 | InputType.LONG_PRESS: 74 | prints(action, "long pressed") 75 | InputType.HOLD: 76 | prints(action, "held") 77 | ``` 78 | 79 | See the [Signals](#signals) section below, for more information on the `input_detected` signal. 80 | 81 | ## Configuration 82 | 83 | The following exported values can be modified in the Godot Editor Inspector, or programmatically 84 | by directly accessing the properties of the node. 85 | 86 | Here is an example of how you can modify the settings in a script: 87 | 88 | ```gdscript 89 | @onready var input_controller = $InputController 90 | 91 | func _ready(): 92 | # Input Timing 93 | input_controller.max_button_tap = 0.18 94 | input_controller.max_double_tap_delay = 0.12 95 | input_controller.max_button_press = 0.45 96 | input_controller.max_long_press = 0.85 97 | 98 | # Input Handlers 99 | input_controller.ui_inputs = ["ui_*", "menu_*"] 100 | input_controller.shortcut_inputs = ["shortcut_*", "quit_game"] 101 | input_controller.unhandled_key_inputs = ["*_key"] 102 | input_controller.unhandled_inputs = ["player_*_action", "player_*_move"] 103 | 104 | # Event Propagation 105 | input_controller.set_input_as_handled = true # Default value 106 | ``` 107 | 108 | ### Input Timing Configuration 109 | 110 | Use these settings to fine tune the timing used to differentiate between a tap, double tap, press, long press, and 111 | hold. These are `float` values measured in seconds, so you can get very precise. 112 | 113 | | Inspector Label | Property Name | Type | Default | 114 | |----------------------|------------------------|---------|---------| 115 | | Max Button Tap | `max_button_tap` | `float` | `0.2` | 116 | | Max Double Tap Delay | `max_double_tap_delay` | `float` | `0.1` | 117 | | Max Button Press | `max_button_press` | `float` | `0.5` | 118 | | Max Long Tap | `max_long_press` | `float` | `1` | 119 | 120 | ### Input Handlers Configuration 121 | 122 | Use these settings to customize which event handlers are used to detect different types of actions, and which input 123 | actions to listen for. 124 | 125 | | Inspector Label | Property Name | Type | Default | Method | 126 | |----------------------|------------------------|-----------------|------------|---------------------------| 127 | | UI Inputs | `ui_inputs` | `Array[String]` | `["ui_*"]` | [_input()] | 128 | | Shortcut Inputs | `shortcut_inputs` | `Array[String]` | `[]` | [_unhandled_shortcuts()] | 129 | | Unhandled Key Inputs | `unhandled_key_inputs` | `Array[String]` | `[]` | [_unhandled_key_inputs()] | 130 | | Unhandled Inputs | `unhandled_inputs` | `Array[String]` | `["*"]` | [_unhandled_input()] | 131 | 132 | [_input()]: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-input 133 | [_unhandled_shortcuts()]: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-unhandled-shortcuts 134 | [_unhandled_key_inputs()]: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-unhandled-key-inputs 135 | [_unhandled_input()]: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-unhandled-input 136 | 137 | Each array can have zero or more strings that represent the names of the actions you want to listen for. The `*` 138 | character is a wildcard that will match any string if used alone, or any part of a string if used in combination with 139 | other characters. 140 | 141 | For example: 142 | 143 | - `["ui_*"]` will match any action that starts with `ui_`. 144 | - `["*_key"]` will match any action that ends with `_key`. 145 | - `["player_*_action"]` will match any action that starts with `player_` and ends with `_action`. 146 | - `["shortcut_*", "quit_game"]` will match any action that starts with `shortcut_` or is exactly `quit_game`. 147 | - `["*"]` will match any action. 148 | 149 | By default, the [_input()] method will be used to handle all actions that start with `ui_`; and [_unhandled_input()] 150 | will be used to handle all other actions. This may or may not have a material impact on your game, but it's good to 151 | know if things aren't behaving as expected. 152 | 153 | More information about how input events are processed in Godot can be found 154 | [here](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html#how-does-it-work). 155 | 156 | ### Event Propagation Configuration 157 | 158 | If set to `true` (default value), the `InputController` will consume an `InputEvent` and stop it from propagating to 159 | other nodes by calling `get_viewport().set_input_as_handled()`. 160 | 161 | To allow the event to propagate after handling it, set this value to `false`. You might want to do this if you are 162 | only using the `InputController` for logging, analytics, or some other observational behavior. 163 | 164 | | Inspector Label | Property Name | Type | Default | 165 | |----------------------|-------------------------|---------|---------| 166 | | Set Input as Handled | `set_input_as_handled` | `bool` | `true` | 167 | 168 | ## Methods 169 | 170 | The `InputController` node has the following methods: 171 | 172 | - `get_ticks()` 173 | - `find_actions(event: InputEvent, actions: Array[StringName])` 174 | - `map_actions_to_handlers(available_actions: Array[StringName] = InputMap.get_actions())` 175 | - `process_input(event: InputEvent, actions: Array[StringName])` 176 | 177 | ### get_ticks() 178 | 179 | This is a helper method that returns `Time.get_ticks_msec()` in seconds, as a `float`. It is used internally to compare 180 | the time elapsed between inputs with the [Input Timing](#input-timing-configuration) configuration values, to determine 181 | the type of input action. 182 | 183 | ### find_actions(event: InputEvent, actions: Array[StringName]) 184 | 185 | This is a helper method that filters a given array of actions, returning a new array that contains any actions that 186 | evaluate to `true` when passed to `event.is_action()` for the given `InputEvent`. 187 | 188 | ### map_actions_to_handlers(available_actions: Array[StringName] = InputMap.get_actions()) 189 | 190 | This method is used internally to apply the [Input Handlers](#input-handlers-configuration) configuration values. 191 | You should not need to call it manually; if you do, be aware that it will clear and override the configuration settings 192 | applied in the inspector section of the editor. 193 | 194 | ### process_input(event: InputEvent, actions: Array[StringName]) 195 | 196 | This is the primary method that is used at runtime. It is called from each of the four input handler methods 197 | (`_input()`, `_unhandled_input()`, `_unhandled_key_input()`, and `_unhandled_shortcuts()`) described 198 | [here](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html#how-does-it-work), based on your 199 | [Input Handlers](#input-handlers-configuration) configuration settings. 200 | 201 | For each action in the given `actions` array, it will emit the appropriate `input_detected` signal, based on the given 202 | `InputEvent`. Then, it will optionally mark the event as handled, based on your 203 | [Event Propagation](#event-propagation-configuration) configuration settings. 204 | 205 | ## Signals 206 | 207 | The `InputController` node emits the following signal: 208 | 209 | - `input_detected(event: InputEvent, action: StringName, type: InputController.InputType)` 210 | 211 | The signal will include three arguments: 212 | 213 | 1. `event`: The `InputEvent` that triggered the action 214 | 2. `action`: The name of the action that was triggered 215 | 3. `type`: The `InputType` of input that was detected. 216 | 217 | The `InputType` enum has the following values: 218 | 219 | - `ACTIVE`: An input action has just begun; its type is not yet determined. 220 | - `TAP`: A quick press and release of a button. 221 | - `DOUBLE_TAP`: Two quick, consecutive taps of a button. 222 | - `PRESS`: A standard press of a button. 223 | - `LONG_PRESS`: A press and slightly prolonged hold of a button. 224 | - `HOLD`: A press and hold of a button that exceeded the long press duration. 225 | - `CANCEL`: An input action that has been canceled and can be ignored. 226 | 227 | The `InputType.ACTIVE` value is used to indicate that an input event has just begun (i.e. the action was just pressed). 228 | The actual type of input will be determined when the button is released. 229 | 230 | The `InputType.CANCEL` value is used to negate the first tap in a double tap sequence. The signal for the first tap 231 | cannot be emitted until either a second tap is detected or the double tap delay has been exceeded; if a second tap 232 | is detected, the first tap gets canceled and can be ignored. 233 | 234 | ## Troubleshooting 235 | 236 | ### Input actions are not being detected 237 | 238 | The `InputController` will only receive an input event if it has not already been handled by a child node, or a sibling 239 | node that appears below it in the scene tree. 240 | 241 | Try creating a new scene with only the `InputController` node and see if the input actions are detected. If they are, 242 | then you know the actions are being handled elsewhere in your code before they reach the `InputController`. 243 | 244 | ### Input actions are being handled by the wrong handler method 245 | 246 | **IMPORTANT:** Be sure you are using a version of InputController >= `1.0.0`. In earlier versions, there was a bug that 247 | prevented custom configurations from being recognized. 248 | 249 | If you are using a version >= `1.0.0`, this is likely an issue with your 250 | [Input Handlers Configuration](#input-handlers-configuration). 251 | 252 | When using wildcards, be aware that the order of the handlers in the list matters. The first handler that matches an 253 | action will be the one that ends up handling it. 254 | 255 | For example, let's say you have the following configuration: 256 | 257 | 1. **UI Inputs**: `["ui_*", "*_menu"]` 258 | 2. **Shortcut Inputs**: `["shortcut_*"]` 259 | 3. **Unhandled Key Inputs**: `["*"]` 260 | 4. **Unhandled Inputs**: `["player_*"]` 261 | 262 | In this case, an action named `shortcut_menu` would be handled by the **UI Inputs** handler, because it matches `*_menu` 263 | and that handler gets first pick of the actions. Likewise, no actions would make it to the **Unhandled Inputs** handler, 264 | because the `*` wildcard was used in the **Unhandled Key Inputs** handler. 265 | 266 | ## License 267 | 268 | This project is licensed under the terms of the [MIT license](https://github.com/sscovil/godot-input-controller-addon/blob/main/LICENSE). 269 | -------------------------------------------------------------------------------- /addons/input_controller/input_controller.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/input_controller/icon.svg") 2 | class_name InputController 3 | extends Node 4 | 5 | signal input_detected(event: InputEvent, action: String, input_type: InputType) 6 | 7 | enum InputType { 8 | ACTIVE, 9 | TAP, 10 | DOUBLE_TAP, 11 | PRESS, 12 | LONG_PRESS, 13 | HOLD, 14 | CANCEL, 15 | } 16 | 17 | const ActionHandlerMap = preload("res://addons/input_controller/action_handler_map.gd") 18 | const ActionState = preload("res://addons/input_controller/action_state.gd") 19 | 20 | ## These values are used to determine the InputType of an InputEvent; all values are in seconds. 21 | @export_group("Input Timing") 22 | 23 | @export var max_button_tap: float = 0.2 # Max time for InputType.TAP. 24 | @export var max_double_tap_delay: float = 0.1 # Max time between taps for InputType.DOUBLE_TAP. 25 | @export var max_button_press: float = 0.5 # Max time for InputType.PRESS. 26 | @export var max_long_press: float = 1.0 # Max time for InputType.LONG_PRESS. 27 | 28 | ## These values are used to identify which actions will be handled by which InputController 29 | ## methods, based on the input event propagation lifecycle explained here: 30 | ## https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html#how-does-it-work 31 | ## 32 | ## By default, all actions that start with "ui_" will be handled by InputController._input(), and 33 | ## all other actions will be handled by InputController._unhandled_input(). This can be customized 34 | ## by changine these settings. 35 | ## 36 | ## The "*" value is used as a wildcard, so "ui_*" means any action that starts with "ui_"; "*_move" 37 | ## means any action that ends with "_move"; "player_*_attack" means any action that starts with 38 | ## "player_" and ends with "_attack"; and "*" means all remaining unhandled actions. 39 | ## 40 | ## More information about when to use each of the input event handler methods can be found here: 41 | ## 42 | ## https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-input 43 | ## https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-shortcut-input 44 | ## https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-unhandled-key-input 45 | ## https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-private-method-unhandled-input 46 | @export_group("Input Handlers") 47 | 48 | @export var ui_inputs: Array[String] = ["ui_*"] 49 | @export var shortcut_inputs: Array[String] = [] 50 | @export var unhandled_key_inputs: Array[String] = [] 51 | @export var unhandled_inputs: Array[String] = ["*"] 52 | 53 | ## If set to true (default), the InputController will consume InputEvents and stop them from 54 | ## propagating to other nodes by calling get_viewport().set_input_as_handled(). To allow the event 55 | ## to propagate after handling it, set this value to false. You might want to do this if you are only 56 | ## using the InputController for logging, analytics, or some other observational behavior. 57 | ## 58 | ## NOTE: The InputController will only receive the input event if it has not already been handled by 59 | ## a child node, or a sibling node that appears below it in the scene tree. 60 | @export_group("Event Propagation") 61 | 62 | @export var set_input_as_handled: bool = true 63 | 64 | ## Map of input handler method names to their respective settings (defined above). 65 | var settings: Dictionary = { 66 | "_input": &"ui_inputs", 67 | "_unhandled_shortcuts": &"shortcut_inputs", 68 | "_unhandled_key_input": &"unhandled_key_inputs", 69 | "_unhandled_input": &"unhandled_inputs", 70 | } 71 | 72 | ## RegEx pattern to find a "*" character in a string and, if present, capture the text around it. 73 | var wildcard: RegEx = RegEx.create_from_string("(.+)?\\*(.+)?") 74 | 75 | ## Collection of ActionState objects keyed by action name, used to track its current state. 76 | var _actions: Dictionary = {} 77 | 78 | ## Object that contains lists of actions that should be handled by each input handler method. 79 | var _handlers: ActionHandlerMap = ActionHandlerMap.new() 80 | 81 | 82 | func _ready() -> void: 83 | map_actions_to_handlers() 84 | 85 | 86 | func _exit_tree() -> void: 87 | _handlers.free() 88 | for action in _actions.values(): 89 | action.free() 90 | 91 | 92 | func _input(event: InputEvent) -> void: 93 | if _handlers.has_actions("_input"): 94 | process_input(event, find_actions(event, _handlers.get_actions("_input"))) 95 | 96 | 97 | func _unhandled_input(event: InputEvent) -> void: 98 | if _handlers.has_actions("_unhandled_input"): 99 | process_input(event, find_actions(event, _handlers.get_actions("_unhandled_input"))) 100 | 101 | 102 | func _unhandled_key_input(event: InputEvent) -> void: 103 | if _handlers.has_actions("_unhandled_key_input"): 104 | process_input(event, find_actions(event, _handlers.get_actions("_unhandled_key_input"))) 105 | 106 | 107 | func _unhandled_shortcuts(event: InputEvent) -> void: 108 | if _handlers.has_actions("_unhandled_shortcuts"): 109 | process_input(event, find_actions(event, _handlers.get_actions("_unhandled_shortcuts"))) 110 | 111 | 112 | ## Wrapper function for Time.get_ticks_msec() that returns the value in seconds, as a float. 113 | func get_ticks() -> float: 114 | return float(Time.get_ticks_msec()) / 1000 115 | 116 | 117 | ## Search a given list of actions and return an array of actions that match a given event. An 118 | ## InputEvent can match more than one action, because multiple actions can have the same keys, 119 | ## joypad buttons, joystick inputs, etc. mapped to them. 120 | ## 121 | ## @param event InputEvent: The event to check each action against. 122 | ## @param actions Array[StringName]: A list of actions to check. 123 | ## @return InputControllerAction: The first action that matches the event, or "" if no match found. 124 | func find_actions(event: InputEvent, actions: Array[StringName]) -> Array[StringName]: 125 | return actions.filter(func (a): return event.is_action(a)) 126 | 127 | 128 | ## Add each input action in a given list (or all actions from InputMap by default) to one of the 129 | ## input handler methods (_input, _unhandled_shortcuts, _unhandled_key_input, and _unhandled_input) 130 | ## based on InputController settings. 131 | ## 132 | ## @param available_actions Array[StringName]: Defaults to the value of InputMap.get_actions(). 133 | func map_actions_to_handlers(available_actions: Array[StringName] = InputMap.get_actions()) -> void: 134 | # Initialize the action arrays in _handlers. 135 | _handlers.clear() 136 | 137 | # Loop through each of the input handler methods in settings. 138 | for method in settings.keys(): 139 | # End the loop early if no actions are available. 140 | if !available_actions: 141 | break 142 | 143 | # Loop through each of the settings for the current method. 144 | for setting in get(settings[method]): 145 | # End the loop early if no actions are available. 146 | if !available_actions: 147 | break 148 | 149 | # If the current setting contains only the wildcard character, 150 | if "*" == setting: 151 | # ...loop through a copy of available_actions, so we can modify the original. 152 | for action in available_actions.duplicate(): 153 | # ...then add each action as a key to the _actions dictionary, 154 | _actions[action] = ActionState.new() 155 | # ...assign it to the _handlers dictionary under the current method, 156 | _handlers.add_action(method, action) 157 | # ...and remove it from the list of available actions. 158 | available_actions.pop_at(available_actions.find(action)) 159 | 160 | # End the loop early, since there are no more actions available. 161 | break 162 | 163 | # Check if the current setting contains a "*" wildcard character. 164 | var matches: RegExMatch = wildcard.search(setting) 165 | 166 | # If the current setting contains a wildcard, 167 | if matches: 168 | # ...grab the strings to the left and right of the wildcard, 169 | var prefix: String = matches.strings[1] # Can be an empty string. 170 | var suffix: String = matches.strings[2] # Can be an empty string. 171 | 172 | # ...then loop through a copy of available_actions, so we can modify the original. 173 | for action in available_actions.duplicate(): 174 | var has_prefix: bool = action.trim_prefix(prefix) != action 175 | var has_suffix: bool = action.trim_suffix(suffix) != action 176 | 177 | # If the action starts with prefix and ends with suffix, 178 | if (!prefix or has_prefix) and (!suffix or has_suffix): 179 | # ...add it as a key to the _actions dictionary, 180 | _actions[action] = ActionState.new() 181 | # ...assign it to the _handlers dictionary under the current method, 182 | _handlers.add_action(method, action) 183 | # ...and remove it from the list of available actions. 184 | available_actions.pop_at(available_actions.find(action)) 185 | 186 | # If the current setting does not contain a wildcard and matches an available action, 187 | elif setting in available_actions: 188 | # ...add it as a key to the _actions dictionary, 189 | _actions[setting] = ActionState.new() 190 | # ...assign it to the _handlers dictionary under the current method, 191 | _handlers.add_action(method, setting) 192 | # ...and remove it from the list of available actions. 193 | available_actions.pop_at(available_actions.find(setting)) 194 | 195 | 196 | ## Process InputEvent actions and, if InputController.set_input_as_handled is true, call 197 | ## get_viewport().set_input_as_handled() to prevent the InputEvent from propagating. 198 | ## 199 | ## @param event InputEvent: The event that triggered the action. 200 | ## @param actions Array[StringName]: The actions to process. 201 | ## @return bool: True if the event was processed; otherwise, false. 202 | func process_input(event: InputEvent, actions: Array[StringName]) -> bool: 203 | if !actions: 204 | return false # No action to process. 205 | 206 | for action: StringName in actions: 207 | _process_action(event, action) 208 | 209 | # If configured to do so, prevent the InputEvent from propagating to other nodes. 210 | if set_input_as_handled: 211 | get_viewport().set_input_as_handled() 212 | 213 | return true # Action was processed. 214 | 215 | 216 | ## Determine the InputType of a given InputEvent. This method is private because it updates the 217 | ## internal state of the InputController. It should only be called when certain conditions are met. 218 | ## 219 | ## This method is a coroutine and, as such, must be called using the `await` keyword. See also: 220 | ## https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#awaiting-for-signals-or-coroutines 221 | ## 222 | ## @param action ActionState: Current state of the action that triggered the InputEvent. 223 | ## @param delta float: Duration (in seconds) of the action input hold before it was released. 224 | ## @return InputType: The type of input, based on the duration of the action being held. 225 | func _determine_input_type(action_state: ActionState, delta: float) -> InputType: 226 | # If a previous input for the same action occurred within the max_double_tap_delay limit, 227 | # then the two inputs combined are treated as an InputType.DOUBLE_TAP. We need to reset 228 | # prev_activated_at, so the previous call will see that and return InputType.CANCEL instead of 229 | # erroneously reporting an additional InputType.TAP after it's timeout is finished. 230 | if action_state.is_possible_double_tap and delta <= max_double_tap_delay: 231 | action_state.prev_activated_at = 0 232 | return InputType.DOUBLE_TAP 233 | 234 | # If the duration of the input is within the max_button_tap limit, it could be the first of two 235 | # subsequent taps that are intended to be an InputType.DOUBLE_TAP. To determine that, we need to 236 | # cache the current time (using get_ticks() for millisecond precision) and then set a timeout, 237 | # to allow a subsequent tap to occur. If it does so within the max_double_tap_delay limit, the 238 | # subsequent call will have already reset our cached time and returned InputType.DOUBLE_TAP, so 239 | # we should return InputType.CANCEL. If not, we should return InputType.TAP. 240 | if delta <= max_button_tap: 241 | action_state.prev_activated_at = get_ticks() 242 | await get_tree().create_timer(max_button_tap + max_double_tap_delay).timeout 243 | 244 | if action_state.prev_activated_at: 245 | action_state.prev_activated_at = 0 246 | return InputType.TAP 247 | else: 248 | return InputType.CANCEL 249 | 250 | # If we rule out InputType.TAP and InputType.DOUBLE_TAP, the rest is pretty straightforward. 251 | if delta <= max_button_press: 252 | return InputType.PRESS 253 | 254 | if delta <= max_long_press: 255 | return InputType.LONG_PRESS 256 | 257 | return InputType.HOLD 258 | 259 | 260 | ## Process an event if it matches a given action. If the action is just pressed and it is not 261 | ## already active, emit the `input_detected` signal with `InputType.ACTIVE`, which indicates that 262 | ## the type of action has not yet been determined. If the action is just released and was active, 263 | ## mark it as inactive and emit the `input_detected` signal with the determined input type. 264 | ## 265 | ## @param event InputEvent: The event that triggered the action. 266 | ## @param action StringName: The actions to process. 267 | func _process_action(event: InputEvent, action: StringName) -> void: 268 | if !event.is_action(action): 269 | return 270 | 271 | var action_state: ActionState = _actions[action] 272 | 273 | # If the action just started, set last_activated_at and notify event listeners. 274 | if Input.is_action_just_pressed(action) and !action_state.is_active: 275 | action_state.last_activated_at = get_ticks() 276 | input_detected.emit(event, action, InputType.ACTIVE) 277 | 278 | # If the action just ended, determine the InputType and notify event listeners. 279 | elif Input.is_action_just_released(action) and action_state.is_active: 280 | var delta: float = get_ticks() - action_state.last_activated_at 281 | var input_type: InputType 282 | action_state.last_activated_at = 0 # Reset this before calling _determine_input_type(). 283 | input_type = await _determine_input_type(action_state, delta) 284 | input_detected.emit(event, action, input_type) 285 | --------------------------------------------------------------------------------