TetraForce is an open-source multiplayer action-adventure RPG inspired by the popular Zelda game, Link's Awakening. It uses Godot’s built-in UDP networking library and scripting language for most of the game’s logic.
The main developers managing the project are fornclake and TheRetroDragon. Back in July, I got in contact with them to discuss putting TetraForce into the cloud! Within a week, TetraForce was running on AWS. Since then, there have been many gradual improvements to get the project where it is today.
For a summary of this project, the Amazon Elastic Container Service cluster manages and runs TetraForce’s containers using a serverless API. Initially, our cluster was hosted in an auto-scaling group of EC2 instances (Virtual Machines). Later, it was moved to AWS Fargate (Serverless Container Platform) to simplify scaling and reduce costs during periods of low utilization.
There is a Serverless REST API using Lambda for creating and joining rooms, which sends and sets server information, such as name and ECS task ID, in a DynamoDB table. When a server closes, an additional lambda removes it from the DynamoDB table.
Now let’s jump into some of the details.
Dockerization
Godot being lightweight, is easy to Dockerize. For the TetraForce Docker image, it just installs dependencies, the Godot server runtime, and the TetraForce’s pck
file. It can easily be extended to add additional pck
files in the future for mods or expansions.
As of when this post was written, this is our working Dockerfile:
FROM centos:centos8
RUN yum install -y wget unzip libXcursor openssl openssl-libs libXinerama libXrandr-devel libXi alsa-lib pulseaudio-libs mesa-libGL
ENV GODOT_VERSION "3.2.2"
# Install Godot Server
RUN wget -q https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_linux_headless.64.zip \
&& unzip Godot_v${GODOT_VERSION}-stable_linux_headless.64.zip \
&& mv Godot_v${GODOT_VERSION}-stable_linux_headless.64 /usr/local/bin/godot \
&& chmod +x /usr/local/bin/godot
# Create Runtime User
RUN useradd -d /tetra tetra
# Add pck file
ADD build/TetraForce.pck /tetra/TetraForce.pck
CMD /usr/local/bin/godot --main-pack /tetra/TetraForce.pck --empty-server-timeout=300
The CMD for the image is to run the Godot server runtime using the TetraForce pck
file. You can notice in the run command we also pass a --empty-server-timeout=300
; this is for our containers to close servers that have no active players automatically.
Server Management
Each game room runs as a task in ECS. Amazon assigns those tasks a public IP. The client receives those IPs by interacting with TetraForce’s API.
The REST API
The client can get room information and create new rooms by making HTTPS requests to the REST API. The REST API uses API Gateway to route requests to a Lambda functions.
The /create_server
endpoint takes in an optional parameter of a name. If it does not have a name, it will randomly generate one. From there, it will spin up a new ECS task tagged with the room’s name and add a new entry into the DynamoDB table. The Lambda function will return whether it was a success and the name of the room created.
The /get_servers
endpoint takes in an optional parameter of a room name and page. If it’s passed a name, it will only do a lookup for the given room. Otherwise, it will return a list of room information. The room information includes the room name, public IP, and port. It’s also modular to all for additional values in the future, such as version.
CloudWatch Events
Server cleanup is managed by an additional Lambda function that is triggered by a CloudWatch Event Rule:
{
"source": ["aws.ecs"],
"detail-type": [ "ECS Task State Change" ]
}
This event will trigger the Lambda anytime an ECS Task in the cluster enters a new state. That leaves the responsibility of verifying the task’s state and then removing the room from the DynamoDB table if it’s no longer running to the Lambda function.
Client Integration
Using Godot’s HTTPRequest
Node, the client can hit the REST API hosted on AWS to request server information or create a new lobby.
export(String) var api_endpoint = "api.tetraforce.io"
var _http_client : HTTPRequest# Asynchronous coroutine.
# Requests API for data from a specific server
# Returns: {"message": [MESSAGE], "data" : [DATA] }
func get_server(lobby : String) -> Dictionary:
_http_client.request("https://" + api_endpoint + "/get_servers?server=" + str(lobby), [], true, HTTPClient.METHOD_GET)
var result = yield(_http_client, "request_completed")
if len(result) > 3 and result[1] == 200:
var json : JSONParseResult = JSON.parse(result[3].get_string_from_utf8())
if json.error:
return _build_error_message(json.error_string)
return json.result
return _build_error_message("Request failed!")
After that, the client will parse the returned JSON object into a dictionary. That dictionary will include the public server IP and Port, which is passed into the connection method when the player attempts to connect to the game server.
What’s next?
The team is currently moving forward towards building a full demo of TetraForce within the next few months. The demo will include a handful of zones, a dungeon, and an eventual boss battle.
The team is looking to include skins in their game as Patreon rewards on the infrastructure side, so a user identity system is here on the horizon.
If you have any questions about anything, feel free to reach out to me on Twitter.
Related Links
TetraForce Discord: https://discord.gg/cxTBVCZ
TetraForce Repository: https://github.com/fornclake/TetraForce
Infrastructure Repository: https://github.com/josephbmanley/tetraforce-infrastructure
Top comments (3)
Great writeup Joseph.
I remember when Fornclake started Tetraforce. The Zelda tutorial hit morphed from helped me out tremendously. Glad to see it's still in development.
This is very helpful post !