|
|
|
|
Reply From: |
sustainablelab |
I came across your post while trying to solve the same problem:
- allow controllers to connect/disconnect mid-game
- extend to N controllers without manually editing the
InputMap
or manually scripting each player’s controller
From your description, I’m imagining a top-down maze with players running around. This imaginary top-down maze game has a scene hierarchy that looks like this:
Main
└─ World
├─ MazeWalls
├─ Player1
...
└─ PlayerN
Here is the general idea:
- script controller detection in
Main.gd
- script player creation and controller input mapping in
World.gd
Relationship between Main
and World
:
Main.gd
creates world
, an instance of the World
scene
- `World` has property `num_players` (the number of players)
- `Main` finds out how many controllers are connected and writes to `world.num_players`
- `World` has methods:
- `add_player(player_index)`
- `remove_player(player_index)`
- `Main` calls `world.add_player(device)` when `Main` detects a **controller connected**
Main
calls world.remove_player(device)
when Main
detects a controller disconnected
Main
detects controllers connecting/disconnecting using Godot’s built-in Input
Singleton
- `Input` has `signal` `Input.joy_connection_changed`
- In `Main.gd`, connect this signal to `func _on_joy_connection_changed(device, connected)`
- The signal sends an `int` **device** number and a `bool` **connected** that is **true** if connected, **false** if disconnected
func _on_joy_connection_changed
uses device
and connected
to figure out which controller was connected/disconnected, and accordingly tell world
whether to add or remove a player.
Main
only knows about controllers and their device numbers. It doesn’t care about the players. So when Main
detects a controller, it tells World
the controller’s device number.
World
only knows about players. It doesn’t care about controllers.
To work with up to N players, World
managers the players in an Array
called players
. When World
adds or removes players, it needs to know the player_index
. This is the index into the array players
.
player_index
and controller device
number are the same number! Arrays start at index 0 and devices are enumerated starting at 0, so device
works as the player_index
.
Now for the meaty part – map inputs for up to N players:
World.gd
defines func add_player(player_index)
:
- instantiate a player (a player scene:
Player.tscn
)
- append that player instance to the array of
players
- add the player as a Child Node of the
World
node
- assign values to the player’s properties:
- a **position** in the maze
- an **appearance** (e.g., picking a color)
- an **input map**
- assigning player position and appearance might sound unrelated to the controller question, but to satisfy the works up to N players requirement, everything needs to be automated
- so
func add_player
needs to handle all assignments in a scalable way, not just input map assignment
- two approaches that are both scalable:
- generate random values
- create a `Dictionary` where player is number is the dictionary key and the value (e.g. the player's color) is the dictionary value
Use a Dictionary
for the input map.
Actions are the keys in the input map dictionary. These key string mimic Godot’s default InputMap
action strings. This example shows a typical mapping using the default strings for right/left/up/down motions:
var input_map = {
"ui_right": Vector2.RIGHT,
"ui_left": Vector2.LEFT,
"ui_up": Vector2.UP,
"ui_down": Vector2.DOWN,
}
To make the above code scalable, use String.format()
to suffix the keys with the controller device number:
var input_map = {
"ui_right{n}".format({"n":player_index}): Vector2.RIGHT,
"ui_left{n}".format({"n":player_index}): Vector2.LEFT,
"ui_up{n}".format({"n":player_index}): Vector2.UP,
"ui_down{n}".format({"n":player_index}): Vector2.DOWN,
}
Automate assigning the input map to the player. Make an Array
of input maps. When a new player is added, append this input map. As defined in the String
formatting above, the player_index
(the device
number) is substituted for n
.
So to summarize so far, as players are added, add_player
creates these input_map
dictionaries and adds to them to the array of input_maps
.
Now to edit the actual InputMap
that connects the InputEventJoypadMotion
action events with the actions defined in the array of input_maps
.
Full code is in my repository GitHub - sustainablelab/eat-and-poop: Learn the Godot Engine by making a game. in script World.gd
. Here is an example for joypad right.
func add_player(player_index: int) -> void:
...
var right_action: String
var right_action_event: InputEventJoypadMotion
right_action = "ui_right{n}".format({"n":player_index})
InputMap.add_action(right_action)
# Creat a new InputEvent instance to assign to the InputMap.
right_action_event = InputEventJoypadMotion.new()
right_action_event.device = player_index
right_action_event.axis = JOY_AXIS_0 # <---- horizontal axis
right_action_event.axis_value = 1.0 # <---- right
InputMap.action_add_event(right_action, right_action_event)
When the controller disconnects the player automatically stops moving (obviously the player must stop moving because there are no more joypad motion action events incoming for that player!). And when the controller reconnects, the player starts moving again!
This behavior comes for free with this solution because everything World
needs to know about the player (like which controller is mapped to this player) is still in the player
instance in the array players
.
When the controller reconnects, it is assigned the lowest available device
number. So the device
number still matches the correct player_index
into the array of players
.
To provide visual feedback when a controller is disconnected/reconnected, I have a “dead” appearance and a “live” appearance. When a controller disconnects, I set the player alpha
to 0.3, making it appear faded out. When the controller reconnects, I restore alpha
to 1.0.
Hardware tests
-
Setup:
- two wired XBox 360 controllers
- two (wireless) Steam controllers
- Windows 10
-
Tests:
- Expect connecting/disconnecting/reconnecting controller mid-game always works
- connect/disconnect/reconnect by plugging/unplugging/plugging the controller
-
Test results:
- Launch from the Godot editor (
F5
): PASS
- Launch from exported
.exe
: PASS
- Launch from Steam as non-Steam game added to Steam library: FAIL
- Cause of failure: Godot built-in
Input.joy_connection_changed
signal is not always emitted when a controller is connected/disconnected
Thanks, you saved me !!!
Murilovsky2030 | 2021-11-23 00:54