What is this project?

This project is something I’ve been iterested in for a long time, from way back when I used to play Hypixel Skywars all the time, and I was really curious how the system to send players worked. Recently, I’ve been learning a lot about Kafka and other message queues, and this has given me some inspiration for how it would be nice to handle moving players. This is the first part in a series of posts based on making a minigame server. The next post will be a slight break from complicated subjects: a One In the Quiver minigame.

What exactly is the problem?

We’ll look at this from the viewpoint of Hypixel. They have a very large playerbase and a lot of servers to move people between, so it’s not as viable to directly list the available servers (how many old minigame servers used to do it). We want to minimize the waiting time of players, while also managing complexity of the system. To start, I’ll look at how to set this up for a gamemode that doesn’t have elo. Its also assumed that players will be playing solo, but we’ll tackle both of these before we’re done.

Okay, so we have an idea of what the problem is, now what?

First, lets get an idea of how this works from the player’s point of view, which will simplify things quite a bit.

Before a player can try to join a minigame, they need to join a lobby. In order to do this, they’ll typically join a server at the domiain like mc.reeve.dev. From the player’s point of view, they’re taken directly into a lobby, and can then start to request to join a minigame. How they request to join doesn’t really matter, only that they pick a certain gamemode and/or map. They will then be forwarded to the game server, where they will typically be put in a waiting period for other players.

Next, lets break down what is happening and what is already made for us (our assumptions).

When a player joins via mc.reeve.dev they connect to a reverse proxy. This proxy acts as the player manager, and will send people to where they need to be. This reverse proxy (whether it is several proxies or one) will typically know about connected lobbies and game servers (through either configuration or service discovery). From this list, it will decide on a lobby that has space, and send the player to one of those. Typically, it’s just one proxy, but with large servers like Hypixel it becomes necessary to create a custom solution that allows for horizontal scaling of the proxies. As of my knowledge, there aren’t any public ones available that provide this out of the box.

A draw.io diagram of the joining process
A draw.io diagram of the joining process

Now that they’re in the lobby, they’re free to walk around and interact with the world they’re in. Up to this point, everything is made for us and is not in the scope of this project. They can request to join the a game, and for the sake of this project that will be via a command.

From here, a server might already be running or there may be a service to start one, but we’ll just assume there are already servers running at this point.

A simple draw.io diagram of the theoretical network topology
A simple draw.io diagram of the theoretical network topology

Solution Drafting

Now that we have a pretty good idea of what the problem is, we can start to brainstorm and draft the solution. The easiest solution could go something like this:

We create a running list of all our minigame servers we have available, as well as information about their current status (loading, waiting for players, in-game, ending, closing) and player count. Once we have this, we can just filter for the games that are currently waiting for players, and have them self-report. In game, we can then just list off the servers available to join and let the players choose one. There are a couple problems with this set up:

  • There is no queue system, which means that scaling the minigame servers will either not happen or will only happen when there are none available or there are some just sitting around empty for a certain period of time.
  • Games might sit in the waiting for players state for a while, since players could be evenly distributed among the available servers (for example, 15 players required to start and 30 players split in 3 lobbies, which could be 2 full lobbies)
  • Players will have to actively look for a server
  • We’d need a different way of handling things if we start adding competitive gamemodes, and we’ll have to rework or completely change it to prevent abuse of the lobby picking system
  • You can limit the amount of options you give a player or add a button for a random server, but the option of choice can be more stress, time, and clicks in order for a player to play
  • Updating this interface can also be tough, since if there’s a large playerbase, player counts will be changing quickly and they might be full between the time you load the info and click to join. If the interface is updated dynamically, there might be a lot of moving numbers and open/closing servers.
A simple draw.io diagram of the first solution
A simple draw.io diagram of the first solution

A slight improvement to this might be keeping a running list of the players in a queue, and then servers can pull from that queue when they get into the waiting state. This has the bonus of being able to count all the players looking to play at once, and knowing exactly how many minigame servers are necessary for all the players to join a game. This also has some problems though:

  • Depending on the proxy, you’ll need to send a message from the minigame server claiming a player to the proxy in order for it to move them to the server. This can be complicated, especially if there’s multiple proxies running.
  • Not only does this add complexity, it adds more responsibilities to the game server that isn’t technically something it should be dealing with.

A slight change to that will make it a little easier to work with in some regards, and harder in others. Instead of the servers doing the work with the queue, we can delegate this to the proxy, and create a second queue for the minigame servers. The proxy will now be the only one to determine where players go, reducing the responsibility of the minigame servers so they can worry only about running the game. This increases the responsibility of the proxy, but it is already handling the players, so handling the queues as well makes more sense there. The problems this creates are easier to work with:

  • The proxy now needs to make sure the players get where they need to be, so that it doesn’t send more players while some are connecting.
  • The proxy will have to watch for player leave and move to lobby events, to keep track of the player counts in each of the game servers.

I think these pitfalls are reasonable for now, and maybe we’ll rework our approach as we figure out more. These keep the general responsibilities of the proxy (managing players), and adds a more specific responsibility of matchmaking players.

Teams

Now, this is a little tricky. Teams complicate this because if we are sending players together, we need to tell the game they’re sent to that they should be together.

One possibility I’m considering is using the messaging system that is part of Velocity (the proxy I’ll be using): https://docs.papermc.io/velocity/dev/plugin-messaging The other possibility I’m considering is creating a more fleshed out teams service, where a Redis database will keep track of teams. Since player information will usually be loaded when a player joins the server, it might make sense to do that lookup when players join. If we’re already doing this to load any cosmetics or upgrades a player may have, it will be easy to load the teams as well.

But how do players get in teams in the first place? Do they need to be in the same lobby?

First question: commands, for now. The simplest way for players to get into a team would be walking up to each other in a lobby and waving at each other or something, but that’d take a lot on our end. So the simplest way to get this done (since this is not the focus of this part of the project) is commands! When players are in-game, they can use a text chat to talk to one another. Any message prefixed with / at the beginning is considered a command. These can be simple, or really complicated to add. For now, we’re going to take the simple approach and create commands like so:

  • /teamcreate
  • /teaminvite <player>
  • /teamaccept <player>
  • /teamignore <player>
  • /teamleave

A more complicated way of doing this would be like so:

  • /team
    • /team create
    • /team invite <player>
    • /team accept <player>
    • /team ignore <player>
    • /team leave

This would be one command, but then we’d have several options a player can use from there. That code can get ugly quick, so that will be something to take the time to think and work through fully.

For now, we’ll go with the ones without arguments. But what happens when someone runs these commands? Typically, commands are created at the server level, but we want all our servers to have this feature, as well as the ability to invite players from other servers.

Now that we’ve built up some responsibilities, lets make things clear about who needs to do what. We’ve said that the proxy will serve as the player manager, so what exactly does that entail? Technically just about everything is “dealing with players”, some just have different work in order to make that happen.

Proxy responsibilities:

  • Manage player connections (built-in)
  • Consume server status messages, so we know who is accepting players
  • Consume player & team queue messages, so we know who wants to play
  • Send players and teams to lobbies
  • Create teams & store them in the Redis
  • Keep a running count of servers in play, as well as their total players

Lobby responsibilities:

  • Act as a buffer for players not in game
  • List current gamemodes & their player counts
  • Produce player & team queue messages, based on what gamemode they interacted with

Game responsibilities:

  • Produce status messages, so the proxy knows who’s ready and who isn’t
  • Run a game until someone wins or the timer runs out
  • Produce player stat messages (kills, deaths, etc. so we can keep count)

??? responsibilities

  • Consume player stat messages (put this information in a long term store, or use Kafka Tables)