How to create an IO Game

Posted SEPT 5, 2017


You may have heard of "IO games". Launched in 2015, started the trend. Multiplayer, accessible with just a browser, easy to play but challenging, theses games became extremely successfull in an instant. In this post, we will create an clone, using C++ on the backend and obviously JavaScript on the frontend. Before starting, you'll find the final result just below:

The Game

This article is split in two parts, the server-side and the client-side.

The Server-Side

When building a "IO game", you'll have several constraints. First, your server code will have to communicate using WebSockets (as it is the simplest way to have a bi-directonial communication channel on the browser). An other one is that you may have a lot of clients connected at the same time (if you are successful!). Using C++, and the uWebSockets library, we will have a server able to handle thousands of WebSocket clients with no sweat. Our server-side code is about 200 lines of code only! Let's dive into it:

These first lines of code are, as you can guess, the declaration of our global variables. Our world is 1920x1080, we have 100 food cells, and we broadcast the state of the game every 40ms.

const int MAX_CONNECTIONS = 200;
const int FOOD_MAX = 100;
const int X_MAX = 1920;
const int Y_MAX = 1080;
const int REPEAT_MS = 40;
const double START_SIZE = 10.;

The second snippet is the Vector struct, with only the necessary operations. It will allow us to store the position, the target position of a player, and perform the computations needed to move our players.

struct Vector {
  double x;
  double y;

  Vector() : x(0.), y(0.) {}
  Vector(double x, double y) : x(x), y(y) { }
  Vector(const Vector& v) : x(v.x), y(v.y) { }
  static Vector Random() { return Vector(std::rand() % X_MAX, std::rand() % Y_MAX); }

  double norm() { return std::hypo(x, y); }

  Vector& operator+=(const Vector& v) { x += v.x; y += v.y; return *this; }

Vector operator*(const Vector& v, double f) { return Vector(v.x * f, v.y * f); }
Vector operator/(const Vector& v, double f) { return Vector(v.x / f, v.y / f); }
Vector operator-(const Vector& l, const Vector& r) { return Vector(l.x - r.x, l.y - r.y); }

This is the most interesting part. Now, with our Vector struct, we can set up the player state. A player has an id, a size, a position, and a target position. I also added the dead and spectating attributes. We then have the methods used to update the position of the player, and to check if it is eating a food cell or an other player.

struct State {
  int id;
  double s; // size
  Vector p; // position
  Vector t; // target of the client
  bool dead = false;
  bool spectating = false;

  State(int id, const Vector &p) : id(id), p(p), t(p), s(START_SIZE) {};

  void update(int dt) {
    Vector pt = (t - p); // target - position vector
    double pt_norm = pt.norm(); 

    double l = (dt / 5.) / (std::sqrt(s / START_SIZE)); // the speed of the player
    if (l < pt_norm) {
      p += (pt * l) / pt_norm;
    } else {
      p = t; // if next position is beyond the target, just set it to the target

    // Limit the position and size
    p.x = std::max(p.x, 0.);
    p.x = std::min(p.x, (double) X_MAX);
    p.y = std::max(p.y, 0.);
    p.y = std::min(p.y, (double) Y_MAX);
    s = std::min(s, (double) (X_MAX / 4));

  void reset() { p = Vector::Random(); s = START_SIZE; t = p; }
  void set_target(int x, int y) { this->t.x = x; this->t.y = y; }

  // food eating
  void eat() { s = std::sqrt(std::pow(s, 2) + 5); }
  bool is_eating(const Vector &v) { return (p - v).norm() < s; }

  // player eating  
  void eat(const State &st) { s = std::sqrt(std::pow(s, 2) + std::pow(st->s, 2)); }
  bool is_eating(const State &st) { return (p - st.p).norm() < (s - st.s) && s > st.s; }

  bool is_playing() { return !dead && !spectating; }

Let's start to code our WebSocket server. First we will handle the events coming from the client, using our uWebSockets library. We just need three event handlers (onConnection, onMessage, onDisconnection). In order to send data to our client, we will use the nlohmann::json to serialize our data in CBOR, a JSON like serialization but in binary (to save some bytes!)

uWS::Hub hub;
int next_id = 0;
int connections_count = 0;

hub.onConnection([&next_id, &connections](uWS::WebSocket<uWS::SERVER> *ws, uWS::HttpRequest req) {
  int id = next_id++;

  State* state = new State(id, Vector::Random()); //new state with random position
  ws->setUserData(state); // attach the data to the socket
  if(connections > MAX_CONNECTIONS) { state->spectating = true; return; }; // spectating if too many players

  // we send the player id to the client
  json j = {{"id", id}};
  std::vector<std::uint8_t> jcbor = json::to_cbor(j);
  ws->send(reinterpret_cast<char*>(, jcbor.size(), uWS::BINARY);

hub.onMessage([](uWS::WebSocket<uWS::SERVER> *ws, char *message, size_t length, uWS::OpCode opCode) {
  State *state = (State *) ws->getUserData();
  if (!state->is_playing()) { return; }

  // parsing the target position of the client
  try {
    std::string message_s(message, length);
    json message_j = json::parse(message_s);

    if (message_j["x"].is_number_integer() && message_j["y"].is_number_integer()) {
      state->set_target((int) message_j["x"], (int) message_j["y"]);
  } catch(std::exception) {

hub.onDisconnection([&connections](uWS::WebSocket<uWS::SERVER> *ws, int code, char *message, size_t length) {
  State *state = (State*) ws->getUserData();
  delete state;

Next, we need to update our world. Since our library is fully async, we need to declare a Timer, and attach it to our event loop. This is equivalent to a setInterval in JS.

std::vector foods(FOOD_MAX); // food cells declaration
std::generate(foods.begin(), foods.end(), []() { return Vector::Random(); });

// Data attached to our timer
struct TimerData { uWS::Hub* hub; std::vector<Vector> *foods; int *last_timestamp; };
TimerData timer_data = { &hub, &foods, new int };

Timer *timer = new Timer(hub.getLoop());
timer->setData((void*) &timer_data);

timer->start([](Timer *timer) {
  TimerData *timer_data = (TimerData*)timer->getData();
  uWS::Hub *hub = timer_data->hub;
  std::vector<Vector> *foods = timer_data->foods;

  // Compute delta time in ms
  int timestamp = milliseconds_since_epoch();
  int *last_timestamp = timer_data->last_timestamp;
  int delta_timestamp = last_timestamp ? timestamp - (*last_timestamp) : 0;
  *last_timestamp = milliseconds_since_epoch();

  // Iterate over clients, and update them  
  hub->getDefaultGroup<uWS::SERVER>().forEach([hub, delta_timestamp, foods](uWS::WebSocket<uWS::SERVER> *ws) {
    State *state = (State *) ws->getUserData();
    if (!state->is_playing()) { return; }


    // Check if food is being eaten
    for (auto food_it = foods->begin(); food_it != foods->end(); ++food_it) {
      if (state->is_eating(*food_it)) {
        (*food_it) = Vector::Random();

    // Check if other player is being eaten  
    hub->getDefaultGroup<uWS::SERVER>().forEach([state](uWS::WebSocket<uWS::SERVER> *ws_opponent) {
      State *state_opponent = (State *) ws_opponent->getUserData();
      if (!state_opponent->is_playing()) { return; }

      if (state->is_eating(*state_opponent)) {
        state_opponent->dead = true;

  // Broadcast all the states updated to clients
  std::vector<State*> states;
  hub->getDefaultGroup<uWS::SERVER>().forEach([&states](uWS::WebSocket<uWS::SERVER> *ws) {
    State *state = (State *) ws->getUserData();
    if (!state->is_playing()) {
      state->dead = false;

  json j = {{"timestamp", timestamp}, {"states", states}, {"foods", *foods}};
  std::vector<std::uint8_t> jcbor = json::to_cbor(j);
  hub->getDefaultGroup<uWS::SERVER>().broadcast(reinterpret_cast<char*>(, jcbor.size(), uWS::BINARY);
}, 0, REPEAT_MS);

And here you go! We now have a server for our "IO game". There are a lot of improvements to be made. First we need to improve the communication between the server and the client. For instance, we don't need to send constantly the food cells over the network. We could also develop a custom-made binary protocol to save some bytes on the network.

As always, you can find the full code here. That's it for the first part of this blog entry! I'll do an other one to explain the client-side as soon as I can.