gRPC

Lunaria is a video game for programmers, in which players write code to interact with the game through an API. This post introduces Lunaria's API that is built using the gRPC framework.

gRPC

Lunaria is a video game for programmers. It is played by writing code that sends commands to the game, and watching the effects of those commands on the game world. In this post, I will introduce the API between the players' code and the game, and explain why I chose to use the gRPC framework.

Introduction

On a high level, Lunaria consists of two components: a simulation running the game, and an API enabling players to interact with the simulation. Because players can only control the game through the API, it probably has the most significant impact on how much fun it is to play the game.

Lunaria is also supposed to be a sandbox for beginner and expert programmers alike, and a place where they can practice their skills. To do this, the game must be compatible with as many programming languages and paradigms as reasonably possible, so that developers can freely choose what to learn. Since the API is again the most important factor determining compatibility with the game, its design matters even more.

gRPC

After much research and consideration, I made the decision to adopt the gRPC framework to build Lunaria's API. gRPC is described as a high-performance, open source universal RPC framework, and it has some interesting features.

API Specification

As the primary touchpoint between players and the game, the API has to be well documented. With gRPC, the endpoints of the API and the messages it consumes and produces are defined as Protocol Buffers, a language-neutral standard to serialize data. Protocol Buffers are strongly typed and fairly easy to read, making them ideal candidates for API documentation.

The following example defines the API that returns the current version of the game. It defines a Version type with three field, an RPC endpoint GetVersion, and two types for the request and response.

syntax = "proto3";

package lunaria.v1;

message Version {
  int64 major = 1;
  int64 minor = 2;
  int64 patch = 3;
}

message GetVersionRequest {}
message GetVersionResponse {
  Version version = 1;
}

service Lunaria {
  rpc GetVersion(GetVersionRequest) returns (GetVersionResponse);
}

Multi-platform

gRPC supports many programming languages and ecosystems out-of-the-box. It has official support for Node, Python, Go, Ruby, Java and more, and other languages are supported by their communities.

Auto-generated libraries

While gRPC is built on open standards and can theoretically be used with any programming language, official or community supported languages provide a lot of tooling that makes it very easy to use gRPC. Most notably, client libraries for many languages can be generated from an API's Protocol Buffer definition. For Lunaria, a Node and a Rust library are already being published as a proof of concept.

Take the Node.js library for Lunaria (lunaria-api) as an example. The library can easily be installed from npm, and requesting the version is as simple as calling a method on the client:

// Asynchronously send the request and process the response in a callback
lunaria.getVersion(request, (err, response: GetVersionResponse) => {
  const version = response.getVersion();
 
  if (version != undefined) {
    const versionString = [
      version.getMajor(),
      version.getMinor(),
      version.getPatch(),
    ].join(".");
 
    console.log(`Lunaria is running version ${versionString}`);
  }
});

Streaming and Authentication

Two other builtin features of gRPC are streaming and authentication. Streaming might be interesting for certain game mechanics, and authentication will come in handy for multiplayer. Even though there are no concrete plans for these features yet, it is still nice to know that they are there when we need them.

Alternatives

Before deciding on gRPC, I did a lot of research and considered the following alternatives. Each has its pros and cons, but none checked as many boxes as gRPC.

REST

Probably the most widely supported API would have been an REST API. Every language on the planet has a way to query an HTTP endpoint (don't @ me), and especially web developers are very familiar with it. There are also decent ways to document these APIs, and some tools to auto-generate client libraries.

In my experience, though, it requires a lot of effort to keep the documentation up-to-date, and the generation of client libraries is often fragile. But maybe more importantly, it is not a very fun API to use. The industry is moving to other solutions such as RPC or GraphQL, which are either more performant or offer more flexibility to clients.

GraphQL

Another option I considered was GraphQL. It is widely supported, easy to document through its schema, and has excellent tooling such as the Playground to explore the API.

There are not many arguments against GraphQL, and most will probably resolve themselves in the future. Streaming and subscriptions are still fairly new, as is the Rust implementation of GraphQL.

Maybe my biggest issue with GraphQL is that it feels very focused on web technologies, while Lunaria is more of a distributed system. This probably wouldn't be a problem, but gRPC felt like the better fit.

Message queues

A third solution I considered where message queues, because it is a very simple solution that other video games have already implemented. In Screeps, for example, players queue up commands for their units that get executed in the next turn.

But compared with the alternatives, message queues are very hard to use. It's difficult to document their message formats and ensure that the documentation stays up-to-date, and it is not possible to generate client libraries that would hide some of the complexity. And since GraphQL and gRPC support streaming, the same pattern can be implemented in a much nicer environment.

Conclusion

gRPC feels like a great choice for Lunaria. It provides a good experience to players, is easy to document and use, and is blazingly fast. The playlunaria/lunaria-api repository contains the specification for Lunaria's API, tooling that publishes client libraries to npm and crates.io, and automated integration tests for each language.

gRPC is of course no silver bullet, and I can already see a few challenges ahead. At least in its initial form, Lunaria will expose a single service, and I am not sure if that's how gRPC is intended to be used. And it might turn out that players would actually feel more comfortable with GraphQL than gRPC. But we'll learn this as we go.

With the API design out of the way, it is now time to focus on the actual game...