Colyseus JS Game Server and Private Rooms, or how I rewrote everything by mistake

Colyseus JS Game Server and Private Rooms, or how I rewrote everything by mistake

ยท

4 min read

I'm sorry. I got ill and had a refactoring syndrome ๐Ÿ˜ซ. It was a serious case of a rewrite. My project is an online persist-universe single/co-op space game.

See, I tried writing my game server from scratch, with all the logic of managing connections a game "server room" with broadcasts and async WebSocket communication with clients. It was then when I discovered that there are many open-source game servers ๐Ÿคฆโ€โ™‚๏ธ๐ŸŽฎ that are available and do a lot of the network work for you.

Being utterly curious, I chose a game server project named Colyseus and decided to try rewriting my existing infrastructure logic with it, to get a feel for it. After reviewing some of the servers in this list from awesomeopensource.com, I went with Colyseus.

Colyseus Rooms

The Colyseus documentation is not perfect at the moment, especially around their "room" concept which was not very clear to me at first. What they call "room name" is actually some sort of a room "class" which you can instantiate instances of. Their default use case seems to be more of a game room matcher with public rooms when you request to list the rooms available with some "room name", but in my case I only want private rooms.

Setting rooms to private makes them unlisted, but I still had to find a way to find an existing game room when a client asks to join their game by a gameName (each game has a randomly generated game "name" or "id"). The code I ended up with (it's TypeScript for me now, with Colyseus being a TypeScript project):

// !! Beware of throw-away code !!
async requestRoomReservation(req: Request, res: Response): Promise<void> {
  try {
    const { gameName } = req.params;
    const game = await getGameByName(gameName);
    if (!game) {
      res.statusCode = 400;
      res.send({ message: `Game '${gameName}' not found!`, gameName });
      return;
    }
    let reservation;
    try {
      const rooms = await matchMaker.query({ name: "spaceGame", gameName });
      const { roomId } = rooms[0];
      reservation = await matchMaker.joinById(roomId, { gameName });
    } catch (error) {
      console.log("NO ROOM! creating");
      const room = await matchMaker.createRoom("spaceGame", {
        gameName,
      });
      reservation = await matchMaker.reserveSeatFor(room, { gameName });
    }
    res.send({ reservation });
  } catch (error) {
    res.statusCode = 500;
    res.send({
      message:
        "Unexpected server error with reserving a seat for your room, sorry",
    });
  }
},

Before the client joins the Colyseus server, they should create a game through an API exposed by Express (which integrates with Colyseus on some hard-coded-by-Colyseus routes ๐Ÿคฎ*). They can also list their personal games via that API. Once they get a gameName, they ask for a Colyseus-thing called a "reservation". I query existing rooms searching for an existing room with the game name, or create one, and then sends the reservation for the Colyseus client to consume.

  • Maybe I can use them under a certain sub-route

A Colyseus room auto-disposes when all clients disconnect, and that's fine by me. The actual game's logic and state is still saved in a database and some worker is consuming jobs and changing game logic. These updates will somehow be conveyed to existing rooms and broadcast to connected clients (Redis PubSub? I over-engineer again?).

Avoiding Over-Engineering

I use Checkvist to jot down my thoughts (a great alternative to WorkFlowy, IMHO), as well as doing some freestyle drawing using my Lenovo Yoga C940's integrated pen. Trying to realize how to connect the workers executing jobs, broadcasting them to clients and other things that make the game logic work, I caught myself up over-engineering things for the time of this project and decided to have all the logic in a single Heroku web worker. KISS as much as possible and get fast to a playable (pre-)MVP as a fellow developer suggested.

For scheduling tasks, such as my precious colony ships production, I intend to use Bree, but that deserves a different post. I will also write about TypeORM and how I use it with Heroku and with myself hopefully soon, but for now, I'll conclude this post and start getting my hands dirty with building colony ships.

ย