Split screen player devices !?

Hello,

this is my first post in the forums. I’m learning Godot for a couple of months now. I have a question about local multiplayer with 2 players and devices they use.

If i want to make so a player is able to switch using the keyboard and a gamepad in single play game modes, i have to add two events in each input action for the player. So the event for the gamepad will use Device0.

But what if he wants to play with a friend in the split screen mode, should they connect a second gamepad with another set of actions, set up to use Device1 ?

Isn’t there a way the player 1 uses the keyboard and the player 2 the gamepad for split screen but being able to use any of the keyboard and the gamepad in the single play modes ?

As i understand this is impossible while if they only got 1 gamepad, it will be always bound to player 1 input actions !

4 Likes

I have the same problem and have been searching for a solution. So far my solution is a rather cumbersome workaround. So if others here have a better option please tell.

Input are handled by two singletons “Input” and “InputEvent”.
The former has more methods that are relevant to a game where you will often need different behavior depending if you press/hold/release keys/buttons.
The latter is a more general class, and will have less methods aimed at serving inputs you actually need in a game.

The problem is now that Input does not come with a property of what device pressed it. Only “InputEvent” has the property .device.

The workaround is therefore to manually add all devices separately in your input mapping. like “controller1_jump = joypad_0, device 0”, “controller2_jump = joypad_0, device 1”, “keyboard_jump = spacebar”, etc.etc.

As I said, the workaround is quite annoying, but it is the best method I found so far.
My guess is that there must be a way to connect Input and InputEvent and carry over the property. But I am a noob too, so I have not been able to figure it out.

3 Likes

Hi guy’s. Same problem. It so ridiculous that device property cant be connected with Input in some easy way. No answers found.

1 Like

Hello everyone!

After leveling up I was finally able to find a solution to the problem regarding split-screen or local multiplayer. The solution came from this post: How to make a local multiplayer game
But the approach was completely reworked, so first you need to read this reference post. The solution is made through the AutoLoad script. But it does not change the essence in any way

The idea is to add only the required set of actions to the InputMap. After that, when connecting players or new joypads, ALL actions from the initial InputMap are copied and new actions with new events are dynamically created!
But of course there are very tough “pitfalls”:

  1. Inside the engine, all input devices are assigned to a specific number, which always starts with 0. That is, the keyboard is always device 0.
    A joypad connected to a PC is device 0. The second joypad is device 1, and so on. So keyboard and joypad #1 is device 0.
    The Input.get_connected_joypads() function shows connected joypads. But you CANNOT CHANGE THE DEVICE NUMBER inside this array, even if you manually shift the indexses inside this array.
  2. It is still unclear whether each new action should have the correct input device set or not. But in many posts on GitHub, those who created new actions manually in InputMap set a new device index for each new set.
  3. Simply copying the inputmap will only end in pain, because events inside actions are OBJECTS. And when an entire array or dictionary is copied, it copies objects (events), but does not break the connection between them. Accordingly, as soon as one of the objects changes (something is erased from them or buttons are changed), this happens both in the original object (InputMap) and in its copy.
  4. Events themselves represent an Object (depending on the assigned buttons inside the action). Problem that we can get access only via Array[InputEvent]. Each of these is a certain type of InputEvent. At the same time, if we look at the project.gd file, the saved new actions are more reminiscent of Dictionaries. Accordingly, it is not so easy to just take and get access to information inside the event, because each component has a different data type: for example, InputEventKey and InputEventJoyButton.
extends Node

# --- Input -------------------------------------------------------------------
@onready var start_actions: Array = InputMap.get_actions()
var start_actions_events: Dictionary = {}
var players_in_game: Dictionary = {}
var prefix_action: String = "PL"

@onready var start_actions: Array = InputMap.get_actions() - this is a list of actions that should be saved as the initial one, which cannot be changed.
var start_actions_events: Dictionary = {} - This is a variable for saving the initial data from the InputMap.
var players_in_game: Dictionary = {} - This is a dictionary with the assignment {Device : Player_ID}, so that it can be assigned to players later.
var prefix_action: String = "PL" - this is a prefix for new actions, the construction will be “action+PL+device” for example “ui _acceptPL0”.


func _ready() -> void:

	init_inputmap()
	Input.connect("joy_connection_changed", _on_joy_connection_changed)

# INPUT MAP ---------------------------------------------------------------------------------------

func init_inputmap():
	for action in start_actions:
		var events: Array = InputMap.action_get_events(action)
		var start_events: Array = events.map(func(i): return i.duplicate(true))
		start_actions_events[action] = start_events

init_inputmap() - The very first function that makes a deep copy of events. The starting actions are iterated over and each action receives not only a copy of the ARRAY events inside, but also a copy of ALL OBJECTS of each “event” (Array[InputEvent]), inside each action. Moreover, a copy of objects using the map() function occurs with the creation of an INDEPENDENT copy. Thus, any change to these events in the future will not affect (since it has no connection) the original events inside the InputMap.


func _on_joy_connection_changed(device: int, connected: bool):
	var players = Input.get_connected_joypads()
	if connected:
		players_in_game[device] = (prefix_action + str(device))  #{0:PL0}
		add_player_inputmap(device)
	else:
		players_in_game.erase(device) #{0:PL0}

func _on_joy_connection_changed(device: int, connected: bool) - This is a function that is associated with the Input.connect("joy_connection_changed", _on_joy_connection_changed) signal. When a new joypad or player connection occurs, this function saves the device number inside the dictionary - as a key and Player_ID as a value.
Then DYNAMIC creation of new actions in InputMap occurs.
When the joypad is disconnected, the key is simply erased, i.e. the device number, but not the actions inside InputMap.


func add_player_inputmap(device: int):
	for action in start_actions:
		var new_action: StringName = (action + prefix_action + str(device))
		if not InputMap.has_action(new_action):
			InputMap.add_action(new_action)
			
			# INFO: Duplicate InputEvents inside Array[InputEvent]
			var new_events: Array = start_actions_events[action].map(func(i): return i.duplicate(true))
			var new_actions_events: Dictionary = {}
			new_actions_events[new_action] = new_events
			
			# INFO: SET new_events to new_actions via dictionary keys
			for index in range(new_actions_events[new_action].size()):
				if new_actions_events[new_action][index] is not InputEventKey:
					new_actions_events[new_action][index].set_device(device)
					InputMap.action_add_event(new_action, new_actions_events[new_action][index])

add_player_inputmap(device: int) - the function starts iterating all the actions inside the starting set of InputMap, which was saved at the very beginning of the code.

  1. For each starting action (ui _accept), a new action with a prefix and player_id (ui _acceptPL0) is created.
  2. A check is performed to see if an action was created when the previous joypad or device was connected. If not, an action, an empty object, without events is added to the InputMap.
  3. For each NEW action, an INDEPENDENT copy of “events” (Array[InputEvent]) is created from start_actions_events[action]. This must be done from a separate variable (dictionary), since each new action with a list of events will fill the InputMap and can copy already copied actions. Then you connect 2 joypads it will be like : “ui_acceptPL0”, “ui_acceptPL0PL1”, “ui_acceptPL0PL1PL2” and so on…
  4. Independent copies of events are entered into the new variable, in the form {new action : Array[InputEvent]}
  5. Inside the copied action events, as I already wrote, there are objects of different types. To separate events of only the required type InputEventJoyButton and InputEventJoyMotion, we must iterate the objects inside each “event” (Array[InputEvent]) and if this Object is of the required type, then add only this Object inside the copy of the “event” (Array[InputEvent]). Also, just in case, the device value is assigned to each new object inside the event using set_device()

# INFO: IF keyboard+Joypad - ERASE joy_button and joy_motion from start_actions
func erase_joypad_events():
	for action in start_actions:
		for index in range(start_actions_events[action].size()):
			if start_actions_events[action][index] is not InputEventKey:
				InputMap.action_erase_event(action, start_actions_events[action][index])

erase_joypad_events() - The last function is called if you need to have joypads and keyboards working simultaneously for different players.
Essentially, iteration of actions from the START SET is performed, then for each action - iteration of objects within each “event” (Array[InputEvent]). If the type of object in the event does not match the InputEventKey (keyboard), then this object is removed from the “event” (Array[InputEvent]). Thus, this function can be attached, for example, to the control type selection button at the beginning of the game or in the game settings.

FULL CODE:


extends Node

@onready var start_actions: Array = InputMap.get_actions()
var start_actions_events: Dictionary = {}
var players_in_game: Dictionary = {}
var prefix_action: String = "PL"

func _ready() -> void:
	
	init_inputmap()
	Input.connect("joy_connection_changed", _on_joy_connection_changed)
	

func _on_joy_connection_changed(device: int, connected: bool):
	var players = Input.get_connected_joypads()
	if connected:
		players_in_game[device] = (prefix_action + str(device))  #{0:PL0}
		add_player_inputmap(device)
	else:
		players_in_game.erase(device) #{0:PL0}


func init_inputmap():
	for action in start_actions:
		var events: Array = InputMap.action_get_events(action)
		var start_events: Array = events.map(func(i): return i.duplicate(true))
		start_actions_events[action] = start_events


func add_player_inputmap(device: int):
	for action in start_actions:
		var new_action: StringName = (action + prefix_action + str(device))
		if not InputMap.has_action(new_action):
			InputMap.add_action(new_action)
			
			# INFO: Duplicate InputEvents inside Array[InputEvent]
			var new_events: Array = start_actions_events[action].map(func(i): return i.duplicate(true))
			var new_actions_events: Dictionary = {}
			new_actions_events[new_action] = new_events
			
			# INFO: SET new_events to new_actions via dictionary keys
			for index in range(new_actions_events[new_action].size()):
				if new_actions_events[new_action][index] is not InputEventKey:
					new_actions_events[new_action][index].set_device(device)
					InputMap.action_add_event(new_action, new_actions_events[new_action][index])


# INFO: IF keyboard+Joypad - ERASE joy_button and joy_motion from start_actions 
func erase_joypad_events():
	for action in start_actions:
		for index in range(start_actions_events[action].size()):
			if start_actions_events[action][index] is not InputEventKey:
				InputMap.action_erase_event(action, start_actions_events[action][index])

Code for player movement inside the “Player” Script:

var input_id: String = "PL0"

func set_player_movement() -> void:
	direction = Vector2(
		Input.get_axis("ui_left" + input_id, "ui_right" + input_id),
		Input.get_axis("ui_up" + input_id, "ui_down" + input_id)
		).normalized()
	self.velocity = direction * character.speed_walk
	move_and_slide()

Setting up InputMap inside Project Settings:

I really hope this will help someone in the future! If you have any questions, write!

1 Like

I also managed to implement a split screen for my little game.

I ended up using resources to predefine some action names and add them through code for keyboard, p1’s gamepad and p2’s gamepad.

1 Like