godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.07k stars 69 forks source link

Separate project's input map into a resource #9809

Open passivestar opened 1 month ago

passivestar commented 1 month ago

Describe the project you are working on

A reusable first person character controller

Describe the problem or limitation you are having in your project

Input map is currently a part of the project's .godot file

You can't copy a character controller into a newly created project, drop a character prefab into the scene and start playing. Even a simple character controller might require a lot of different actions (jumping, sprinting, crouching, interacting, etc) and there's no straightforward recognized way to add those bindings into a project from your character controller asset

Describe the feature / enhancement and how it helps to overcome the problem or limitation

In Unity input bindings are resources (scriptable objects). This feature is essential when distributing character controllers on the asset store because it allows an asset developer to provide default bindings with the asset. This allows users to quickly test a controller without any manual setup. Users can also use the default bindings file as a template for their own bindings - they can easily be copied and modified (this is a bit different from a config file https://github.com/godotengine/godot-proposals/issues/423 because if bindings are a resource they can be easily edited in inspector)

Without it users would have to refer to the asset's manual to find out the names of actions that need to be bound, which is a lot of unnecessary friction

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Implementation details are up for discussion, I suppose project would need to support referencing a bindings resource through a path or a uid. It could also be a list of resources instead of a single resource to support a more modular and non-destructive approach to bindings (in that case bindings from the latter resources would have higher priority in case of action name conflicts). Project settings Input Map tab UI would need to be changed to support showing the list of bindings resources and editing them from there for convenience

It's also worth mentioning that in Unity's Input System those bindings aren't global at all, they are recognized from any PlayerInput component. I think this idea is worth exploring as well, because this approach has been working pretty well for Godot (i.e LightmapGI is a node unlike in other engines). It's more flexible, it's easier to reason about and it allows for a more predictable scripting API. Of course if we do this we'll need to consider UX and make sure everything still works out of the box as you'd expect. Managing complexity can be a challenge, but I believe it's possible to create an input system that is both easy to use and doesn't compromise on flexibility/extensibility if you need it

If this enhancement will not be used often, can it be worked around with a few lines of script?

To work around this limitation I made a couple of resources and added some logic to the player controller script to load actions from them:

# input_action_list.gd

extends Resource
class_name InputActionList

@export var list : Array[InputAction] = []
# input_action.gd

extends Resource
class_name InputAction

@export var name: String
@export var deadzone: float = 0.5
@export var events: Array[InputEvent]
# player_controller.gd

@export var default_actions : InputActionList

@export var add_default_actions: bool = false:
    set(val):
        for action in default_actions.list:
            if not ProjectSettings.has_setting("input/" + action.name):
                var input = {
                    "deadzone": action.deadzone,
                    "events": action.events
                }
                ProjectSettings.set_setting("input/" + action.name, input)
        ProjectSettings.save()
        EditorInterface.restart_editor(true)

This exposes a "button" on the character controller, that, when clicked, adds predefined input actions from a custom resource into the project file

image

The problems are:

All of this can be avoided if Godot ships with a less rigid input binding system

Is there a reason why this should be core and not an add-on in the asset library?

The point of this is to reduce complexity when setting up a controller in a new project. Having to download an asset for an input system would defeat the purpose (Unity's input system is a separate package that needs to be downloaded and people aren't super happy about that)

It's also worth mentioning that Godot doesn't have any support for dependencies. Which means that when a user downloads your character controller, you'd need to show a message asking to manually download an input system dependency just to be able to see a character running around and jumping on the screen, which doesn't sound great to say the least. Ideally your asset should ship with all of the code required to use it, at least when it's something as simple as a character controller

Calinou commented 1 month ago

You can't copy a character controller into a newly created project, drop a character prefab into the scene and start playing. Even a simple character controller might require a lot of different actions (jumping, sprinting, crouching, interacting, etc) and there's no straightforward recognized way to add those bindings into a project from your character controller asset

If it's an EditorPlugin, it can create input actions in its _enter_tree() method (just like it can create/modify project settings and autoloads). They can be removed in _exit_tree() if they are equal to the default value too.

However, there's a bug which makes input actions added by @tool scripts not appear in the Project Settings: https://github.com/godotengine/godot/issues/44776

In short, it's possible for editor plugins to perform automated setup so that they work out of the box. A lot of editor plugins currently don't do this, but they could once the above bug is fixed :slightly_smiling_face:

passivestar commented 1 month ago

@Calinou <tangent> Editor plugins are awkward. They need plugin.cfg. They need to be enabled. They need to be in a specific folder. I just want to copy files. I may have 100 different reusable components stored locally that I want to be able to drag and drop into my project. Some of them may not even have any scripts in them (i.e a scene with only a pre-configured Environment node that I want to reuse in different projects). If some of them are plugins I need to consciously remember that and drag them into the addons folder instead. I want a player controller and an environment to be in the same UX realm and have the same workflow. Editor plugins create unnecessary complexity and UX friction out of thin air. More often than not there isn't any value in "enabling" things that you copy into your project (for a character controller any setup is avoidable if godot was to adopt unity's approach where there's no global input bus, only individual input components referencing input map resources)

IMO plugins need to serve the purpose of allowing you to package an asset into a nice box with a description on it (in plugin.cfg) for the ease of distribution (and potentially a list of dependencies if godot gets support for them in the future). But the plugin system shouldn't affect how your asset itself works </tangent> 😄

Calinou commented 1 month ago

More often than not there isn't any value in "enabling" things that you copy into your project (for a character controller any setup is avoidable if godot was to adopt unity's approach where there's no global input bus, only individual input components referencing input map resources)

It sounds like there should be a way for newly installed editor plugins to be automatically enabled then. :slightly_smiling_face:

This can be done for addons installed from the asset library, but also for new addons detected on the project filesystem after the editor window gains focus (so it works with manually copied folders).

It works like this in most other programs where plugins are enabled as soon as you install them (web browsers, code editors/IDEs, etc).

I think I had a PR for this at some point (for the asset library at least), but I can't find it right now.

passivestar commented 1 month ago

It sounds like there should be a way for newly installed editor plugins to be automatically enabled then. 🙂

Absolutely! And also make plugin.cfg optional and make it work from any directory and we will have reinvented unity's approach (where plugins are detected by their class alone) :D

My point initially was that plugin system shouldn't be necessary for something like a character controller at all. It doesn't even make sense semantically. EditorPlugin class API is all about extending editor's functionality (adding docks, custom import handlers, custom debugger stuff, inspectors, etc). A character controller is not a plugin. It's an asset 🙂

theraot commented 1 month ago

I also ran into this. My workaround involves reading the actions from a folder.

var folder_path := (get_script() as Script).resource_path.get_base_dir()

func _load_input_actions() -> bool:
    var result := false
    var input_folder_path := folder_path.path_join("input")
    for folder in DirAccess.get_directories_at(input_folder_path):
        var action_path := input_folder_path.path_join(folder)
        var events:Array[InputEvent] = []
        for file in DirAccess.get_files_at(action_path):
            var input_event := load(action_path.path_join(file)) as InputEvent
            if input_event != null:
                var duplicated:InputEvent = input_event.duplicate()
                if duplicated != null:
                    events.append(duplicated)

        var dict := Dictionary( {"deadzone" : 0.5 , "events":  events } )
        result = _write_project_setting("input/" + folder, dict) or result

    return result

func _write_project_setting(setting_name:String, value:Variant) -> bool:
    var already_exists := ProjectSettings.has_setting(setting_name)
    if already_exists:
        var found:Variant = ProjectSettings.get_setting(setting_name)
        if typeof(found) == TYPE_DICTIONARY and typeof(value) == TYPE_DICTIONARY:
            var value_dictionary:Dictionary = value
            var found_dictionary:Dictionary = found
            for key:Variant in value_dictionary.keys():
                if found_dictionary.has(key):
                    continue

                found_dictionary[key] = value_dictionary[key]

            value = found_dictionary

        if str(found) == str(value):
            return false

    ProjectSettings.set_setting(setting_name, value)
    return true

I use the return value to know if need to call ProjectSettings.save() and EditorInterface.restart_editor(true).


Addendum: By the way, in Unreal "Enhanced Input" system, input actions are also files.