I’m doing some experimental work to figure out whether the netplay design I have in mind would be feasible using ZeroMQ. I’m working in Ruby at the moment because it’s the most natural language for me to work in. The ZMQ guide has Ruby examples using ffi-rzmq, which maps pretty closely to the underlying API. Whatever design I prototype in Ruby should work equally well in another language.
I’m thinking about the networking requirements in tandem with my concurrency requirements, which is something ZMQ kind of encourages you to do. You’re encouraged to rely on using ZMQ in-process communication instead of mutexes, condition variables, etc. to coordinate between your threads. This is worth trying out, although I don’t necessarily know how well it will play with whatever graphical toolkit I end up using.
So the way I’m envisioning this right now is that each player has an outgoing event queue and an incoming event queue. Messages can be received and delivered asynchronously. Successful delivery counts on TCP’s retry mechanisms, and there’s no acks in the protocol. Each of these queues lives in its own thread. An additional thread is responsible for running the simulation. It reacts to events fed into it from the incoming event queue and emits events for the outgoing event queue. It also listens for input from the UI thread (i.e., whatever the user is doing). Finally, it ticks the game logic around 60 times per second so that objects can move, AI can operate, etc.
What you end up having is the simulation thread using input from the current player, events from the other player, and the current state of the arena to 1) set up the next state of the arena so it can be displayed and 2) emit events for the other player’s arena.
Setting up a minimal version of this design in Ruby was easy. In essence, the app works like a chat system — you type in text, hit enter, and the message is relayed to the local simulation thread. The simulation thread adds identifying information and its current tick to the message (so you can see it’s working) and passes it on to the output queue. The other instance of the app receives the message in its input queue, passes it into the simulation thread, and displays it.
Here’s one side of the exchange:
% ruby duplex_msg.rb 5555 5556 right are you there HEARING >> left: 132: are you there? I AM HERE! holy shit dude HEARING >> left: 705: holy shit dude? NO KIDDING RIGHT! cannot believe this works HEARING >> left: 1090: cannot believe this works? ME NEITHER!
And the other side:
% ruby duplex_msg.rb 5556 5555 left HEARING >> right: 100: nothing? are you there! I AM HERE HEARING >> right: 586: I AM HERE? holy shit dude! NO KIDDING RIGHT HEARING >> right: 1003: NO KIDDING RIGHT? cannot believe this works! ME NEITHER
Note the increasing tick number, which goes up in real time. This stands in for the local game logic. The last message sent is incorporated into the reply in order to simulate a change to the game state from the incoming event.
The implementation is pretty simple — 80 lines of Ruby. I’ve created a Gist for it. I’m pretty confident at this point that ZMQ will be useful for my purposes. This design seems to work acceptably for much higher volumes than I expect to require, too: piping ‘yes’ to both ends churns out a great deal of output and eats up a lot of CPU but proceeds diligently without any weird behavior.