Finishing the game round when a player wins

In our game, when all players except one fell, or if somehow all players fell, we finish the game.

This happens in two phases.

  1. Showing the victory screen:

    • We play the game over animation, a distant camera rotating around the level.

    • Then, we load the victory scene, where the Gobot characters applaud the winner.

  2. Ending the game round: We wait for some time and restore the lobby.

We separate this process into two phases because there are many cases where we might want to immediately close the round without playing the finish animation. For example, if the server or a player disconnects, we want to immediately send the player back to the lobby.

While the wrap-up phase will be an RPC (Remote Procedure Call) dispatched by the server when the game is won, closing the game is local to each player.

Let's add those two phases!

First, we'll use a signal to communicate the state to the loaded scene. Near the top of the game_state.gd file, write:

## Emitted when someone won the match.
signal game_round_won()

Then, let's implement show_victory_screen(). We also write a convenience function for closing the connection.

## Clears the network peer and closes the connection.
func disconnect_from_game() -> void:
    if multiplayer.multiplayer_peer:
        multiplayer.multiplayer_peer.close()
        multiplayer.multiplayer_peer = null


## The server calls this function when the game ends, and it gets called on all
## clients.
##
## Because we do not always want to play the win animation before returning to the lobby,
## this is separate from the `close_game()` function.
##
## This function restores the mouse on all clients and adds the victory scene.
@rpc("authority", "call_local")
func show_victory_screen(winner_name: String, winner_id: int) -> void:
    var lobby = get_tree().get_root().get_node_or_null("Lobby")
    # This function is only meant for players. On the server's side, there's no lobby instance.
    # If we're on the server, we don't want to do anything. The game process will be closed.
    if lobby != null:
        game_round_won.emit()
        # We wait some time to let the game over animation play a bit, and close the connection to the game round.
        await get_tree().create_timer(2.0).timeout
        disconnect_from_game()

        # We replace the level scene instance with the victory scene instance and restore the mouse cursor.
        get_node("/root/Level").queue_free()
        Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
        var victory_scene = load("res://assets/victory_scene/victory_scene.tscn").instantiate()
        get_tree().get_root().add_child(victory_scene)
        victory_scene.setup(winner_name, winner_id)

If there is no lobby, we're on the server. We don't need to do anything.

If there is a lobby, we emit the game_round_won signal. We wait for the game over camera to finish orbiting around the level. Then, we close the connection and remove the level.

We load the victory scene locally for each player. The victory scene doesn't need to communicate with the server. It only needs to know the name and number of players, which it gets from the GameState autoload.

When each player presses space or clicks in the victory scene, we want to run another function, close_game(). Let's add it, still in the game_state.gd file:

## Each client calls this function locally after the match has ended and the
## victory animation has played.
##
## Removes all scenes, disconnects all clients, and restores the lobby.
func close_game() -> void:
    # If the level instance is there, then the game is in progress and we need to end it.
    # So, we remove the level.
    if has_node("/root/Level"):
        get_node("/root/Level").queue_free()
    # Same for the victory scene.
    if has_node("/root/VictoryScene"):
        get_node("/root/VictoryScene").queue_free()

    # If we ended the game early, for example, because of a disconnection, we need to restore the mouse here.
    Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
    # The game round effectively ends here, so we clear the player list in preparation for the next round.
    game_closed.emit()
    players.clear()
    _players_ready.clear()

Clearing the players and emitting the game_closed signal will effectively restore the lobby.

And with that, we have all the building blocks for a complete game loop. All that's left code-wise is wiring things in the _ready() function. That's what we'll do in the next part.