💣 Bomber Demo

This guide will walk you through creating and deploying a multiplayer game with Godot 4.2.



Overview

Godot provides great multiplayer capabilities out of the box. In this tutorial, we'll take Godot's bomber demo example and add Rivet to it. This demo uses ENet, but it's easy to swap out for another of Godot's high-level networking APIs.

Godot bomber demo preview


Step 1: Clone project

We'll be using a modified version of Godot's multiplayer bomber demo. First, start by cloning Rivet's example repo: https://github.com/rivet-gg/examples

The bomber demo is in godot/bomber. The demo has been refactored to try and reduce the amount of code that will be modified for the Rivet integration.

The main branch of the repo contains the final code. If you want to follow along and add all of the required code yourself, you can check out the pre-rivet branch.


Step 2: Set up the Rivet plugin

If you're using the main branch of the example repo, then it will already contain a recent version of the Rivet plugin. If you're using the pre-rivet, then start by following the installation instructions on the plugin's repo.

Once the plugin is installed in Godot, you'll need to link your project with Rivet by signing in. You can find the Rivet plugin in the same area as the FileSystem window in Godot.

Godot plugin deploy screen

Step 3: Update game code

Configure Rivet

We need to create a new file to configure how our game will be run on the Rivet Cloud. Copy the following to rivet.yaml:

engine:
  godot:

matchmaker:
  max_players: 12
  docker:
    dockerfile: 'Dockerfile'
    ports:
      default:
        port: 10567
        protocol: 'udp'
  game_modes:
    default: {}

Update gamestate.gd

In this tutorial, all of the code we need to change is in the gamestate.gd script. Each code block below will replace part of a single function.

First, we'll start with the ready function. We need to hook up the RivetHelper autoloaded script to the local start_server signal. This is so the plugin can handle part of the multiplayer setup before our code does. We also need to call setup_multiplayer to initialize the multiplayer system.

func _ready():
	RivetHelper.start_server.connect(start_server)
	RivetHelper.setup_multiplayer()

	multiplayer.peer_connected.connect(self._player_connected)
	# ...

Next, we're going to update how our code starts the server. This code will run on the Rivet servers when your game is deployed, and so it uses the rivet_print function that is part of RivetHelper. This makes it easier to see logs that are about Rivet, and not about your game when you're looking in the hub.

func start_server():
	print("Starting server on %s" % DEFAULT_PORT)

	peer = ENetMultiplayerPeer.new()
	var error = peer.create_server(DEFAULT_PORT, MAX_PEERS)
	RivetHelper._assert(!error, "Could not start server")
	multiplayer.set_multiplayer_peer(peer)
	var response = await Rivet.matchmaker.lobbies.ready({})
	if response.result == OK:
		RivetHelper.rivet_print("Lobby ready")
	else:
		RivetHelper.rivet_print("Lobby ready failed")
		OS.crash("Lobby ready failed")

Note that in the code above, we use the Rivet matchmaker with Rivet.matchmaker.lobby.ready(). This is a part of Rivet's API, you can read docs about all of the matchmaker endpoints. This call will tell Rivet that the server is ready to accept players. Later, we'll call Rivet.matchmaker.lobby.setClosed() to tell Rivet that the lobby is closed and no more players can join.

Continuing with the new code, we'll update how the game client connects to a server. We're first going to add code that will use Rivet's matchmaker to find a lobby to join. Next, if the response is OK, we'll connect to the server using the port that was returned from the matchmaker.

func join_game(new_player_name):
	print("Joining game as %s" % new_player_name)
	player_name = new_player_name

	var response = await Rivet.matchmaker.lobbies.find({
		"game_modes": ["default"]
	})

	if response.result == OK:
		RivetHelper.set_player_token(response.body.player.token)

		var port = response.body.ports.default
		print("Connecting to ", port.host)

		peer = ENetMultiplayerPeer.new()
		var error = peer.create_client(port.hostname, port.port)
		RivetHelper._assert(!error, "Could not start server")
		multiplayer.set_multiplayer_peer(peer)
	else:
		print("Lobby find failed: ", response)
		game_error.emit(response.body)

Finally, the last thing we need to do is tell Rivet that the lobby is closed after the game is started. This makes sense for this game, since we don't want players to be able to join a game that has already started. However, other games might want to leave the lobby open so that players can join mid-game.

@rpc("any_peer")
func begin_game():
	if !multiplayer.is_server():
		return
	# Tell Rivet that this lobby is closed
	await Rivet.matchmaker.lobbies.setClosed({})

	load_world.rpc()
	var world = get_tree().get_root().get_node("World")

Step 4: Deploy to Rivet

Write Dockerfile

Add the following to Dockerfile:

FROM ghcr.io/rivet-gg/godot-docker/godot:4.2.1 AS builder
WORKDIR /app
COPY . .
RUN mkdir -p build/linux \
    && godot -v --export-release "Linux/X11" ./build/linux/game.x86_64 --headless

FROM ubuntu:22.04
RUN apt update -y \
    && apt install -y expect-dev \
    && rm -rf /var/lib/apt/lists/* \
    && useradd -ms /bin/bash rivet

COPY --from=builder /app/build/linux/ /app

# Change to user rivet
USER rivet

# Unbuffer output so the logs get flushed
CMD ["sh", "-c", "unbuffer /app/game.x86_64 --verbose --headless -- --server | cat"]

Deploy game server

Now that we've prepared the game to be built with Rivet integrated, we can deploy it to the Rivet Cloud.

In the Rivet plugin, we can go to the deploy tab, and select "Staging" from the dropdown. Then, click "Build & Deploy". This will build the Dockerfile we just created, and upload the Docker image to Rivet's servers.

Godot plugin deploy screen

Deploy game client

Now that we've deployed the game to Rivet, we can build a version of our game that will be able to connect to Rivet. We can do this either through Godot's export system, or by running the game from the editor. Both are configured to get access to the tokens they need for the namespace you have set.


Conclusion

You're all done!

Was this page helpful?

Edit Page

Rivet

Open-source solution to deploy, scale, and operate your multiplayer game

This website is not sponsored by or affiliated with Unity Technologies or its affiliates. Unity Trademark(s) are trademark(s) or registered trademark(s) of Unity Technologies or its affiliates in the U.S. and elsewhere. | This website is not sponsored by, affiliated with, or endorsed by Epic Games, Inc. or its affiliates. 'Unreal Engine' is a trademark or registered trademark of Epic Games, Inc. in the U.S. and elsewhere. | The HTML5 Logo by the World Wide Web Consortium (W3C), used under a Creative Commons Attribution 3.0 License. Source | The Godot Engine Logo by the Andrea Calabró, used under a Creative Commons Attribution 4.0 International License. Source | Docker and the Docker logo are trademarks or registered trademarks of Docker, Inc. in the United States and/or other countries. Docker, Inc. and other parties may also have trademark rights in other terms used herein.

© 2024 Rivet Gaming, Inc. All rights reserved.