Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-bindable Inputs #19

Open
Phazorknight opened this issue Jan 8, 2024 · 15 comments
Open

Re-bindable Inputs #19

Phazorknight opened this issue Jan 8, 2024 · 15 comments
Assignees
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@Phazorknight
Copy link
Owner

Since Nathan Hoad's input helper includes functions to rebind inputs on runtime, it should be relatively straight foward to create a "Controls" menu in the options that lists all current key bindings and lets the user rebind them as needed.

Please let me know if someone would be up to tackle this, as I'd love to focus more getting scene persistence working.
Thanks in advance!

@Phazorknight Phazorknight added enhancement New feature or request help wanted Extra attention is needed labels Jan 8, 2024
@Jealousmango
Copy link

Hi! I'd love to take a shot at implementing rebinding keys if this issue is still valid.

@Phazorknight
Copy link
Owner Author

Yes, this is still valid. Would be awesome if you could take a shot.
Please make sure you pull from the latest /main/ as I've added another input map action on that one: interact2.

@Jealousmango
Copy link

Wanted to drop a quick update on where I'm at on this issue!

I've setup a placeholder Controls menu option within the existing Options menu and I'm populating that menu with the input map. I've also started hooking up the data for things like the button and key input text. Once that's working remapping the input should be straightforward. I'm hoping to have some free time this week to continue making progress.

image

@Phazorknight
Copy link
Owner Author

Looks promising! Thanks for the update.

@generrosity
Copy link

that looks amazing!

Just incase its useful, DashNothing did a clever remapping algorythm, and they used a dictionary to translate the ui_key elements 👍

I've also just proposed tweaking the Options page to have the title and 'back' buttons always visible 😅 just baby changes from me currently

@JouBqa
Copy link

JouBqa commented Feb 21, 2024

Good job. Can you also add toggle crouch/sprint?

@Phazorknight
Copy link
Owner Author

Phazorknight commented Feb 21, 2024

Good job. Can you also add toggle crouch/sprint?

This is an addition for #55 / Gameplay options. Not for rebindable inputs.

DashNothing did a clever remapping algorythm

I can't confirm how @Jealousmango approaches it, but COGITO comes with Nathan Hoad's InputHelper, which already includes functions for input remapping, so I think it's currently just about implementation.

@Jealousmango
Copy link

Good job. Can you also add toggle crouch/sprint?

This is an addition for #55 / Gameplay options. Not for rebindable inputs.

DashNothing did a clever remapping algorythm

I can't confirm how @Jealousmango approaches it, but COGITO comes with Nathan Hoad's InputHelper, which already includes functions for input remapping, so I think it's currently just about implementation.

Yeah, that's how I've approached the remapping. I used the methods mentioned on the InputHelper documentation to successfully rebind a key so now it's mostly working out the UI flow and cleaning up the content.

I have wondered about the default input map names. Populating the menu with actions like ui_accept feels clunky, but changing that to a more friendly name might be outside the scope of this issue.

@Phazorknight
Copy link
Owner Author

Is there a simple way to exclude the godot default inputs? AFAIK they're never explicitly used in the scripts besides the default UI navigation and it's pretty rare that someone would want to rebind these.

Alterantively there might be away to replace the input map action names with readable ones with a simple dictionary or something, with falling back to the default string name if no entry is found. 🤔

@generrosity
Copy link

generrosity commented Feb 21, 2024

Dictionary is how I've seen it - use the translation, or skip the key altogether. Or a dictionary of godot default inputs to skip I suppose?

Although equally you could "translate" by swapping underlines for space and camelcasing the text (having a static dictionary you have to update seperately isn't great, unless you are dynamically adding keybinds as part of dis/enable of chunks of movement code for devs - ie out of scope?)

@Jealousmango
Copy link

Is there a simple way to exclude the godot default inputs? AFAIK they're never explicitly used in the scripts besides the default UI navigation and it's pretty rare that someone would want to rebind these.

Alterantively there might be away to replace the input map action names with readable ones with a simple dictionary or something, with falling back to the default string name if no entry is found. 🤔

I wasn't able to find a simple way to exclude the default inputs, but I haven't dug too deeply yet. One possible route would be creating a map of blacklisted actions and including all of the Godot default actions.

I think you're right about defining a dictionary for more readable names and falling back on the default string if one isn't provided. I'll be sure to include that. Thanks for the feedback!

@FailSpy
Copy link
Contributor

FailSpy commented Feb 27, 2024

I wasn't able to find a simple way to exclude the default inputs, but I haven't dug too deeply yet.

I've done some investigating in the past on this. And as far as I could find, there isn't a method available from GDScript that can tell you whether an Input mapping is built-in or not. There is, tantalisingly, in the Godot source code a function called 'get_builtins'
https://github.com/godotengine/godot/blob/a586e860e5fc382dec4ad9a0bec72f7c6684f020/core/input/input_map.cpp#L382
I was completely unable to find any reference to this that's accessible via GDScript.
If you're sufficiently motivated, you might be able to use GDNative to hook into that directly.

Personally wasn't willing to go that far, so I came up with this hack-y Resource instead (edited slightly to make more sense in context):

@tool
extends Resource
class_name RebindableActions

@export var inputs : Array[String]
static var custom_actions : Array[String]

func _init():
	inputs = get_actions().duplicate()
	ProjectSettings.property_list_changed.connect(_refresh)

static func _refresh():
	custom_actions = []
	var was_input = false
	var loaded_defaults = false
	for setting in ProjectSettings.get_property_list():
		var is_input = setting.name.begins_with('input/')
		if was_input and not is_input:
			loaded_defaults = true
		elif is_input and loaded_defaults:
			custom_actions.push_back(setting.name.split('/')[-1])
		was_input = is_input

func get_actions():
	if custom_actions:
		return custom_actions
	_refresh()
	return custom_actions

func _property_can_revert(property):
	if property == 'inputs':
		return get_actions() != inputs

func _property_get_revert(property):
	if property == 'inputs':
		return get_actions().duplicate()

This works because ProjectSettings will provide you the property list in the order it was loaded.
So it loads the built-in project settings first, and then loads any user configured project settings. Even if you add/remove inputs from the built-in actions, all built-in actions will stay bunched together. And of course, you can reverse _refresh()s behaviour to get all builtin actions.

This will work on export as well.

If you would rather just a full list to be updated of the built-in actions manually, you can find them earlier on in that same Godot source code:
https://github.com/godotengine/godot/blob/a586e860e5fc382dec4ad9a0bec72f7c6684f020/core/input/input_map.cpp#L292

RebindableActions can then serve as your "whitelist" of inputs. There's nothing stopping a user from adding built-in inputs if that's desireable, or removing custom ones.

You will need to add some behaviour to _refresh() that can detect and add new Inputs on project settings changes to auto-import those, at the very least for any RebindableActions that return false on "can_revert"

@Jealousmango
Copy link

Jealousmango commented Feb 28, 2024

I wasn't able to find a simple way to exclude the default inputs, but I haven't dug too deeply yet.

I've done some investigating in the past on this. And as far as I could find, there isn't a method available from GDScript that can tell you whether an Input mapping is built-in or not. There is, tantalisingly, in the Godot source code a function called 'get_builtins' https://github.com/godotengine/godot/blob/a586e860e5fc382dec4ad9a0bec72f7c6684f020/core/input/input_map.cpp#L382 I was completely unable to find any reference to this that's accessible via GDScript. If you're sufficiently motivated, you might be able to use GDNative to hook into that directly.

Personally wasn't willing to go that far, so I came up with this hack-y Resource instead (edited slightly to make more sense in context):

@tool
extends Resource
class_name RebindableActions

@export var inputs : Array[String]
static var custom_actions : Array[String]

func _init():
	inputs = get_actions().duplicate()
	ProjectSettings.property_list_changed.connect(_refresh)

static func _refresh():
	custom_actions = []
	var was_input = false
	var loaded_defaults = false
	for setting in ProjectSettings.get_property_list():
		var is_input = setting.name.begins_with('input/')
		if was_input and not is_input:
			loaded_defaults = true
		elif is_input and loaded_defaults:
			custom_actions.push_back(setting.name.split('/')[-1])
		was_input = is_input

func get_actions():
	if custom_actions:
		return custom_actions
	_refresh()
	return custom_actions

func _property_can_revert(property):
	if property == 'inputs':
		return get_actions() != inputs

func _property_get_revert(property):
	if property == 'inputs':
		return get_actions().duplicate()

This works because ProjectSettings will provide you the property list in the order it was loaded. So it loads the built-in project settings first, and then loads any user configured project settings. Even if you add/remove inputs from the built-in actions, all built-in actions will stay bunched together. And of course, you can reverse _refresh()s behaviour to get all builtin actions.

This will work on export as well.

If you would rather just a full list to be updated of the built-in actions manually, you can find them earlier on in that same Godot source code: https://github.com/godotengine/godot/blob/a586e860e5fc382dec4ad9a0bec72f7c6684f020/core/input/input_map.cpp#L292

RebindableActions can then serve as your "whitelist" of inputs. There's nothing stopping a user from adding built-in inputs if that's desireable, or removing custom ones.

You will need to add some behaviour to _refresh() that can detect and add new Inputs on project settings changes to auto-import those, at the very least for any RebindableActions that return false on "can_revert"

When I worked on this I just knew there had to be some way to get those actions from the Godot source code, but being fairly new to Godot myself I thought it would faster to just type them all out manually 😅. This is super helpful info and I'm happy that my hunch was correct! Next time I'll definitely go hunting in the source code for this type of thing.

Currently I'm filtering all of the built-ins with a defined map. I'll probably put a pin in the filtering part for now but I could revisit this before opening the PR if your auto-loading method feels like a better way to handle loading the built-ins.

Thanks for all the help!

@Jealousmango
Copy link

Another quick update about my progress!

I've filtered out all of the built-in action names and have also implemented the discussed "friendly" names mapping for action names. If no friendly name is added to the map the label text will fall back to the default name.

I also have a simple rebind menu that pops up when the rebind button is clicked with data for the relevant action piped into the rebind menu. I'm listening for input on that menu and rebinding the associated action to the new input. Currently working through the saving dialog and logic.

Once that's done I'll clean up the button labels based on the detected device. For Joypads I'd really love to dynamically change the button label to an icon for the correct device, but that would require importing icons for each type of Joypad I think? I'd welcome any feedback on this!

When I prepare this PR, should I open that against the new tabbed menu design from #55? For testing my current setup is just a Controls button on the main menu but I can migrate what I have into the tabs redesign if that makes sense.

image

@Phazorknight
Copy link
Owner Author

Should I open that against the new tabbed menu design from #55?

Yes, ideally. I've prepped a tab in it called "Bindings"

For Joypads I'd really love to dynamically change the button label to an icon for the correct device

There's already a script for this called DynamicInputIcon.gd, which depending on which device is detected, switches the icon accordingly. I'm sure you can easily adapt it to your needs, but be careful when editing it directly, as it's currently used for all UI prompts.
It only differentiates between generic gamepad and keyboard/mouse, but it can be extended to do gamepad-type specific icons.
I was planning to add this later using Kenney's new Input UI icons, once the rebinding is in place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants