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

Improve setting inputs for local multiplayer games #3555

Open
maksutheam opened this issue Nov 16, 2021 · 14 comments
Open

Improve setting inputs for local multiplayer games #3555

maksutheam opened this issue Nov 16, 2021 · 14 comments

Comments

@maksutheam
Copy link

Describe the project you are working on

1v1 fighting game, similar to Street Fighter or Mortal Kombat (This one specifically)

Describe the problem or limitation you are having in your project

(First time making an issue here, feel free to ask if something isn't clear)

As I was making my game, I noticed that setting up inputs for multiplayer was tedious and had many points of possible failure.

I started out by making the controls work for one player, which was quite simple. Just make some input actions, for example "move_left", "jump" and "punch", set fitting keyboard- and controller inputs for them, make a new scene named "Player" with KinematicBody as root and attach a script that looked something like this:

extends KinematicBody

func _process():
        if Input.is_action_pressed("jump"):
                jump()
        if Input.is_action_pressed("punch"):
                punch()
        if Input.is_action_pressed("move_left"):
                move(left)

But wait, I need to make this work for 2 players! For that to work, I have to make whole new input actions, like "move_left2", "jump2" and "punch2" with their own keyboard- and controller inputs. I didn't want to repeat myself by making a separate scene for player 2, as both players should have the same abilities, so I edited the existing Player-scenes script to something like this:

extends KinematicBody

export var player = 1 #sets which player controls this scene
var move_left
var jump
var punch
#etc.

func _ready():
        if player == 1:
                move_left = "move_left"
                jump = "jump"
                punch = "punch"
        elif player == 2:
                move_left = "move_left2"
                jump = "jump2"
                punch = "punch2"

func _process():
        if Input.is_action_pressed(jump):
                jump()
        if Input.is_action_pressed(punch):
                punch()
        if Input.is_action_pressed(move_left):
                move(left)

With this code, I can have another player by instancing the scene and setting player to 2.

Now, let's say that I wanted to add a new action, "move_right" for example. I would have to add it once per player on the input map, make a new variable, write it once per player again in the _ready() function and write
if Input.is_action_pressed(move_right)
, where I actually needed it. If I misspell the name anywhere during this process, which I sometimes did, it will not work. The editor throws an error when this happens, but I wish I could avoid this issue entirely.

Adding new players isn't any easy, either. If I wanted a third player, I would have to make new actions like "move_left3", "jump3" etc. and set new inputs for each one, again, and add them all in the _ready() function, again.

Also, I hate that I have to add in new controller inputs for each player, despite the button on the controller being the same. It's not like I want player 1's "punch" be on the A button and player 2's on R1 or something.

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

If I could just edit the actions once in the input map and then once in the code where I need it, just like when I made it work for one player, it would make my life a lot easier.

My solution would be "Input Groups". They are like physics layers, but for actions' inputs. For example, for the "jump" action, one could set the A button on controller 1 to be in Input Group 1 and the A button on controller 2 to be in Input Group 2. Now, when the A button on controller 2 is pressed, it would say something like "The "jump" action on group 2 was triggered".

See also issue #2273 for something similar with "Action Sets".

Additionally, I would appreciate if I could duplicate inputs. That would make setting controller inputs easier, as then I would only have to change what controller the input is coming from.

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

I'll add drawings to describe how I would change input map later. But imagine that next to an input, there would be buttons similar to the physics layer ones, and with that you could choose which Input Group the input is a part of. By default, a new input is part of the first group.

Input.is_action_pressed(action) and similar functions would become something like Input.is_action_pressed(action, group) where the group is the Input Group as an int. If left unchanged, it will default to the first group (so if one is making a single-player game, they wouldn't have to worry about groups).

With my solution, all I would have to do to add multiplayer is to change the initial Player-scene code to this:

extends KinematicBody


export var player = 1 #sets which player controls this scene

func _process():
        if Input.is_action_pressed("jump", player):
                jump()
        if Input.is_action_pressed("punch", player):
                punch()
        if Input.is_action_pressed("move_left", player):
                move(left)

And just like previously, I can instance the scene this code is attached to and change the player variable to set which player controls the instance, except it's much simpler!

What If I want to add another action, like "move_right"? I would add only one "move_right" action in the input map, add in the inputs for player 1 and 2, put them into appropriate input groups and then add something like this to the _process() function:

        if Input.is_action_pressed("move_right", player):
                move(right)

What if I want to add another player? I'll just add inputs to existing actions and put them into a new group (group 3 for example), and then instance the scene and set player to 3.

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

I worked around this the way I wrote on the "Describe your problem" section, but as I mentioned, it's tedious, I end up repeating myself a lot, and it will be easy to make errors.

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

I'm not an expert, but I don't think that add-ons can add the "Input Groups" feature, and certainly not duplicating inputs.

@Xrayez
Copy link
Contributor

Xrayez commented Nov 16, 2021

I agree that something like this should be implemented in core, or we simply need a better interface. If you'd like to see an alternative approach of solving this problem, you may want to look at my own two-year-old proposal #104. My main use case is being able to split inputs down to multiple characters, but #104 could be extended to deal with several player input as well.

For your particular proposal, the workaround is described by reduz, see #639 (comment), unfortunately the proposed solution is seen to be too complex to be implemented in core.

@AaronRecord
Copy link

AaronRecord commented Nov 16, 2021

As a workaround you can do something like:

func get_input_action(original: String) -> String:
    return original + str(player)


func _process():
        if Input.is_action_pressed(get_input_action("jump")):
                jump()
        if Input.is_action_pressed(get_input_action("punch")):
                punch()
        if Input.is_action_pressed(get_input_action("move_left")):
                move(left)

You'll still need to add the input actions manually or with InputMap though.

@KoBeWi
Copy link
Member

KoBeWi commented Nov 17, 2021

Or even simpler:

func is_action_pressed(action):
    return Input.is_action_pressed(str(player, action))

func _process():
        if is_action_pressed("jump"):
                jump()
        if is_action_pressed("punch"):
                punch()
        if is_action_pressed("move_left"):
                move(left)

The worst part of setting up local multiplayer is defining all the new actions. Last time I was doing it I didn't even bother with the InputMap editor, I just manually copied player 1 in project.godot multiple times. Handling input is fine already, as you can just use helper methods like the one I provided above.

@maksutheam
Copy link
Author

For your particular proposal, the workaround is described by reduz, see #639 (comment), unfortunately the proposed solution is seen to be too complex to be implemented in core.

Well thats a shame, sure wish I would have seen that before writing the whole wall of text.

You'll still need to add the input actions manually or with InputMap though.

The worst part of setting up local multiplayer is defining all the new actions. Last time I was doing it I didn't even bother with the InputMap editor, I just manually copied player 1 in project.godot multiple times.

I admit, these are some fine workarounds to my problem, but even then, you both still had to add actions manually. So, how about just adding the ability to duplicate actions/inputs?

You know, select on one or multiple actions/inputs, then press right click and select "duplicate" or press Ctrl + D like in the editor, and it makes a duplicate of the action/inputs and renames it (if you were duplicating jump it would rename the copy into jump2). I would still have the issue of having a lot of actions to take care of, but at least setting them up would be a lot easier, and it shouldn't be as complex to implement as my previous solution (probably, I'm not an expert).

@Calinou
Copy link
Member

Calinou commented Nov 19, 2021

Related to #1249.

See godotengine/godot#29989 where something similar was implemented. However, it wasn't merged as no consensus was reached on the feature itself.

@bend-n
Copy link

bend-n commented Nov 29, 2021

what about something like

func _process():
        if is_action_pressed('jump_%s' % player):
                jump()
        if is_action_pressed('punch_%s' % player):
                punch()
        if is_action_pressed(('left_%s' % player)):
                move(left)

@myin142
Copy link

myin142 commented Jul 11, 2022

Just throwing my opinion in here. I have created a PlayerInput class to all my projects that handles player input, even if it's just one player. I created a separate class for it because I didn't like to use singletons. First because there is just one and second they are not easily mockable in a test.

The PlayerInput class basically gets all the events from _unhandled_input and registers only the correct ones for the player. The player is identified using the device_id and if it's a joypad or not. It works pretty good for controlling multiple characters, but I'm struggling to get it working for the GUI which is why I found this issue. But now that I think about it, I could just create a global scene that contains all players inputs and access them like the normal Input singleton. But this doesn't solve the problem with the focus, however after this godotengine/godot#62421 it might be possible.

Here is the latest source if anyone is interested: https://github.com/kuma-gee/explosive-delivery/blob/master/src/input/PlayerInput.gd

@Braveo
Copy link

Braveo commented Aug 10, 2022

I'm all in for an improvement input maps, but I'm wondering if in the implementation, we could make Input Maps more modular rather than having them as layers. We can create different presets and do something like in Super Smash Bros where a player can name their input map and just save it, and make as many as they'd like. This can then be assigned to each controller to their like.

This is how I'd implement it:

image

  • Presets can be created on runtime and saved for new players to enter in their own custom inputs.
  • Presets can be distributed across multiple devices who prefer one specific type of preset.
  • Legacy input could still be easy to use for beginners as the default preset will handle pretty much all input as usual.

Compatibility Breaking Changes, as well as Considerations:

  • A compatibility-breaking change would be the use of splitting inputs across devices in one map, but maybe you could mark devices as agnostic (?), or allow multiple devices to be associated with one player.
  • UI and player input might be preferably split into two different base action maps. Maybe separating the two in the input system similar to how it's being done in Godot 4, but rather as two different base files would work well?
  • Speaking of multiple devices with one player, if this was possible, we could bring accessibility to this system, allowing players to plug in foot petals. This sounds harder to implement, but I highly advocate for accessibility being put into more consideration. This is iffy, but something like this maybe (?):
    image

Final Thoughts

I think modular input maps would be great. The input layers mentioned above would work really well too since I highly doubt there'd be more than like 16 players locally. The whole multiple devices for one player might be harder to implement in the modular system but much easier in the layer system. If we can't find a one-size-fits-all situation then we can make the input map system open enough for developers to add to their needs, and make inputs much easier to manage (or discuss that in further depth within the other proposals mentioned above).

@Shadowblitz16
Copy link

Shadowblitz16 commented Nov 11, 2022

I would like to make a smash bros like ui in which each player can have a mouse cursor controlled by the gamepad/keyboard/mouse/touch which can react to the ui

@Calinou
Copy link
Member

Calinou commented Nov 11, 2022

I would like to make a smash bros like ui in which each player can have a mouse cursor controlled by the gamepad/keyboard/mouse/touch which can react to the ui

This is being tracked in #4295.

@taitep
Copy link

taitep commented May 7, 2023

Maybe add the possibility to change player filter from default when running Input.is_action_pressed and similar as well as getting the player of an event, or maybe dividing other things then controls into devices in a different way.

@stephanbogner
Copy link

stephanbogner commented Jun 22, 2023

An enhancement to this would be awesome.

@Braveo's suggestion seems awesome (but I am no expert).
The only thing I'd change is to reverse the hierarchy for presets: Not Y → Run, but Run → Y, because then you can assign multiple Run → Y, Shift like how it's currently also possible in Godot.

To add even more complexity (:sweat_smile: ) some thoughts that I had during my game:

  • Singleplayer vs. local coop: When playing in single player you might want to allow WASD and arrow keys as input, but when using coop WASD belongs to player 1 and arrow keys to player 2. How to express this nicely?
  • How to save and load presets? An easy way to save and load presets would be awesome (e.g. preset.to_json() and preset.parse(json_string))
  • What about settings? Should input settings like "invert right stick" or "look sensitivity" be stored in the preset or not? (I guess no, but not 100% sure)

Simpler implementation than presets?

  1. My initial thought to hack a solution together was to spawn multiple input maps – one per player (pseudo-code var input_map_1 = InputMap.new()).
  2. I would then copy the configuration from the InputMap setup in Godot settings. And then adjust whatever the user wants to be changed.
  3. Some input handler would then pipe the input event through the correct InputMap (pseudo-code InputEvent.is_action(input_map_1, "jump")) ... but InputEvent doesn't allow passing an InputMap along.
  4. Since that doesn't work I am thinking (as a workaround) to create my own class PlayerInputMap that is a blend between InputEvent and InputMap:
    • (1) Takes an InputEvent and allows to check for actions: Pseudo-code input_map_player_1.is_action_pressed(input_event, "jump")
    • (2) Allows rebinding: Pseudo-code input_map_player_1.add_event_to("jump", input_event)
    • (3) Note: The PlayerInputMap would need to know which device is assigned to which player ... but I am working on that too 😅

@Shadowblitz16
Copy link

If this is done ui controls need to take use of this system.
Currently filtering player controls for ui is difficult

@zequintino
Copy link

zequintino commented Apr 24, 2024

what about this solution?
1:33
https://www.youtube.com/watch?v=tkBgYD0R8R4&ab_channel=GDQuest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests