Architecting a multiplayer game server in Rails 5

January 22, 2016

Let's just say you wanted to build a realtime multiplayer authoritative server in Rails 5 with ActionCable and WebSockets...

What? Why would you do that?

Nevermind the why for now, let's just think about the architecture here...

but there are other systems out there that are much better suited for...

I know, I know, shhhh. Let's just have some fun here.

Now I'm talking about a multipler authoritative server. The game's logic is run on the server (in a separate Rails process) and streamed to clients vis WebSockets. ActionCable provides a turnkey solution for WebSockets and the client connections, so that's taken care of. But what about an engine that's running on the server performing all your game logic?

Having used Sidekiq extensively in my work, this provides a template for essentially what we want: a companion process that loads your entire rails app (e.g. has access to all your models, application logic, and rails convenience). This process can be your game engine running your loop at X frames per second, processing your game state.

The key here is that this game loop process also needs to be a subscriber and publisher to ActionCable messages. You could do this over WebSockets like any other client, but perhaps we should just have the game pubsub on the backing datastore. By default ActionCable uses Redis through the em-hiredis library so bypassing WebSockets and connecting to Redis pubsub is pretty straight forward:

@thread = Thread.new{
  EM.run do
    EM.error_handler{ |e|
      puts "Error raised during event loop: #{e.message}"
    }
    ActionCable.server.pubsub.subscribe("my_channel", ->(msg) {
      # callback for when a message is received on this channel
    }, ->(arg) {
      # callback when we've successfully subscribed to the channel.
    })
  end
}

So, lets say you've mounted your game engine somewhere in rails and it continuously runs a game loop, you can simply start a new process like:

  bundle exec rails runner "GameLoop.start"

That basically provides the foundation of our system:

  • ActionCable handles client connections and streaming
  • A rails-enabled separate process provides our game loop
  • the game loop has 2-way communication with our clients by listening to Redis pubsub

Putting performance and other well established solutions aside, it's an interesting thought experiment. And doing this all within the Rails stack has its advantages if your game:

  • shares logic/models/data structure between a traditional website and your game
  • maps well to the ActiveRecord ORM
  • has multi-platform clients (Unity3D, javascript) of which one can only communicate over WebSockets