Customized controls per player for local multiplayer / coop

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By st_phan

Context:

  • I am making a local coop/multiplayer game
  • I want to allow key rebinding due to accessibility (and player preference)
  • → It can be the case that e.g. two gamepads are connected, but with different control schemes

Problem:
Godot’s input map doesn’t allow to specify anything per player.

Question:
Is there a best practice or recommendation to approach this?
Thanks in advance <3

:bust_in_silhouette: Reply From: zhyrin

This video has a solution: https://www.youtube.com/watch?v=tkBgYD0R8R4

Thank you for the answer.

If I haven‘t missed anything, the solution is based on creating input map entries for each player (e.g. run_player_1 and run_player_2).

While that works, it‘s uglier and less scalable than a solution I‘d hope for.

st_phan | 2023-06-22 19:50

How do you expect a solution to be prettier and more scalable?
I think this is as good as it gets. There is a finite number of players that can be handled on a local machine anyway.

zhyrin | 2023-06-23 10:32

In the issue I linked to above there is quite an extensive discussion on how the input system could be majorly enhanced (if Godot’s core would be expanded) – check it out, it’s interesting, especially this comment.

I posted my question to see if I had missed an obvious approach that is fairly flexible (like the proposed stuff in the issue).

st_phan | 2023-06-23 10:52

:bust_in_silhouette: Reply From: godot_dev_

I have solved this before and will help by providing a coding example immediatly below and will then explain the logic aftewards

const BTN_A = 1 #(BIT SHIFT 0) 0001 in binary, 1 decimal
const BTN_B = 1 << 1 # 0010 in binary, 2 deciaml 
const BTN_X = 1 << 2 # 0100 in binary, 4 decimal

const BTN_A_KEY = "A"
const BTN_B_KEY = "B"
const BTN_X_KEY = "X"

var btnMap = {}
var pInRemap  = {}

func _ready():
	#initialize button map
	btnMap[BTN_A_KEY] = BTN_A
	btnMap[BTN_B_KEY] = BTN_B
	btnMap[BTN_X_KEY] = BTN_X
	
	#no remapping by default
	for k in btnMap.keys():
		pInRemap[k]=btnMap[k]
		
		
#targetBtn is a string representing button (will be used as key to access the button maps)
#newBtns is an array of strings, representing the buttons to map to when pressing target button
func remapButton(targetBtn, newBtns):
	
	if (targetBtn == null):
		return
		
	var btnCode =null
	
	#only compute the button bit maps for none empty remapping
	if newBtns != null and newBtns.size() > 0:
		btnCode=0
		#go set all bits of btnCode, for each new button
		for b in newBtns:
			btnCode = btnCode | btnMap[b]
		
	#remap
	pInRemap[targetBtn] = btnCode
	
	
#example of remapping the A button to be a macro of 'B+X'
remapButton(BTN_A_KEY,[BTN_B,BTN_X])
	
func processInput():

	#suppose the way you mapped your buttons is a 2 player game as follows:
	#"P1_A": the "A" button for player 1
	#"P2_A": the "A" button for player 2
	#"P1_X": the "X" button for player 1
	#"P2_X": the "X" button for player 2
	#etc.
	var inputDeviceIds = ["P1","P2"]
	
	for inputDeviceId in inputDeviceIdS:
		var inputJustPressedBitMap = 0
		var btnKeys = pInRemap.keys()
		#iterate all buttons to create bit map
		for k in btnKeys:
			
			#remapping of buttons made it so a button maps to no input?
			if pInRemap[k] == null:
				continue
			#tranlate button id to device button (2 different devices have same button)
			#differentiate devices via inputDevice id. Needd to map this in input seetings
			var inputId = inputDeviceId+"_"+k
			#check if player just pressed it
			if Input.is_action_just_pressed(inputId):
				
				inputJustPressedBitMap = inputJustPressedBitMap | pInRemap[k]
				
		#take bit-wise-and operator to determine whether button was pressed
		if (inputJustPressedBitMap & BTN_A) ==BTN_A:
			#player <inputDeviceId> pressed a button that maps to button A
			pass
		if (inputJustPressedBitMap & BTN_B) ==BTN_B:
			#player <inputDeviceId> pressed a button that maps to button B
			pass
		if (inputJustPressedBitMap & BTN_X) ==BTN_X:
			#player <inputDeviceId> pressed a button that maps to button X
			pass

The idea is that a dictionary maps action input names to button bitmap codes (each button has its own unique bit set to 1). This way, you can represent a series of button presses with a single integer variable. For example, in the above code

001 = would be treated as button A was pressed
101 = would be treated as button A and X were pressed together

This then allows to you create a dictionary that remaps a raw button press to null input, another input, or a macro of inputs

Thanks for sharing, this is quite a unique an interesting approach!

st_phan | 2023-06-23 10:53

Oops, just realize there is a small bug in the code I provided. Each player should have their own script with a inputDeviceId, so that each player would have a button remaps dictionaries (so the for loop that iterates the input device names would be unecessary in the processInput fucntion)

godot_dev_ | 2023-06-23 15:36