Making a Game - Handling Player Disconnects - EP 30

by

Video Tutorial

Aims of this Tutorial

  • Make the server decide whether we should continue the game or cancel the game when a player quits.
  • If we continue the game, make the server control the 'zombie' player with a plan with no thrust or weapons firing.
  • If we cancel the game, broadcast this to players to let them know that the game is cancelled.

When do we cancel the game?

To answer this question, consider the minimal configuration for the game to continue. This would be 2 alive, online players. Thus, we need to determine how many players are alive and online and see if we have 2 or more to continue. We have the ids of all the online players in our root node, and each player object has the is_alive member variable. To find the player corresponding to an id, we have a dictionary on the arena that we can use to look up the player object. Now where does all this code go? Well, we need it to be called when a player disconnects. Luckily, we have a callback already connected that we can use, so let's use that!

# root_node.gd

func _peer_disconnected(id : int) -> void:
	ids.erase(id)
	self.console_print("Player has disconnected with id " + String(id))
	if !playing:
		return
	var alive_online_players : int = 0
	$Arena.players[id].is_zombie = true
	for id in ids:
		if $Arena.players[id].is_alive:
			alive_online_players += 1
	if alive_online_players < 2:
		self.server_request_exit()

The playing variable here just keeps track of whether the server is in the lobby, waiting for players (and so we do not want to do any extra handling beyond logging the disconnect), and when the server is running a game for a set of players. Also note that we set the player object belonging to the player who disconnected to a zombie. This will be important in the next part. Of course, this code will not work until we write the server_request_exit function.

Creating zombie plans

What do I mean by a zombie plan? Well, if a player disconnects, what happens to the player's ship? Does it disappear? If so, does it reappear when we start the plan and disappear when we get to the same timestamp? If not, what do we do if the disconnected player had an integral part in the plans so far, how do we handle that? My solution to all these problems is to not make the ship disappear, but rather make the server control it by giving it plans with no thrust and no weapons firing. This makes it a sitting duck, but does not affect anything before it, while avoiding the players requiring to suspend their disbelief that a ship could just vanish for no reason.

So when we calculate our plans, if we are a zombie, we need to create a zombie plan. This can be done by using the remote functions for building a plan that we used before! This function is given below.

# player.gd

func create_zombie_plan() -> void:
	add_thrust_element(5, Vector3.ZERO, Vector3.ZERO)
	add_weapons_element(5, false)

Note that we also need to add one more condition to check if everyone is ready, and that is to add and !player.is_zombie because zombies are always ready for the next turn as they will create their plan!

Quitting the game

We already have a function to end the game when someone clicks the end game button. But we can't quite use this function, because we want to alert the player that the game was cancelled. To do this, we will call a popup on the client. The code on the server to handle this essentially a copy of the old function, but calling a different rpc.

# root_node.gd

func server_request_exit() -> void:
	console_print("Server requested exit...")
	rpc("server_exit_requested")
	$ExitTimer.start()

Now, on the client, we need to handle this in almost the same way as before, but also show a popup. To show a popup, we need to make the popup first. As a child of the root node, we can create a PopupDialog node and change its transform to center it and resize it. Then we can create a Label node as a child of that and give it some informing text. The rpc will then call popup on the popup node.

# root_node.gd

remote func server_exit_requested() -> void:
	switch_to_lobby()
	$DisconnectDialog.popup()

Gitea Commits

The relevant commit for the client is found here.

The relevant commit of the server is found here.