└── 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 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
--------------------------------------------------------------------------------