How to make a local multiplayer game

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

Hi there!

I’m clueless of how to implement a local multiplayer logic into my game. It’s a simple top-down shooter (nothing special really, since I’m pretty much just starting with Godot and game dev. in general) and I want to instance n-player_classes controlled separately by each controller/player

The problem is that I have a player class (controlled via the input manager) that should be instanced as soon as someone is connecting his controller or press a button → how can I tag/assign this particular instance to the connected controller? Furthermore is it possible to extend the player count dynamically with each new controller connected? Unfortunately I couldn’t find anything anywhere about local (not online) multiplayer implementation.

It would be awesome if someone/you could explain the general approach towards this problem. It’s more important to understand the solution instead of copy/paste code.

Thanks guys :smiley: any help is very much appreciated!

You’ve looked at the documentation on higher level multiplayer, right? The implementation can work for local player, as well as online, games.

Ertain | 2019-04-28 23:05

I do not think the OP is talking about LAN multiplayer. I think the OP means the scenario where one computer has several controllers connected. The documentation on high-level multiplayer does not cover implementation for this local player scenario.

sustainablelab | 2020-12-31 23:26

:bust_in_silhouette: 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