Setting up the game to run on the server

In this part, we will add the code needed to run an instance of the game on the server.

This is the host instance of the Godot game, the authoritative server that will run the game simulation in the cloud.

The W4 services will run a new host instance of the game when the lobby is ready for a game round. That game instance will serve as the game's host and server, receiving player input and sending back positions.

Then, when the round ends, we will stop this instance of the game project.

It means that each time players start a game round, a new game instance is freshly run in a container in the cloud.

Let's write some functions that will only run on the server.

We need to create functions that:

  • "Shut down" the server at the end of a game round. This will close the running Godot instance for that game round.

  • Instantiates the game level on the server and all connected clients.

  • Set up the running game instance on the server to accept connections.

Let's set up how to shut down first. For that, we use W4GD.game_server.set_server_state():

## Requests to shut down this running game instance in the cloud.
##
## This function can only be called from the server.
func shutdown_server() -> void:
    W4GD.game_server.set_server_state(W4GD.game_server.ServerState.SHUTDOWN)

This function will do nothing if called from the player's computer. This is why there's no need to do any checks before running it.

Next, let's add the functions that set up the game for all players. Setting up the game involves:

  1. Loading the game level.

  2. Creating an instance of the game level on every player's computer and the server.

  3. Having each player's computer notify the server that they are ready to play.

  4. Once all players are ready to play, the server notifies players that the game can start.

We will use two functions for that:

  1. _begin_game(): this will be called by the server and run on all participants, including the server. It will instantiate the game level.

  2. _set_player_ready(): Each participant will call this function, adding the player to the ready list.

We'll also use a signal to notify when all players are ready.

Add the _begin_game() function to the game_state.gd file, right after shutdown_server().

There will be an error because _set_player_ready() doesn't exist yet. Please don't mind it; we'll add the missing function in a moment.

## Instantiates the game level on the server and for all connected players.
##
## The authority on this function is server, as indicated by the `@rpc` annotation.
@rpc("authority", "call_local")
func _begin_game() -> void:
    var level = load("res://level/multiplayer_level.tscn").instantiate()
    get_tree().get_root().add_child(level)
    var lobby = get_tree().get_root().get_node_or_null("Lobby")
    if lobby != null:
        lobby.hide()
        _set_player_ready.rpc()

The GDScript @rpc (Remote Procedure Call) annotation controls how this function works in an online multiplayer scenario and has two arguments:

  • "authority": this function can only be called from the server.

  • "call_local": this function will be executed on all connected peers (all running game instances, including players and the server).

This will load the level (multiplayer_level.tscn) and create an instance of the scene for all participants. Once the level is loaded, the participant will call _set_player_ready() on all peers, notifying their presence.

Note

To learn more about RPCs in Godot, head to the official Godot documentation: Remote Procedure Calls.

Somewhere towards the top of the file, add a signal for when every player is ready to start the game round, and an array to store the players who are ready:

## Emitted from any peer as soon as all players are ready to play.
signal all_players_ready()

## Stores player peer ids for player who are ready to play (connected to the lobby).
var _players_ready: Array[int]= []

Then, lower down the script, add the _set_player_ready() function. We also throw in a are_all_players_ready() function to verify is all players are ready.

## Returns `true` if all the players are ready.
## You can use this utility function from anywhere to update the user interface for players waiting.
## For example, to display a label indicating we're waiting for everyone to connect.
func are_all_players_ready() -> bool:
    return _players_ready.size() == players.keys().size()

## Players call this method when connecting to the running game instance.
##
## This is called from any peer, and dispatches to all peers, so everyone can know
## that a specific player is ready.
@rpc("any_peer")
func _set_player_ready() -> void:
    _players_ready.append(multiplayer.get_remote_sender_id())
    # If all players are ready, we emit the signal so we can start the game.
    if are_all_players_ready():
        all_players_ready.emit()

The @rpc annotation here specifies this runs on all peers. When each peer is ready, they will run this RPC (Remote Procedure Call) command on all peers.

To recap:

  1. The game server calls _begin_game(). The call gets repeated on every peer, which instantiates the game level on each player's computer.

  2. Each peer calls _set_player_ready(). The call gets repeated on every peer, which adds the peer's id to the _players_ready array.

  3. When all peers have run that call, they have a _players_ready array that's equal to the players list size.

  4. Each local peer emits all_players_ready, which we can use to update the user interface in the local game instance easily. It's not strictly necessary, but it's convenient.

Let's add a convenience function to begin the game and ensure that it can only be run on the server:

## Starts the game for all players.
##
## Run it only from a server.
func begin_game() -> void:
    # This assert is just here to inform a developer working on the project, as it'll only run in debug builds.
    assert(multiplayer.is_server(), "This function should only run on the server.")
    _begin_game.rpc()

Finally, let's wire all of this in a host_game() function that starts listening to player input.

First, around the top of the file, specify a default port to communicate with the server:

## Default game server port.
const DEFAULT_PORT := 10567

Note

The default port can be any number between 1024 and 49151 that is not on the list of registered or common ports as of November 2020.

Let's also add signals for when the game ended or an error occurred at the top of the file:

## Match victory scene has finished playing
signal game_closed()
## There was an error
signal game_error(what: String)

We can add the host_game() function now:

## Whichever client calls that method hosts a game and becomes a server.
##
## This is normally only called from a game hosted on W4.
func host_game() -> bool:
    # When W4 is ready to start a game, we call the begin_game() function.
    W4GD.game_server.match_ready.connect(begin_game)
    game_closed.connect(shutdown_server)
    game_error.connect(func error_and_shut_down(msg: String):
        printerr("GAME ERROR: %s"%[msg])
        shutdown_server()
    )

    # ENet is a library integrated into Godot that provides a simple and efficient communication
    # layer on top of UDP sockets. Here, we use it to open communication on the server.
    var peer := ENetMultiplayerPeer.new()
    peer.create_server(DEFAULT_PORT, MAX_PEERS)

    # If the server fails to start, we tell the user and return false.
    if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
        OS.alert("Failed to start multiplayer server.")
        return false

    # This is a property of nodes that allow them to interact with Godot's networking system.
    # Setting this property enables network communication.
    multiplayer.multiplayer_peer = peer

    # Change the server's state to ready so it can be allocated to a game round.
    W4GD.game_server.set_server_state(W4GD.game_server.ServerState.READY)
    return true

With that, we have some of the basic functions needed to run a game instance on the server.

We also should add the functions that make the server finish a game, but we'll implement joining as a player first; things will make more sense this way.

We'll add the functions to join a game in the next part.