Automatically Update Godot Game Clients
Posted on Sun 04 September 2022 in Tech • 5 min read
Scenario
I've been working on making a Godot game called Cosmic Trading Crew recently. This is the first time I've had a game server and game client, as the players are all connecting to a central server to join a crew and play together. I've been deploying new versions of the server code as I go along, but I realized that anyone using an older client may not be able to play without fully downloading the latest version. On top of that, any changes I make between client versions may cause clients to break.
I decided to make the client automatically download the latest game code whenever it started. This way, anyone who installs the game will rarely have to re-download the game from the website. Additionally, I will be able to streamline my server code so it doesn't have to handle older client versions in addition to the latest versions.
Godot PCK Files
When you build or export a Godot game, it creates a PCK file that has all of the game code and assets inside. Sometimes this is a part of the executable, and sometimes it is a separate file. When you export, there is an option to just export the PCK.
I decided to make no modifications to my existing game client. Instead, I will build this PCK file whenever I have changes and host it in a public S3 bucket. I'm doing all development in Mac OSX, so I figured out the CLI command to build a PCK.
For hosting, I'm using an AWS S3 bucket with a Cloudfront distribution so I can use my domain in Route 53 to point right to the file. I use the AWS CLI to do all of this, since I had the bucket and Cloudfront distribution already set up. This way, I don't need to use the S3 URL, I can instead use something like "https://mydomain.com/file.pck".
Here's my bash script that does all of these steps for me:
# Build the PCK
/Applications/Godot.app/Contents/MacOS/Godot --path ./client --export-pack "Mac OSX" ./builds/cosmic-trading-crew-client.pck
# Upload to my S3 bucket
aws s3 cp client/builds/cosmic-trading-crew-client.pck s3://mybucket.com
# Invalidate the Cloudfront distribution so users aren't getting a cached version
aws cloudfront create-invalidation --distribution-id E99Z9A9AAA99A9 --paths '/*'
Building a Game Launcher
Instead of trying to modify my existing Godot project to load a different PCK file for itself, I decided to create a whole separate Godot project just to load my current project. This sounds complicated, but the new project is basically doing nothing except downloading and launching something. Once complete, I can build this launcher for all OS types and upload those to itch.io one time, and anyone who gets it will be able to play the latest version without re-downloading.
Scene tree
The launcher project is a single scene with a Control node as the root. I added some child nodes of a panel and label that just says "Loading...", but these children are optional.
GDScript
Since my original project had a script that was an autoload singleton, I had to make the same autoload variable name. This is pointing to a dummy file that does nothing, but the variable needed to be defined for the original game project to work. In the script below, I'll replace this dummy file with the version from the PCK.
Attached to the root Control node is a script.
extends Control
var done := false
var last_bytes := 0
var time := 0.0
var http
func _ready():
http = HTTPRequest.new()
add_child(http)
http.use_threads = true
http.download_file = "user://cosmic-trading-crew-client.pck"
http.connect("request_completed", self, "_on_file_request_completed")
http.request("https://mydomain.com/cosmic-trading-crew-client.pck")
func _process(delta):
if not done and time >= 1:
var speed_in_bytes = http.get_downloaded_bytes() - last_bytes
var speed_in_mega_bytes = speed_in_bytes / 1000000.0
print(speed_in_mega_bytes, " MB/s")
last_bytes = http.get_downloaded_bytes()
time = 0
time += delta
func _on_file_request_completed(result, response_code, headers, body):
done = true
print("File downloaded")
run_game()
func run_game():
ProjectSettings.load_resource_pack("user://cosmic-trading-crew-client.pck")
# res:// now includes the original game project
g.set_script(load("res://src/globals.gd"))
g.initial_setup()
get_tree().change_scene("res://src/mainmenu.tscn")
The short version of what's happening in each function is:
-
_ready()
- Define the download settings and file destination, hook up the signal for when the download is complete, then start the download of the given URL. -
_process(delta)
- Show the download progress and speed as debug messages. This is optional, and my PCK file is currently so small that I don't even see these most of the time. It can be useful if your project is bigger. -
_on_file_request_completed(...)
- When the file is done downloading, start the game. -
run_game()
- Load the PCK file, replace the temporary global singleton with the loaded singleton, then go to the main project's initial scene.
Downsides
The biggest downside of this approach is that it adds a whole new Godot project to the mix. I already had two projects (client and server), so this is now a third project in the same game. That said, I don't think this project will require much maintenance, as most development will be in the main game project.
The other downside to the current implementation is it requires an always-on internet connection. For my current project, that's fine, as the game doesn't work without connecting to the server anyway. However, I think this could be modified to look at a checksum and compare it to the existing PCK file. Alternatively, you could provide an option to the user to download when they want to do so, or use the existing files. For my goals of having a constantly updated client, I wanted it to always download instead.
Conclusion
This solution ultimately serves my current needs of having a client that downloads the latest version of the game without the user having to manually go to the game's website to download and install it. This way, a user can have the client downloaded, and any time they play the game I know it will connect to the server and be running the latest deployed PCK. This reduces my server complexity of having to be backwards-compatible with all possible clients. It also reduces the steps I need to take to deploy new clients, as before I'd have to build all OS versions and upload to itch.io and then notify users of the new version.