© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
B. BorsGame Backend Developmenthttps://doi.org/10.1007/978-1-4842-8910-5_3

3. Dedicated Game Servers

Balint Bors1  
(1)
Munich, Germany
 

In Chapter 1, you implemented a very simple way of server hosting. In this chapter, you go further and implement a more advanced solution with the help of PlayFab, as well as in Azure with Kubernetes and Agones. You will explore and learn what you need to create online multiplayer games.

First, you will review different server hosting alternatives. Then, using PlayFab’s multiplayer server hosting feature and the provided SDK, you will enable online multiplayer mode for your game.

In the second part of this chapter, you move on and build a server hosting environment in the cloud. Agones, on top of Kubernetes, will manage and allocate the servers for each online game session. You will learn how to build your own backend infrastructure.

Server Hosting Alternatives

Dedicated game server hosting means basically that you want your server to run somewhere in the cloud and your clients to have access to it, even if the clients are located all over the world. Cloud-hosted game servers allows players to play together, without being in the same location or on the same network.

Technically, these two capabilities must be in place:
  • Server hosting. The server must run on a machine somewhere on the Internet and be reachable by all clients.

  • Server management. Some logic must manage the lifecycle of game servers. For example, you want to allocate servers for specific players or shut down servers if the players finish the game. If you consider it in broader terms, scaling up or down the servers can also belong here. This implies that a single virtual machine will not be sufficient.

Figure 3-1 shows three ways to host your server.

Three columns and three rows represent a relation between the game server, server management, and server hosting versus the application servers named IAAS, PAAS, and GBAAS.

Figure 3-1

Dedicated game server hosting in the cloud

  • The most basic and traditional way is when you have a (virtual) machine with public Internet access or pay for a hosting provider to host your server. You can also buy virtual machines from a cloud provider, then you decide on the Infrastructure as Service (IaaS) model. Here, you only have a machine with no capability to manage your game servers or even scale on demand. You need to write your custom logic to solve these problems, which can take considerable effort. You can see an example of this solution in Chapter 1.

  • The most convenient way is to let a Game Backend-as-a-Service (GBaaS) provider do everything. You use their API to access their services and you only need to worry about making your game. In this chapter, we implement this approach with the help of PlayFab.

  • In between, you find the Platform as a Service (PaaS) model. Azure Kuberentes Service (AKS) is a fully managed PaaS offering from Microsoft. Still, you have to configure it, which can be a daunting task. But it implements a lot of useful functionalities, for example, it scales automatically when demand grows. On top of this, you need the game server management functionality. You can use an existing production-proof solution, such as Agones. In this chapter, you implement this approach in Azure with the help of Kubernetes.

Now that you have understanding the various server hosting options, let’s start by exploring the GBaaS way.

PlayFab

PlayFab provides a convenient way for server hosting. You only need to configure PlayFab and use the PlayFab Game Server SDK (GSDK) to access the servers. PlayFab arranges all the management tasks. It scales the servers according to your need.

Configuring PlayFab

By default, PlayFab disables the multiplayer server feature. After you log in to PlayFab, you should enable it from the Multiplayer menu. If you have not yet added your credit card information, choose Add Credit Card.

PlayFab will not charge you until you reach the limit. Currently, it is 750 free core hours and 10GB of egress (outgoing) traffic. It is good enough to experiment with PlayFab.

Caution

Be aware that this means core hours. If you choose, for example, a two core virtual machine, which is the minimum, you only have 375 core hours. You can run out of the free tier limit easily and then PlayFab will charge you.

On this page, you can see the current pricing of PlayFab services: https://playfab.com/pricing/

After you submit your credit card information, you can enable the multiplayer server feature of PlayFab.

A game server is basically a process that runs on an operating system, which runs on a machine. Nowadays, we use the more flexible virtual machines (VMs) instead of real hardware. So you need a provider, which hosts the virtual machine and the server process on it. But what happens if thousands of players suddenly want to use your game?

You then need more virtual machines to serve the increasing demands, and an automatism which spins up additional VMs. You cannot know how many VMs you need in advance. Instead of saying you need a specific amount of VMs, you define how and when PlayFab should create VMs.

You define this in PlayFab with the help of game server build. A build contains the following properties:
  • Virtual machine size. PlayFab currently provides Dasv4 (2-16 cores) VMs. This type comes from Azure’s virtual machines instances. The D-type is for running general-purpose workloads.

  • Servers per machine/server type. If you choose the Containers server type, PlayFab will generate as many containers on each virtual machine as many servers per machine you choose. Each container will have a different port number, but they will all map to the game port you define for Mirror in Unity (the default value is 7777).

  • Assets. These are files you upload to the server in compressed (ZIP) format. This should contain your server process, which will automatically run on the VM (as defined under the Start command).

  • Network. The port and protocol for incoming network traffic. If you choose the default Mirror settings, the port is 7777 and the protocol is UDP (KCP counts as UDP).

  • Region. Currently the options are EastUS or NorthEurope. You define two important parameters here. These are the only parameters that you can also change later for your build:
    • StandingBy server. This is the number of servers in StandingBy state waiting to become active. These are running servers flagged as StandingBy because currently no player is using it. Starting up a server can take time, so PlayFab starts up these servers before players actually allocate them. You can also set the Dynamic Standby option later, which will provision additional StandingBy servers dynamically with the growing demand.

    • Max servers. The maximum number of servers in the chosen region. Obviously, the number of StandingBy servers must be smaller than or equal to the configured max servers.

Caution

Except for the number of StandingBy and Max servers, you cannot change any parameters of your build later.

To each build, you will get a BuildID. Figure 3-2 illustrates the key components of the multiplayer server hosting implementation in PlayFab. You can have multiple builds, and within each build, you have multiple regions. Each region can have multiple servers (virtual machines), and each server has containers where the server process runs.

To join to a specific server, clients should only provide the BuildID, the PreferredLocation, and the SessionID.

Classification of multiplayer represents build ID, preferred location, host ID, session ID, and clients.

Figure 3-2

Multiplayer server hosting implementation

When first creating a new build, it will turn into the Initialized state. You need to increase the number of StandingBy servers, so that the build goes into the Deployed state. First, it goes through the Deploying state, checking if the server you created in Unity can turn into the StandingBy state. This happens only if you implemented the ReadyForPlayers method and call it periodically.

Configuring Unity

First, you need to download the PlayFab Game Server SDK (GSDK) for the Unity game engine. The simplest way to download the Unity package is from the following URL. Then import all the assets into your project:

https://github.com/PlayFab/gsdk/raw/main/UnityGsdk/MpsGsdk.unitypackage

In order to use it, you need to enable the ENABLE_PLAYFABSERVER_API directive in the Unity Editor. Go to File ➤ Build Settings…and select Dedicated Server and Switch Platform. Now go to Player Settings… ➤ Player ➤ Dedicated Server Settings ➤ Other Settings ➤ Scripting Define Symbols. Then add the directive to it and click Apply.

Without this directive, you cannot compile, because of the #if ENABLE_PLAYFABSERVER_API directive in the GSDK code.

Create two game objects, called PlayFabServer and PlayFabClient, and add them to your scene as a child of the PlayFab game object. Also create two scripts (such as PlayFabServer.cs and PlayFabClient.cs) which will contain the code. You should add these scripts to the respective game objects, so that Unity can run them.

Inherit the server and the client scripts from Mirror’s NetworkManager. Make sure that you have only one NetworkManager at one time on the scene.

Note

You can put the client and server in separate projects if you want. This can be useful, especially if it is a bigger project. For this tutorial, we keep them in one project.

Implementing the Server

You can now create the server. From the GDSK, you need to start PlayFabMultiplayerAgentAPI and call the ReadyForPlayers() method every five seconds. Put all the server code into the PlayFabServer.cs script.
using System.Collections;
using UnityEngine;
using Mirror;
using PlayFab;
public class PlayFabServer : NetworkManager
{
    void StartPlayFabAPI()
    {
        PlayFabMultiplayerAgentAPI.Start();
        StartCoroutine(ReadyForPlayers());
    }
    IEnumerator ReadyForPlayers()
    {
        yield return new WaitForSeconds(.5f);
        PlayFabMultiplayerAgentAPI.ReadyForPlayers();
    }
}
You have to start the Mirror server as well. You inherited the PlayFabServer class from the NetworkManager, so that you can call the StartServer method of Mirror right away by initializing PlayFab. Add the Start method to the PlayFabServer class:
    void Start()
    {
        StartPlayFabAPI();
        this.StartServer();
    }

The server will then listen to a port for incoming remote client connections.

Note

Here, you merely start a server and not a host. In case of a host, you would also start a client on the same machine.

In Unity’s Inspector, you can see the following server parameters:
  • Transport. By default, Mirror currently uses KCP transport, which claims to be the fastest and best suited protocol for game-related traffic. You can choose from many, but basically use TCP if reliability is important, and UDP if speed has precedence over losing data.

  • Network address. For servers, it is always the localhost.

  • Max. connections. The maximum number of allowed concurrent connections.

  • Disconnect inactive connections. When you want to remove players whose clients are not responding to the given Disconnect Inactive Timeout time period.

Although the Transport and the NetworkManager cannot live without each other, they have a separate component by design. Mirror wanted to separate the way of transport, so that you can change it any time and use the most suitable one.

Check your PlayFabServer in the Inspector. If your Transport field is None, drag-and-drop the Kcp Transport component into it.

You can change the transport parameters in Unity Inspector. For now, the most important parameter is the port number. It is the port your game server is listening on. By default, Mirror uses 7777. Be sure to avoid port conflicts; in most cases, the default value just works fine. Other parameters help optimize the performance of data transfer between the clients and the server.

Shutting Down the Server

It is important to implement server shutdown procedures to avoid running servers that no one uses. You can simply count the number of newly joining players. When a player leaves the session, you decrease this counter. As soon as the counter reaches zero, that means you can stop the server process and have PlayFab free up the container for new sessions. To achieve this, add the following code to your PlayFabServer class:
    private int numberOfConnectedPlayers = 0;
    public override void OnServerConnect(NetworkConnectionToClient conn)
    {
        base.OnServerConnect(conn);
        Debug.Log("Connected client to server, ConnectionId: " + conn.connectionId);
        numberOfConnectedPlayers++;
    }
    public override void OnServerDisconnect(NetworkConnectionToClient conn)
    {
        base.OnServerDisconnect(conn);
        Debug.Log("Client disconnected from server, ConnectionId: " + conn.connectionId);
        numberOfConnectedPlayers--;
        if (numberOfConnectedPlayers == 0)
        {
            StartCoroutine(Shutdown());
        }
    }
    private IEnumerator Shutdown()
    {
        yield return new WaitForSeconds(5f);
        Application.Quit();
    }

Building the Server

Make sure you disable the NetworkManager and the PlayFabClient game objects. Drag-and-drop the PlayFabServer.cs script to the PlayFabServer game object. Add the player prefab (Slime) to it just as in case of the NetworkManager. Then, go to File ➤ Build Settings… and select Dedicated Server.

Note

In Unity Hub, the Linux Dedicated Server Build Support must be installed.

Select a folder and generate your game server.

Testing Your Server

You can choose to test your server on your local machine before uploading it to PlayFab. It’s useful to debug if you have an issue integrating the GSDK. You have these options:
  • LocalMultiplayerAgent. Previously called MockVMAgent, which emulates the PlayFab environment on your local machine. With the VmAgent, the game server can go through the states you will also have in your real PlayFab deployment (https://github.com/PlayFab/MpsAgent).

  • Thundernetes. A newer initiative to debug your server locally, where you deploy your server to a customized Kubernetes cluster. The PlayFab team implemented custom resources for Kubernetes to manage the state transitions of your server (https://github.com/PlayFab/thundernetes).

Both solutions are available only for testing and debugging your solution and not for using in real situations. You also have a third option for debugging:
  • PlayFab Connect. You can connect to your virtual machine directly through your PlayFab Game Manager. Here you use a remote desktop or SSL and go to your virtual machine directly. You can inspect each server and log with the help of simple command-line tools.

Unfortunately, PlayFab does not give you too much information when there is an issue with your server. For example, when it is hanging at deployment. It is a good idea to test it locally first. You will see how to use Thundernetes for local testing.

Thundernetes

You can create a Docker image, which includes an operating system and the game server files. Then, you’ll create running containers from this image when a new session starts. Now you just need something that automates this process. Kubernetes is a production-proof container orchestration system, which is exactly what you need.

Thundernetes runs on top of Kubernetes. This is an extra layer that implements the behavior you need for running game servers.

You’ll now install a local Kuberentes cluster with Thundernetes and deploy your game server on it. Start by installing the prerequisites for this scenario:
Start Minikube and verify that it creates a Kubernetes cluster and pods in the kube-system namespace:
minikube start
kubectl config current-context
kubectl get pods -A

Install Thundernetes on the cluster.

Tip

You can find more information about the installation steps of Thundernetes at https://playfab.github.io/thundernetes/quickstart/installing-thundernetes.html.

Start by installing the certificate manager for Kubernetes:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.0/cert-manager.yaml
Verify that the installation was successful:
kubectl get pods -n cert-manager
Install the Thundernetes core logic:
kubectl apply -f https://raw.githubusercontent.com/PlayFab/thundernetes/main/installfiles/operator.yaml
Remove the DaemonSet for Windows, because you will use Linux servers in this example:
kubectl delete -n thundernetes-system daemonset thundernetes-nodeagent-win
Verify that the pods are up and running:
kubectl get pods -n thundernetes-system
Next, you have to create your Linux-based image, which will include your game server. Go to your game server’s folder and create a file called Dockerfile. Copy the following into it:
FROM ubuntu:18.04
WORKDIR /game
ADD . .
CMD ["/game/mygameserver.x86_64", "-nographics", "-batchmode", "-logfile"]
Execute the following command in your Dockerfile’s folder. Make sure you provide a tag for your image:
docker build -t mygameserver-image:0.1 .
Verify that the image was built:
docker image ls mygameserver-image
Now load this into Minikube:
minikube image load mygameserver-image
And verify that the image is loaded:
minikube image ls
Create a new file called GameServerBuild.yaml. You can read about each parameter of a game server build in this documentation: https://playfab.github.io/thundernetes/gameserverbuild.html
apiVersion: mps.playfab.com/v1alpha1
kind: GameServerBuild
metadata:
  name: mygameserver-build
spec:
  titleID: "ABCD"
  buildID: "85ffe8da-c82f-4035-86c5-9d2b5f42d6f5"
  standingBy: 3
  max: 3
  crashesToMarkUnhealthy: 5
  portsToExpose:
    - 7777
  template:
    spec:
      containers:
        - image: docker.io/library/mygameserver-image:0.1
          name: mygameserver
          ports:
          - containerPort: 7777
            name: gameport
Apply this configuration to the Kubernetes cluster:
kubectl apply -f GameServerBuild.yaml
This creates a new kind of Kubernetes resource called GameServerBuild. It will instruct the earlier deployed Thundernetes resources how to deal with game servers. Verify that everything is working fine:
kubectl get gsb
NAME                       STANDBY ACTIVE    CRASHES    HEALTH
mygameserver-build         3/3                          Healthy
kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
mygameserver-build-ehusv   1/1     Running   0          9m20s
mygameserver-build-tuvhm   1/1     Running   0          9m20s
mygameserver-build-zkewy   1/1     Running   0          9m20s
If the pods are not in the Running state, you can troubleshoot with the help of these commands:
kubectl describe pod mygameserver-build-ehusv
kubectl logs mygameserver-build-ehusv --follow

The logs should show a StandingBy state. Let’s allocate a server to see if the transition to the active state happens, and if the client receives a server IP of the allocated server.

Tunnel the Thundernetes controller’s external IP to your localhost with the help of Minikube. This is important, because otherwise you cannot reach the API:
minikube tunnel
Now verify that the external IP became your localhost:
kubectl get svc thundernetes-controller-manager -n thundernetes-system
NAME                              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
thundernetes-controller-manager   LoadBalancer   10.105.20.188   127.0.0.1     5000:31270/TCP   3h29m
You can now allocate a server with a POST request, and Thundernetes provides the appropriate IP and port where your clients can connect:
curl -X POST "http://localhost:5000/api/v1/allocate" -H "Content-Type: application/json" --data-raw "{"buildID": "<your build Id>","sessionID": "<your session Id>"}"
{"IPV4Address":"192.168.58.2","Ports":"7777:10006","SessionID":"ac1b7082-d811-47a7-89ae-fe1a9c48a6da"}
Finally, verify that you have one active server:
kubectl get gsb
NAME                 STANDBY   ACTIVE   CRASHES   HEALTH
mygameserver-build   2/3       1                  Healthy

If the server works on the local Thundernetes cluster, it should also work on PlayFab. You can now move on to deploy it to PlayFab.

Creating a Build in PlayFab

Now you have everything to create a build in PlayFab. Choose Multiplayer ➤ Builds ➤ New Build. Upload the earlier created docker image to the provided registry.

Check with Docker that the image is available:
docker image ls mygameserver-image
In PlayFab’s New Build form, select the Upload to Container Registry link. Then, click the Copy Docker Login command. This will allow you to log in to the container registry. Execute it on your command line:
docker login -u <username> -p <password> <registryId>.azurecr.io
Create the image using the registry:
docker build -t <registryId>.azurecr.io/mygameserver-image:0.1 .
Upload it to the registry:
docker push <registryId>.azurecr.io/mygameserver-image:0.1
After you have uploaded the image, fill in the form as shown in Figure 3-3. Because this image contains the game server files, you don’t need to ZIP it and upload it again.

The user interface of My game studio represents servers including new build details of name, server details, container image, and assets.

Figure 3-3

Creating a new build in PlayFab

It will take a few minutes before PlayFab provisions your server and accepts client requests.

Troubleshooting Problems

If your server code has some issue, PlayFab cannot deploy the server, and it will go into the Unhealthy state. Here, you can only try to debug locally, as you cannot go to the server directly with a remote desktop.

Otherwise, if the deployment was successful, but the server still has some issue or you want to investigate the logs, you can use PlayFab Connect.

Choose PlayFab ➤ Multiplayer ➤ <your build name> ➤ Servers ➤ Connect, and RDP onto the machine. You should start a command prompt by searching for cmd on the server. Then right-click it and choose Run as Administrator.

To list the currently active and running containers, use this command:
docker ps
You can also see how Docker maps the VM port to your game server port, which you set up in Unity at the Transport component:
0.0.0.0:30001->7777/udp
With docker inspect, you can get runtime information about your container. Apply docker ps to get the container ID and then use the following command:
docker inspect <containerid>
You can also inspect the logs coming from each server. You can see the actual state of your server and follow the changes with the following command:
docker logs <containerid> --follow
state: PlayFab.MultiplayerAgent.Model.HeartbeatRequest, payload: {"CurrentGameState":"StandingBy","CurrentGameHealth":null,"CurrentPlayers":[]}
Operation: Continue, Maintenance:, State: StandingBy
Another possibility is to go directly onto the container and investigate problems from there (on Linux start sh instead of cmd):
docker exec -it <containerid> cmd

Creating the Client in Unity

On the client side, you have to call the RequestMultiplayerServer method and pass the following information to PlayFab:
  • BuildId. This is the ID you get after creating the build in PlayFab’s Game Manager. You can find the ID under your build’s name (choose Multiplayer ➤ Server ➤ Builds).

  • PreferredRegions. This must fit to the region configured for your build (currently either EastUS or NorthEurope). You can have multiple regions configured for one build. With this parameter, you can route the player to the closest region to reduce latency.

  • SessionId. This can be an existing ID, which means the player joins an active game session. Or this can be a new ID, which means the player starts a new game where other players can join.

It is simple to implement, and you can let PlayFab do the heavy work for you. With these three parameters, you can navigate your clients to the proper server. Create a new script file PlayFabClient.cs and add it to the PlayFabClient game object.
using System.Collections.Generic;
using UnityEngine;
using Mirror;
using PlayFab.MultiplayerModels;
using PlayFab;
using kcp2k;
public class PlayFabClient : NetworkManager
{
    GameObject playFab;
    public void StartPlayFabClient()
    {
        playFab = GameObject.Find("PlayFab");
        RequestMultiplayerServerRequest requestData = new RequestMultiplayerServerRequest();
        requestData.BuildId = playFab.GetComponent<PlayFabSettings>().buildId;
        requestData.PreferredRegions = new List<string>() { "EastUs" };
        requestData.SessionId = playFab.GetComponent<PlayFabSettings>().sessionId;
        if (requestData.SessionId.Equals(""))
        {
            requestData.SessionId = System.Guid.NewGuid().ToString();
            playFab.GetComponent<PlayFabSettings>().sessionId = requestData.SessionId;
        }
        PlayFabMultiplayerAPI.RequestMultiplayerServer(requestData, OnRequestMultiplayerServer, OnRequestMultiplayerServerError);
    }
    private void OnRequestMultiplayerServer(RequestMultiplayerServerResponse response)
    {
        this.networkAddress = response.IPV4Address;
        this.GetComponent<KcpTransport>().Port = (ushort)response.Ports[0].Num;
        Debug.Log("Server found. IP " + this.networkAddress + ":" + this.GetComponent<KcpTransport>().Port);
        Debug.Log("SessionId: " + response.SessionId);
        this.StartClient();
    }
    private void OnRequestMultiplayerServerError(PlayFabError error)
    {
        Debug.Log(error.ErrorMessage);
    }
}

If the request was successful, the OnRequestMultiplayerServer method provides RequestMultiplayerServerResponse, which contains the IP address and port number of the server assigned to this session. You pass this information to the Mirror client, so that it can connect to the listening server.

Extending the ControlPanel

To simulate the client starting a new game, give a button to the Control Panel. Go to the ControlPanel.cs script and simply add a button that allows you to start a PlayFab client:
GameObject playFabClient;
private void Start()
    {
        playFabClient = GameObject.Find("PlayFabClient");
    }
void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("Start PlayFab Client"))         playFabClient.GetComponent<PlayFabClient>().StartPlayFabClient();
}

Testing

You can start your client from a Unity Editor instance and see if it joins the server automatically. Disable PlayFabServer and enable the PlayFabClient game object. Drag-and-drop your player prefab (Slime) to the Player Prefab field of PlayFabClient. When you start your game, don’t forget to authenticate the player first.

If you want to join to the same session, open the cloned project in another Unity Editor. Copy the session ID from PlayFabSettings in the original project so a second player can join to the same session. With matchmaking, you can simplify this step. The matchmaker will decide if you can join to an existing session or start a new one.

You can observe how the server changes from StandingBy to Active in PlayFab’s Game Manager, and also back to StandingBy when all the players leave.

Now that you have learned how to build and use multiplayer game servers in the backend in PlayFab, the next section shows you how to build the same functionally with Azure.

Azure

It is time to build your server in Azure and compare it to the GBaaS solution. You need a basic understanding of containerization, Docker, Kubernetes, and a general understanding of cloud computing and Azure, as well as Terraform. The earlier chapters help with in this, explaining the main concepts and commands to move forward.

The Problems

Basically, you want your Mirror server to run on a machine, listening on a port, to which you can connect with your clients. The clients only need an IP and a port number for the connection. In theory, you could simply put the Mirror server to a virtual machine in Azure and connect to it to the clients.

However, this solution is too simplistic. You would notice its limitations quickly:
  • Every time a new player starts a new game, a new session will start. For additional game sessions, you need additional servers. Thus, you need an automatism to create new servers generated when the players start new game sessions. On the other hand, you need an automatism to deallocate the game server when the game finishes and the players leave the session. You need a mechanism to handle the transition of game server states, i.e. some kind of logic that manages your servers.

  • Your player base will increase (hopefully) with time. At the beginning, you might have only ten players, but that can go up to millions of players. The backend should automatically increase the number of servers, or even decrease when fewer player want to use it. This scaling up and down of the infrastructure should happen automatically.

If you go with pure Azure services, you would quickly encounter these problems. You can decide to implement game server management and scaling on your own. That’s a lot of work, and I would not suggest it for individuals or small development teams. Here is where Agones, on top of Kubernetes, can be a valid option.

Kubernetes and Agones

Agones is a free, open-source library to host, run, and scale dedicated game servers on top of Kubernetes. Kubernetes is the de facto standard for container orchestration used by many companies around the world. The problem with Kubernetes is that Google originally invented it to manage web-based workloads.

A web session is usually stateless, so a client’s request can go once to one server, the next time to another, and it will not disturb the user. In case of games, however, your client needs to communicate with the same server. Also, if the server goes down, the game session is over.

For a stateless web application, this would not be a problem. So Kubernetes could solve the scaling problem, but you need something specific for the game. That is where Agones helps. Agones will manage the state of your game servers throughout the complete lifecycle, from their creation to shutting them down.

With that, you can conclude that the combination of Kubernetes and Agones provides a powerful foundation to host your game servers. If you got to know Thundernetes earlier, you will notice the similarity between them.

Be aware that building a Kubernetes cluster with Agones is relatively easy, but more challenging than building it with a GBaaS solution, such as PlayFab. This chapter goes through each step, explaining what happens and why. For this solution, the developer should understand not only the game code but also the infrastructure code.

Building Agones

Nothing is easier than installing Agones on a Kubernetes (AKS) cluster. Download the following Terraform script:

https://github.com/googleforgames/agones/blob/release-1.24.0/examples/terraform-submodules/aks/module.tf

Copy it into an empty folder and execute the following commands:
terraform init
terraform apply
After the Terraform script finishes, you receive a readily built AKS cluster with Agones on top. You could deploy the game. In reality, you will face the following problems:
  • The Terraform script will not run through completely at first. You can start debugging to determine why.

  • You don’t know what exactly you have deployed with the Terraform script. That can cost a lot of money. Using resources in the cloud costs money, so you have to be careful as to which services you use.

  • You want to understand the infrastructure (code) as much as you want to understand the application code. Otherwise, you cannot fix, extend, or operate it.

  • You should only deploy the minimum required infrastructure resources to minimize costs.

The solution is to build and learn your infrastructure step-by-step.

Three Steps to Build the Infrastructure

I suggest using Visual Studio Code with the related plugins and terminal window for developing the Terraform scripts and executing CLI commands.

To have some structure, you can put each resource in its own file (such as aks.tf, acr.tf, and so on). When you execute terraform apply, Terraform will go through all the files in the current folder.

Later, if you want to extend the infrastructure with additional resources, you can also distribute the Terraform code in multiple folders and modularize it.

Figure 3-4 shows the steps needed to build the infrastructure for the game servers on Azure. You can divide the steps into three major blocks:
  1. 1.

    Provisioning the Azure infrastructure resources (such as the AKS cluster, public IP, or the container registry).

     
  2. 2.

    Deploying Agones and the game server image on top of the infrastructure you created in the earlier step.

     
  3. 3.

    Creating Agones resources (such as fleets) for running game servers, automatically scaled, and providing the server IP and port of ready servers for clients.

     

Block diagram of Azure agons represent local machine, Azure with user ID location and network security rule, and Agones .dev file.

Figure 3-4

Implementing Agones on Azure

To summarize what you need to build: first, you will provision a couple of resources with the help of Terraform in Azure. This starts with an AKS cluster, which comes with a lot of additional resources such as load balancer, network security group, and public IP. After the Kubernetes cluster is running, you will deploy and configure Agones on top of it.

You will also need to create a container registry to store the server image. From this registry, Kubernetes deploys the game servers as needed. Agones will manage the game servers by organizing them in fleets and allocate according to the needs of your clients.

The next sections go through these steps in detail.

Provisioning Azure Resources

You take the Terraform code from the previous chapters and extend it with the resources built in this chapter.

Create a Resource Group

Create a resource group, which is a logical grouping of Azure resources. Although the resource group itself is location independent, it shows the location of the included resources. You will refer to this location's value from other resources in the next steps. Create a file called resourcegroup.tf and add the following content:
resource "azurerm_resource_group" "agones" {
  name     = "agones-resources"
  location = "West Europe"
}

Build Azure Kubernetes Service

You create the core component, the Azure Kubernetes (AKS) cluster, next. Note that even though the Terraform code looks short, Azure will generate several additional resources as well in the subscription.

Here you refer to the location you set in the previous step on the resource group and to the resource group (agones-resources) itself, so you should add the AKS cluster to that group.

A node on Kubernetes means a virtual machine, a virtual machine is a scale set in Azure. At the default_node_pool you define the type of the virtual machine you want to use under the AKS cluster. The Standard_B2 is one of the cheapest (if not the cheapest) virtual machine, so for experimenting and building the infrastructure, it is the most cost effective. You can also choose to have only one node (one VM) for the same reason. In production, you can increase this based on the number of players.

It is important to set the enable_node_public_ip to get public IPs on nodes. Otherwise, it will not be possible to access the servers from the outside world. Create a file called aks.tf with the following content:
resource "azurerm_kubernetes_cluster" "agones" {
  name                  = "agones-cluster"
  location              = azurerm_resource_group.agones.location
  resource_group_name   = azurerm_resource_group.agones.name
  dns_prefix            = "agones"
  default_node_pool {
  name                  = "default"
  node_count            = 1
  vm_size               = "Standard_B2s"
  enable_node_public_ip = true
  }
  identity {
    type = "SystemAssigned"
  }
}
When creating a Kubernetes cluster in Azure, Azure will automatically create multiple AKS supporting infrastructure components. For those it will create a new resource group (which starts with MC_). If you observe the content of this resource group, you will find the following components generated:
  • Network security group: This is a kind of firewall to control which inbound and outbound traffic Azure allows. It is associated with the AKS subnet. You can extend this with your own network security rule.

  • Route table: This determines where to route the traffic; what the next hop IP address is.

  • Virtual machine scale set: This includes the VM instances; the nodes under the Kubernetes cluster.

  • Load balancer: This is only for outbound traffic. The VMs under AKS sometimes have to reach the Internet, for example, to download updates.

  • Public IP: This is attached to the Load Balancer for outbound traffic.

  • Virtual network: All the components are in a virtual network, which you can further divide into subnets. This determines the internal address space and IPs for the servers.

Verifying the Cluster

After you have all the terraform scripts done, run them with the following:
terraform apply

If Terraform finishes without any problem, you have the Kubernetes cluster in Azure. You can check this in the Azure Portal or through the command line.

Kubectl is the command you use to manage Kuberentes resources. Before using it, you should configure the kubeconfig file. With the help of Azure’s CLI, you can get the cluster’s credentials easily. With the following command, you put cluster administrator credentials in a file:
az aks get-credentials --name agones-cluster --resource-group agones-resources --file agones-kubernetes-config --admin
Without giving a filename, the credentials will go into the default $HOME/.kube/config file. You need to set the KUBECONFIG environment variable to your own configuration file, otherwise kubectl will look for the default kubeconfig file:
set KUBECONFIG=C:Agonesagones-kubernetes-config
You have to make sure that you are in the right context. You can have multiple clusters, for example, the one you created with Minikube locally. Make sure that kubectl works with is context you created in Azure.
kubectl config get-contexts
CURRENT   NAME                   CLUSTER          AUTHINFO                      NAMESPACE
*         agones-cluster-admin   agones-cluster   clusterAdmin_agones-resources_agones-cluster

Add a Network Security Rule

Agones will generate the port numbers for the servers between 7000 and 8000. With the AKS cluster, you have already received a network security group. However, you need to add a new rule to allow inbound UDP traffic for the ports between 7000 and 8000.

You need to apply a small trick to get the name of the network security group. Terraform provides the opportunity to read resources, which were not directly created by Terraform itself. In this case, you only created the Kubernetes cluster by Terraform, which then created the corresponding network security group, so in Terraform it is not directly defined. To resolve this, you need to search for a network security group type in the node resource group and pass it as a parameter to the network security rule. Create a new file called nsg.tf and add the network security group to it:
data "azurerm_resources" "agones" {
  resource_group_name = azurerm_kubernetes_cluster.agones.node_resource_group
  type = "Microsoft.Network/networkSecurityGroups"
}
resource "azurerm_network_security_rule" "gameserver" {
  name                        = "gameserver"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Udp"
  source_port_range           = "*"
  destination_port_range      = "7000-8000"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_kubernetes_cluster.agones.node_resource_group
  network_security_group_name = data.azurerm_resources.agones.resources.0.name
}

Be aware that this may not give you the network security group name upon the first Terraform apply. This is an API issue. In this case you need to run Terraform later again or copy the network security group’s name directly into the Terraform code. Fortunately, it is generated from the DNS prefix of the nodes. If it is agones, then the name of the network security group will always be aks-agentpool-55978144-nsg.

Build Azure Container Registry

You also need an Azure internal container registry, where you store the server images. Kubernetes will pull this image and create a container when Agones requires a new server. By enabling the admin user, you can later push the game server image to ACR with the provided username and password. Create a new file called acr.tf and add the following resource:
resource "azurerm_container_registry" "agones" {
  name                = "agonesacr"
  resource_group_name = "agones-resources"
  location            = azurerm_resource_group.agones.location
  sku                 = "Basic"
  admin_enabled       = true
}

Accessing ACR from AKS

It is important to attach the Azure Container Registry to AKS, otherwise AKS cannot pull images from there. You will store the images in ACR and AKS should be able to pull images anytime it needs. Note that you can also do this step through CLI, after you have AKS and ACR. This is also valid for all the terraform code, but in this case, you may find it more convenient to do it with the CLI.
az aks update -n agones-cluster -g agones-resources --attach-acr agonesacr
However, you want that all the infrastructure code is in Terraform. Let’s attach the ACR to the AKS. Here you will use the Azure Active Directory provider (azuread). First, register an application in Azure AD. Let’s extend the acr.tf with the following:
resource "azuread_application" "agones" {
  display_name = "agones"
}
Then create a service principal and associate it with the registered application:
resource "azuread_service_principal" "agones" {
  application_id  = azuread_application.agones.application_id
}
Add a password to the service principal. Azure will automatically generate it and Terraform will export the value of it:
resource "azuread_service_principal_password" "agones" {
  service_principal_id = azuread_service_principal.agones.id
}
In the scope of ACR, you need to assign the AcrPull role to the service principal:
resource "azurerm_role_assignment" "agones" {
  scope                = azurerm_container_registry.agones.id
  role_definition_name = "AcrPull"
  principal_id         = azuread_service_principal.agones.object_id
}
Finally, change the system assigned identity of the AKS cluster to this newly created service principal. Make sure that the service principal is newer than the AKS cluster with the depends_on meta argument.
resource "azurerm_kubernetes_cluster" "agones" {
...
  # identity {
  #  type = "SystemAssigned"
  # }
  service_principal {
    client_id     = azuread_application.agones.application_id
    client_secret = azuread_service_principal_password.agones.value
  }
  depends_on = [
        azuread_application.agones,
        azuread_service_principal.agones,
        azuread_service_principal_password.agones
  ]
}

With this method, you have allowed AKS to pull images from ACR.

Install Agones Using Helm

Now that you have created the Azure infrastructure, it is time to deploy Agones. With the help of Helm CLI, it is really easy. Helm will use your kubeconfig. Make sure you are in the right context.
helm repo add agones https://agones.dev/chart/stable
helm repo update
helm install my-release --namespace agones-system --create-namespace agones/agones

Creating and Deploying the Server

After you have deployed the Agones infrastructure, you have to develop the server code in Unity that will receive the client requests. After building the code, create an image from it and push it to the container registry in Azure.

Implementing the Server

You have to add Agones Unity SDK to Unity first. Go to Unity ➤ Window ➤ Package Manager ➤ + ➤ Add Package from GIT URL…, and copy the following URL to the text box:

https://github.com/googleforgames/agones.git?path=/sdks/unity#v1.22.0

In Unity Editor, create two new game objects, AzureServer and AzureClient, and add them as child objects of the Azure game object. You should also add the two core Mirror components, NetworkManager and KcpTransport, to them. Drag-and-drop the player prefab to the Player prefab field.

Create a new script called AzureServer.cs and fill it with the following:
using UnityEngine;
using Agones;
public class AzureServer : MonoBehaviour
{
    private AgonesSdk agones = null;
    async void Start()
    {
        agones = GetComponent<AgonesSdk>();
        bool ok = await agones.Connect();
        if (ok)
        {
            Debug.Log("Server is connected.");
        }
        else
        {
            Debug.Log("Server failed to connect.");
            Application.Quit();
        }
        ok = await agones.Ready();
        if (ok)
        {
            Debug.Log("Server is ready.");
        }
        else
        {
            Debug.Log("Server ready failed.");
            Application.Quit();
        }
    }
}

Without these calls, the game server will be stuck in Unhealthy state. You have to add the AgonesSDK component to the AzureServer game object.

Now leave only the AzureServer game object enabled and create a server build. Go to Unity Editor, File ➤ Build Settings…, select Dedicated Server as Platform, and set the Target Platform to Linux. Click Build and select a new folder for the server files.

Create Your Game Server Image

In this step, you create the game server image. Go to the folder where you created your server build. Create a file named Dockerfile and copy it into the game server folder. This describes your image. When a container is created, your server will start automatically. Also, you have to wait for some time until the sidecar container starts up.
FROM ubuntu:18.04
WORKDIR /game
ADD . .
CMD sleep 2 && ./mygameserver.x86_64
In the same folder, execute Docker to build the image. It is important to set the tag of the registry to the target ACR. You can also set the images with the actual version numbers:
docker build -t agonesacr.azurecr.io/mygame-server:0.1 .
As a result, the image is stored locally, and you can check it with the following command:
docker images agonesacr.azurecr.io/mygame-server:0.1

Push the Image to ACR

Now you just want to push the image to ACR. You need to log in to ACR with Docker. Let’s use Azure’s CLI to get the credentials for the docker login.
az acr credential show --name agonesacr
docker login agonesacr.azurecr.io
If the login was successful, you can push the game server image to ACR:
docker push agonesacr.azurecr.io/mygame-server:0.1

Now AKS will be able to pull the image from ACR as soon as Agones requests it.

Agones Configuration

You have configure in the Terraform script that you have public IPs reachable for the clients on the nodes. Agones will create the game servers on demand and assign a unique port to them. This IP:port pair will tell the clients exactly which server they should use for their session. See Figure 3-5.

An illustration of player 1 and player 2 with game clients. a block show node with azure services.

Figure 3-5

Access of game servers in Agones

Agones works with custom resource definitions. There are built-in resources in Kubernetes, such as pods, services, deployments, and so on. Agones defines new ones, for example, the gameserver resource. Let’s create one game server. Create a yaml file (called gameserver.yaml) and add the following content to it:
apiVersion: "agones.dev/v1"
kind: GameServer
metadata:
  generateName: "mygame-server-"
spec:
  ports:
  - name: default
    portPolicy: Dynamic
    containerPort: 7777
  template:
    spec:
      containers:
      - name: mygame-server
        image: agonesacr.azurecr.io/mygame-server:0.1
        resources:
          requests:
            memory: "128Mi"
            cpu: "128m"
          limits:
            memory: "128Mi"
            cpu: "128m"

In this file, you describe the parameters of a GameServer, such as its name, the container’s port, the image used, the requested CPU, and the container memory. It is important to give it enough resources or the pod will simply crash and never run.

Now create this resource on the AKS cluster:
kubectl create -f gameserver.yaml
With the following command, you can check if the game server is in the ready state:
kubectl get gs
NAME                  STATE       ADDRESS         PORT   NODE                              AGE
mygame-server-rfb6j   Scheduled   40.118.53.172   7833   aks-default-29929622-vmss000000   3m42s
You can also check if the pods are running:
kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
mygame-server-8hdln   2/2     Running   0          36s

You can test if your game client can connect to the address and port. To do so, you have to temporarily disable the Azure game object and enable NetworkManager. Use a NetworkManagerHUD component to provide the server IP and add the port number to the KcpTransport in the inspector.

Note that the IP is the public IP address of the node, and the port is generated and assigned to the game server by Agones.

Create a Fleet

Having one game server is not enough, because then you can only start one game session. You need a fleet of servers, and Agones helps with this. Fleet is a custom resource in Kubernetes implemented by Agones, which generates multiple servers according to your definitions.

Create a file (called fleet.yaml) and fill it with the following content:
apiVersion: "agones.dev/v1"
kind: Fleet
metadata:
  name: mygame-server
spec:
  replicas: 5
  template:
    spec:
      ports:
      - name: default
        containerPort: 7777
      template:
        spec:
          containers:
          - name: mygame-server
            image: agonesacr.azurecr.io/mygame-server:0.1
            resources:
              requests:
                memory: "128Mi"
                cpu: "128m"
              limits:
                memory: "128Mi"
                cpu: "128m"

It is similar to the GameServer resource. The only difference is the replicas parameter, which will determine the number of game servers generated automatically. If you remove a game server, the fleet will generate a new one. Practically, if players exit a session, the game server goes into shutdown state, and it will be removed from Kubernetes.

Create the fleet resource and check on the number of game servers:
kubectl create -f fleet.yaml
kubectl get gs
Tip
If you want to start over again without any game servers deployed, use the following command to remove all the game servers and their pods:
kubectl delete gs,fleet,pod --all

Fleet Autoscaler

The fleet autoscaler allows you to create and remove game servers dynamically according to the current demand. Also, you don’t want to reserve CPU and memory in advance.

Create a new file (called fleetautoscaler.yaml) and fill it with the following content:
apiVersion: "autoscaling.agones.dev/v1"
kind: FleetAutoscaler
metadata:
  name: mygame-server-autoscaler
spec:
  fleetName: mygame-server
  policy:
    type: Buffer
    buffer:
      bufferSize: 2
      minReplicas: 2
      maxReplicas: 10

The fleetName must match the name of the earlier defined fleet. Using min and maxReplicas, you can set the limits of scaling. The bufferSize shows the number of game servers that are always ready. Note that the replicas setting of the fleet resource will be overwritten by the fleet autoscaler. In this example, by default there will be only two game servers ready. If one becomes allocated, a new one will be added as ready. That way, there are always two servers in the ready state.

Game Server Allocation

The Game Server Allocation chooses a ready server and provides its IP and port to the calling client. Create a gameserverallocator.yaml file:
apiVersion: "allocation.agones.dev/v1"
kind: GameServerAllocation
spec:
  required:
    matchLabels:
      agones.dev/fleet: mygame-server
Execute this yaml from the command line:
kubectl create -f gameserverallocation.yaml -o yaml
The output should contain the allocated server’s IP and port. You can check that one of the servers is allocated, and the fleet autoscaler keeps two servers ready for new allocations:
kubectl get gs
NAME                        STATE       ADDRESS            PORT   NODE                              AGE
mygame-server-r9ghp-7chqq   Ready       40.118.53.172      7094   aks-default-29929622-vmss000000   115s
mygame-server-r9ghp-8dfs4   Ready       40.118.53.172      7368   aks-default-29929622-vmss000000   49s
mygame-server-r9ghp-fn9jn   Allocated   40.118.53.172      7701   aks-default-29929622-vmss000000   115s

Extending the Control Panel

Now you can allow players to join these servers. You need a simple way to manually provide the server IP and port to the client. For that, you have to extend the control panel with the following code:
using Mirror;
using kcp2k;
public class ControlPanel : MonoBehaviour
{
    public const int AZURE_STARTCLIENT = 4;
    string serverIP = "";
    string serverPort = "";
    GameObject azureClient;
    private void Start()
    {
        azureClient = GameObject.Find("AzureClient");
    }
    void OnGUI()
    {
        if (selection == AZURE_STARTCLIENT)
            GUILayout.Window(0, new Rect(0, 0, 300, 0), StartAzureClient, "Start Azure Client");
    }
    void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("Start Azure Client"))
            selection = AZURE_STARTCLIENT;
    }
    void StartAzureClient(int windowID)
    {
        GUILayout.Label("Server IP:");
        serverIP = GUILayout.TextField(serverIP, 20);
        GUILayout.Label("Server Port:");
        serverPort = GUILayout.TextField(serverPort, 10);
        if (GUILayout.Button("Start Azure Client"))
        {
            azureClient.GetComponent<NetworkManager>().networkAddress = serverIP;
            azureClient.GetComponent<KcpTransport>().Port = (ushort)int.Parse(serverPort);
            azureClient.GetComponent<NetworkManager>().StartClient();
        }
        if (GUILayout.Button("Cancel"))
            selection = ROOTMENU;
    }
}

Copy any running server’s IP and port into the text fields of the control panel, click Start Azure Client, and verify that the client can connect to the server and the character appears on the screen.

The next chapter extends this with an automatic matchmaker, so that players will be automatically connected to ready servers without having to type in the server IP and port.

Summary

In this chapter, you implemented the PlayFab and Azure dedicated servers, which allow online multiplayer gaming from any distance between players. You learned that you need much more effort to implement it in Azure. On the other hand, you have endless possibilities to customize the solution to your needs. But you may think that PlayFab costs much more. Let’s do a quick comparison.

PlayFab provides a generous 750 hours (for one core) free evaluation time. If you compare the price after the evaluation period, it aligns closely to Azure VM costs. Table 3-1 compares the cost of the PlayFab and Azure virtual machines.
Table 3-1

Prices of PlayFab and Azure Virtual Machines

Instance

vCPU

RAM

Storage

PlayFab

Azure

Difference

D2asv4

2

8 GiB

16 GiB

$0.208/hour

$0.199/hour

$0.009/hour

D4asv4

4

16 GiB

32 GiB

$0.4159/hour

$0.398/hour

$0.0178/hour

D8asv4

8

32 GiB

64 GiB

$0.8318/hour

$0.796/hour

$0.0358/hour

D16asv4

16

64 GiB

128 GiB

$1.6636/hour

$1.592/hour

$0.0716/hour

As you can see, there is almost no difference between the price of the PlayFab and Azure pay-as-you-go instances. However, the difference becomes considerable if you are ready to buy reserved instances on Azure, as shown in Table 3-2.
Table 3-2

Prices of PlayFab and Reserved Virtual Machines in Azure

Instance

PlayFab

Azure 1Y

Azure 3Y

Difference

D2asv4

$0.208/hour

$0.1549/hour

$0.1323/hour

$0.0757/hour

D4asv4

$0.4159/hour

$0.3098/hour

$0.2645/hour

$0.1514/hour

D8asv4

$0.8318/hour

$0.6198/hour

$0.5290/hour

$0.3028/hour

D16asv4

$1.6636/hour

$1.2394/hour

$1.0579/hour

$0.6057/hour

If you decide to host your server purely on Azure VMs, you will pay around the same price. But if you are ready to reserve VMs for up to three years, you can save 30-40 percent, compared to PlayFab hosting. Be aware that on Azure you will have additional costs (such as load balancers, firewalls, and monitoring), while in PlayFab you only have to pay for the VMs.

Review Questions

  1. 1.

    Why do you need dedicated game servers hosted in the cloud?

     
  2. 2.

    What are the possible ways to host game servers? How do they compare?

     
  3. 3.

    What is a build in PlayFab?

     
  4. 4.

    What are the options to troubleshoot issues with the PlayFab server?

     
  5. 5.

    What are the problems with a single virtual machine when implementing server hosting for multiplayer games?

     
  6. 6.

    Which features of Kubernetes and Agones support multiplayer games?

     
  7. 7.

    What is the role of containerization when using Kubernetes?

     
  8. 8.

    How do you access game servers in Agones?

     
  9. 9.

    What is the purpose of fleets in Agones?

     
  10. 10.

    Compare the realization of multiplayer game servers regarding cost and complexity.

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset