§01 Kickstart Your Godot 4 MMO with a Golang Backend
Ready to dive in? In the introduction, we explored the motivations behind this project, the technology stack, and what to expect from the series. Now, we’ll start building. In this post, we’ll install the necessary tools, set up a basic packet system, and establish a simple communication flow between the server and the client to lay the groundwork for our MMO.
If you prefer, you can view this lesson on YouTube.
Installing essential tools
Let’s quickly install Golang and Godot, and then we will get to the fun stuff!
Golang
Head over to the Golang download page and simply follow the instructions for your OS. For reference, this project was created with version 1.23.3, but I don’t imagine there will be any issues with newer versions.
Test your installation by running go version
in your terminal. You should see something like go version go1.23.3 windows/amd64
, of course, with your OS and architecture. If you get an error, try closing and reopening your terminal.
Godot
The Godot website should detect your OS and provide you with the latest download link. One thing I love about Godot is that it doesn’t require an installation, you can simply unzip the download wherever you like and run the executable!
At the time of writing, Godot 4.4 is almost out. It has a feature I am excited to use, so I will be using experimental release 4.4-dev3. If you are reading this before 4.4 is released, you will have the smoothest experience following along with this experimental release as well. If you are reading this after 4.4 has been released, then please use the latest stable version.
⚠️ Note: If you are planning on hosting the game on your own server and have players connect using their web browsers, you will want to avoid 4.4-dev4+ for now. There is an issue getting the HTML5 export to connect to websocket servers when they are running on a port other than 443. This is a known issue and will likely be fixed in the future, but to be safe, stick with 4.4-dev3 for now.
One thing to ensure is that you get the Standard version of Godot, not the .NET version. This allows us to code in GDScript in the built-in editor, which is what we will be doing for this project.
Protocol buffers compiler
Since we are using Protocol Buffers to generate code for our packets, we need to install the compiler, protoc
. This is the tool that will take our .proto
file (a simplistic way to define our packets) and turn them into code we can use.
- Manually download from the latest release the zip file corresponding to your operating system and computer architecture (
protoc-<version>-<os>-<arch>.zip
), or on Windows, it will beprotoc-<version>-win64.zip
. - Unzip the archive your directory of your choice, but preferably one of the following standard locations:
%LOCALAPPDATA%
on Windows/usr/local/bin
on Unix
- Add the
bin
folder to your system’sPATH
environment variable.- On Windows, you can press Win + R, type
SystemPropertiesAdvanced
, click Environment Variables, and under User variables, double-click onPath
, then click New and paste the path to thebin
folder, which should look something like%LOCALAPPDATA%\protoc-<version>-win64\bin
. Press OK on all the windows you opened.
- On Unix, you can add the following line to your
.bashrc
or.zshrc
file:export PATH="$PATH:/usr/local/bin/protoc-<version>-<os>-<arch>/bin"
- On Windows, you can press Win + R, type
Test your installation by running protoc --version
in your terminal. You should see something like libprotoc 28.3
. If you get an error, try closing and reopening your terminal.
That’s it for now! We will grab the other tools as we need them.
Hello, world!
To make sure we can at least get something running, let’s create our main Go file.
Create a new folder for your project (I will call mine RadiusRumble
, but will often refer to it just as /
). Create the following structure, which is a common Go project layout. If you are interested, you can read more about it here.
/
└───server/
└───cmd/
main.go
Inside main.go
, add the following code:
/server/cmd/main.go
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
Now, open your terminal, navigate to your folder, and run the application:
go run server/cmd/main.go
You should see Hello, World!
printed to the console. If you see an error, check your system’s PATH
variable and double-check your Go installation.
Quality of life
I am willing to bet a good majority of you are using Visual Studio Code. If you already have your own preferred setup, feel free to skip this section.
We should really take a moment to install the Go for VS Code extension. It is kind of a must.
Now, let’s create our debug configuration and install the debugger.
- Open the debug tab on the left sidebar, or with Ctrl + Shift + D
- Click on create a launch.json file
- Choose Go: Launch package from the dropdown list
- A folder called
.vscode
will be created in your project root, with a file calledlaunch.json
. Replace its contents with the following:
/.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run server",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/server/cmd/main.go",
"output": "${workspaceFolder}/server/cmd/debug_executable.exe",
}
]
}
The output
field is highly recommended if you are on Windows, as without specifying this, the debugger will create a uniquely named executable each time you run the program, resulting in the firewall asking you to allow it every time. This way, you can simply add debug_executable.exe
to your firewall exceptions.
Now you should be able to simply press F5 to run your program, and look at the output in the Debug console tab at the bottom (not the Terminal or Output tab).
If you see an error saying dlv
is not installed, you can simply press the Install button that appears in the error message. If you don’t see this, you can install it manually:
go get -u github.com/go-delve/delve/cmd/dlv
Defining our packets
The heart of our project will be how we decide to communicate between the server and the client. Over the web, this might be done with JSON or XML, but we have the freedom to do whatever we want. We could even use a packed binary format for maximum efficiency, but to keep things semi-readable and reduce the chance of errors and writing the same code twice (once for the server and once for the client), we will use Protobuf.
Create a new folder called shared
in your project root, and inside it, create a new file called packets.proto
. Your project structure should now look like this:
/
├───.vscode/
│ launch.json
│
├───server/
│ └───cmd/
│ main.go
│
└───shared/
packets.proto
Inside packets.proto
, add the following code:
/shared/packets.proto
syntax = "proto3";
package packets;
option go_package = "pkg/packets";
// Define your messages
message ChatMessage { string msg = 1; }
message IdMessage { uint64 id = 1; }
// Define the main Packet message
message Packet {
uint64 sender_id = 1;
oneof msg {
ChatMessage chat = 2;
IdMessage id = 3;
}
}
The way we are defining our packets is pretty useful. We have a wrapper called Packet
, which encapsulates a message being anything we want to send, together with an ID to identify the sender. This way, we can easily add new messages, send/receive them, and know who they are from. Surprisingly, a lot can be done with just this basic structure.
The oneof
keyword is a way to define a union of messages. This means that only one of the messages defined in the oneof
block can be set at a time. We can easily grab the message type by checking which one is set.
As for the messages we have defined to start with, we have a ChatMessage
for sending chat messages, and an IdMessage
for when the server sends a client its ID.
Before we can use these messages, we need to compile them into Go code. Before we can do that, we need to install the Go protocol buffers plugin:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Now we're free to go ahead and compile our .proto
file. Run the following command in your terminal:
protoc -I="shared" --go_out="server" "shared/packets.proto"
This command tells the protoc
compiler to look in the shared
folder for the .proto
file, and to output the generated Go code in the server
folder. The first few lines of the packets.proto
file itself tell the compiler which package to use for the generated code, so we don’t need to specify it here.
You should see some new files appear in your server
folder under pkg/
:
/server/
├───cmd/
└───pkg/
└───packets/
packets.pb.go
If you feel like it, you can install the vscode-proto3 extension for Visual Studio Code. It will give you syntax highlighting and some other useful features for
.proto
files. One such feature is the ability to save the file and have it automatically compile the generated code. For that to work, you can add asettings.json
file to your.vscode
folder with the following content:Click to expand
/.vscode/settings.json
{ "protoc": { "path": "protoc", "compile_on_save": true, "options": [ "-I=c:\\Users\\<your name>\\Desktop\\code\\RadiusRumble\\shared", "--go_out=server", ] } }
Unfortunately, it looks like you do need to specify the absolute path to the
.proto
file, at least on Windows, with a lowercase drive letter. This is a bit of a pain, but it’s better than nothing.
Making packets in Go
In order to import our new package, we need to initialize a Go module inside our server
folder. It’s pretty common to name your module after your GitHub repository, but you can realistically name it whatever you want:
cd server
go mod init server
go mod tidy
You should see the creation of a go.mod
and go.sum
file in your server
folder. Now we can import our packets
package in main.go
:
package main
import (
"fmt"
"server/pkg/packets"
)
func main() {
packet := &packets.Packet{
SenderId: 69,
Msg: &packets.Packet_Chat{
Chat: &packets.ChatMessage{
Msg: "Hello, world!",
},
},
}
fmt.Println(packet)
}
When you run your program, you should see the following output:
sender_id:69 chat:{msg:"Hello, world!"}
So, I’ll be the first to admit: this looks like a lot of work for a simple message, so I create helper functions to make it easier to create packets. I define that in a new file called util.go
under /server/pkg/packets/
:
/server/pkg/packets/util.go
package packets
type Msg = isPacket_Msg
func NewChat(msg string) Msg {
return &Packet_Chat{
Chat: &ChatMessage{
Msg: msg,
},
}
}
func NewId(id uint64) Msg {
return &Packet_Id{
Id: &IdMessage{
Id: id,
},
}
}
Now we can simplify our main.go
file:
/server/cmd/main.go
// ...
packet := &packets.Packet{
SenderId: 69,
Msg: packets.NewChat("Hello, world!"),
}
fmt.Println(packet)
We can even serialize our packet to see what it will look like over the wire:
/server/cmd/main.go
package main
import (
"fmt"
"server/pkg/packets"
"google.golang.org/protobuf/proto"
)
func main() {
packet := &packets.Packet{
SenderId: 69,
Msg: packets.NewChat("Hello, world!"),
}
data, err := proto.Marshal(packet)
if err != nil {
fmt.Println("Error marshaling packet:", err)
return
}
fmt.Println(data)
}
When you run your program, you should see the following output:
[8 69 18 15 10 13 72 101 108 108 111 44 32 119 111 114 108 100 33]
Those are the bytes that represent our packet. It’s not very human-readable, but it verifies that we are easily able to send this between the server and the client. We can even deserialize it back like this:
/server/cmd/main.go
package main
import (
"fmt"
"server/pkg/packets"
"google.golang.org/protobuf/proto"
)
func main() {
data := []byte{8, 69, 18, 15, 10, 13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33}
packet := &packets.Packet{}
proto.Unmarshal(data, packet)
fmt.Println(packet)
}
sender_id:69 chat:{msg:"Hello, world!"}
Setting up the Godot project
I do suppose we’d better see how this all looks from the client perspective. Create a new Godot project in the same root folder as your server project. I will call mine simply client
. Make sure to select the Compatibility renderer if you want to export to the web, but you can always change this later.
First, let’s install the Godobuf plugin.
- Download the latest release. It should be called Source code (zip)
- Unzip the archive somewhere on your computer
- Copy the
addons
folder to yourclient
project folder. Your project structure should look like this:/ ├───.vscode/ ├───client/ ├───addons/ │ └───protobuf/ ├───server/ └───shared/
- Open your Godot project and enable the add-on by going to Project > Project Settings > Plugins and enabling the Protobuf plugin.
You should see a new Godobuf tab appear in the bottom left panel, underneath the scene tree, adjacent to the FileSystem tab. This is where we can input our .proto
file and generate our code.
- Click on the Godobuf tab
- Click on the … button under Input protobuf file and navigate up a level to your
shared/packets.proto
file. For some reason, it shows a warning that you will “overwrite” the file, but this is not the case. Just choose OK - Click on the … button under Output GDScript file, enter simply
packets.gd
and click OK - Click on Compile and you should see a popup appear saying Compile success done if it worked correctly
If you see an error, one thing I found is for some reason, it doesn’t like it if you name a field
message
, which is why I changed it tomsg
in the.proto
file. Check to see if you have done the same
Again, you can take a look at the generated code if you’re curious. It should appear in the FileSystem tab now, under res://packets.gd
.
Regardless, let’s see what it takes to create a packet in Godot.
Making packets in Godot
Create a new node in your scene called Main and attach a new script, main.gd
to it. Hit Ctrl + S to save your scene as main.tscn
before editing the script:
/client/main.gd
extends Node
const packets := preload("res://packets.gd")
func _ready() -> void:
var packet := packets.Packet.new()
packet.set_sender_id(69)
var chat_msg := packet.new_chat()
chat_msg.set_msg("Hello, world!")
print(chat_msg)
Here we are importing our generated code, which we will be able to access by typing packets.
and seeing the available attributes by pressing Ctrl + Space in the editor. This code is reminiscent of the Go code we wrote earlier, but with a few differences. We can’t directly set the message field of the Packet
object, but instead have to create a new message object and set its fields. This is because the Packet
object is a container for the message, and the message itself is a separate object.
When you hit F5 to run your project, you should get a popup asking you to select the main scene. Just choose Select Current and you should see the following output in the output console at the bottom of the editor window:
msg: "Hello, world!";
This is the same message we created in Go, but now we are doing it in Godot. We can even go through the same proof of concept by serializing and deserializing the packet. This time let’s try just getting the field value from the chat message:
/client/main.gd
var new_packet := packets.Packet.new()
new_packet.from_bytes([8, 69, 18, 15, 10, 13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33])
print(new_packet.get_chat().get_msg())
Hello, world!
Now that is certainly an interesting hello world program! It is important to note the proto.Unmarshal
function in Go is equivalent to the from_bytes
function in Godot. Using the same byte array used in Go allows us to confirm that data is consistent across both platforms, validating our protocol implementation.
Ok, so we can create packets in both Go and Godot, and have the means to convert them to/from bytes. So how do we send them to each other? Head on over to the next post! We’ll set up a websocket server in Go, establish client connections, and exchange these packets in real-time.
If you have any questions or feedback, I’d love to hear from you! Either drop a comment on the YouTube video or join the Discord to chat with me and other game devs following along.