Let's make a server to store all those replays!

I wanted the extra challenge and an excuse to learn Rust. This means we need to actually make a server to store this on!

After making a way to create and play back game replays in the last post, I needed somewhere to store them all! Since I wanted the replays to be easily accessible and always available, it made sense to integrate them into a leaderboard, and store the data on a server.

Choosing to store them on a central server rather than local networking, or even worse storing them locally and avoiding networking altogether, means our game will be more replayable as it adds an element of global competition and community, meaning everyone can always learn from others' techniques.

One option I considered was using an existing online services package, such as Microsoft's PlayFab. Using that would guarantee more stability and better uptime, as well as taking the workload of coding the backend off of me, however I wanted the extra challenge and an excuse to learn Rust. This means we need to actually make a server to store this on!

Picking Rust

Okay I know what you're thinking: Why on earth would they not keep things simple and use Python like last time?

Hear me out, there are a couple reasons I decided not to use Python or any other scripting language for this.

  • Performance
    Compiled code will perform better than interpreted, reducing latency on requests, of which there may be many, especially in scenarios with multiple concurrent players
  • Fewer dependencies
    A compiled binary is easier to distribute to a server, not requiring me to install additional packages on the target, and reducing the workload of setting an appropriate environment for the server to run
  • Learning
    This gives me an opportunity to learn a completely new-to-me language, and gain more experience in compiled languages, which will be useful when working on performance sensitive applications in the future

Now that I'd decided on using a compiled language over scripting, I needed to pick which one. I considered using C++ for this, but Rust drew my attention with it's Crates and cargo build system, which felt similar to Python's libraries. This was appealing to me because I felt like I'd be able to work quicker with more libraries avaliable to me, reducing the code complexity greatly especially for dealing with networking which was welcome.

There are two main web frameworks for Rust: Rocket.rs and Actix.rs. I went with Actix since it was the first one I stumbled upon, benchmarks well for speed, and seemed relatively easy to use.

Planning our endpoints

  • PUT /update-account
    We'll use this endpoint for users to register accounts, and update their usernames
  • PUT /submit-ghost
    We need to make this endpoint authorised so it's not possible to upload a ghost on someone else's behalf
  • GET /get-ghost/{id}
    This is basically just serving a flat file, no authentication needed because we don't have the option to make a replay private
  • GET /leaderboad/{page}/{length}
    Make sure not to return raw user data as that would contain secrets, so only return usernames and IDs with each entry

My original plan was to support an OAuth2 provider to offload management of game accounts to an external body, but this proved to be out of scope and unnecessary, since almost everyone would only play the game on one device. I believe this was partially because we would not be providing a WebGL build of the game, and partially because for the average player, our game was not going to provide much content with only one level.

Based on this I decided it would be appropriate to create accounts tied to devices (stored in the Unity provided PlayerPrefs). I used a randomly generated number to identify accounts, and additionally stored a random Guid that I would use as the account "secret", an alternative to a user created password that allows for more complexity compared to the average password.

What our data looks like

File structure

$ tree
.
├── ghosts
│├── 00ec8c4d-7847-4c93-b237-bbeee8e6a525
│├── 02249e5b-9e2a-4c20-a035-ee91540e9c67
│├── 066698d1-cdc5-4508-a666-7676a9739a77
│├── 1a7b63a5-7b8a-4016-b2e9-1b233174c7bc
│└── ...
├── leaderboard.json
└── players.json

Account data

$ cat players.json
{
  "players": [
    {
      "id": 787982380,
      "secret": "34d39bec-a890-406a-b459-5405d35ad5a4",
      "username": "avery"
    },
    {
      "id": 866580648,
      "secret": "9b7580ef-4e2a-47cf-aca5-11690598c4f0",
      "username": "jevin"
    },
    {
      "id": 1864751761,
      "secret": "1f7f1e8c-0b4d-46d6-b613-75699e257398",
      "username": "Ali"
    },
    {
      "id": 1814005773,
      "secret": "0db898b3-aff0-4bfc-bc54-c1889ce7a2d2",
      "username": "emmy:3"
    },
    {
      ...
    }
  ]
}

Leaderboard data

$ cat leaderboard.json
[
  {
    "playerId": 866580648,
    "levelId": 3,
    "ghostId": "00ec8c4d-7847-4c93-b237-bbeee8e6a525",
    "time": 43.72
  },
  {
    "playerId": 1814005773,
    "levelId": 3,
    "ghostId": "02249e5b-9e2a-4c20-a035-ee91540e9c67",
    "time": 45.06
  },
  {
    "playerId": 1864751761,
    "levelId": 3,
    "ghostId": "df8fcace-dd42-4cdd-939d-ee4005f6dbb1",
    "time": 46.2
  },
  {
    "playerId": 787982380,
    "levelId": 3,
    "ghostId": "1a7b63a5-7b8a-4016-b2e9-1b233174c7bc",
    "time": 47.34
  },
  {
    ...
  }
]