Using A Tornado Socket.IO Server For A Multiplayer Game
Posted on Thu 04 February 2021 in Tech • 4 min read
Making A Multiplayer Game During A Game Jam
Last weekend was Global Game Jam 2021, which coordinates a single game jam around the world by having various sites as hosts. Typically this is done in-person at those sites, but this year it was all virtual. The theme was "Lost and Found". I joined a group of friends who had participated before at the UB Shady Grove site, but our group this year was 9 people and the max for a group is 6. We worked this out by starting off the jam in a brainstorming session with all 9 of us, decided on two different game ideas, then chose which idea we wanted to work on.
The game I helped create is a 2-player game called Guinea Dig: Lost In Ground, where you and another player are guinea pigs who are digging through the ground to find each other. One of the main reasons I chose to work on this game was that none of us had made a multiplayer game before and we wanted to give it a try. Also, most of the team knew how to program in Phaser as the front-end, but I don't think I'd be able to contribute to that in a useful way. Instead, I offered to provide the back-end server that would communicate with both players, allowing me to work in Python.
Multiplayer Gameplay Via Websockets
One of the main technical decisions our team had to make was communication between the clients and the server. REST APIs would work for a turn-based asynchronous game, but we wanted it more real-time than that. Websockets seemed to provide a bidirectional communication that worked with Phaser and Python (through the python-socketio package), so we chose that as our client-server bridge.
Developing with the team was surprisingly simple once we had figured out the Websocket implementation. A person working on the front-end in Phaser could say "I'm going to send a signal called 'move' with a string that has the direction of movement", and I could accept that signal on the server and process it in Python. Then, as part of that processing, the server could send it's own signal with the new coordinates for the player. I didn't have to know any Phaser, and the rest of the team didn't have to know any Python. State the signal name and the payload, and that's all you need.
Since we were in a game jam format with limited time, we had to make some quick decisions for the gameplay. It turned out to be easier for the server to always send all information to all players in the game room any time there was an update to that information. With more time, it may be better to only tell certain information so the client wouldn't cheat. We tried to keep any "global" knowledge on the server, with any display-specific knowledge on the client, which I think helped separate duties between the two.
Python-Specific Implementation Details
As I was the only person on the team developing the backend server, I made a few quick decisions based on what worked best at the time. I set up an EC2 server in AWS that had a URL pointing to it with an SSL certificate, so the clients could just point to that URL in the front-end code and I could change out the server or IP address if necessary. I decided to use Tornado as the web server since it's very lightweight with minimal code needed to stand it up. All code was in a single file for rapid development, so I kept some of the game state and server state information in global dictionaries. These dictionaries had the room and user information, which made it easy to always call on those in any function.
With python-socketio, any websocket signal that is received triggers a distinct python function. This worked well for keeping the code clean, so changes to a single function didn't bleed over into other signal handlers. One tricky part about using websockets is there is a bit of a black box when a signal is sent, as you have no indication that it was received on the other side. To help with this, I tried logging every time a signal was received before anything else was done, which helped immensly while debugging to know if it was a problem with getting a signal or with processing the corresponding data.
Something I've implemented in prior game jams is a leaderboard. I think this helps encourage players to replay your game and gives that sense of beating a score. Normally, I'd store those scores in a DynamoDB table, but since I already had a server for this I stored the scores in a local JSON file on the server. Tornado made it easy to serve that JSON file, so the client could pull the JSON and sort the data to display the latest leaderboard.
Lessons Learned
There were many firsts for me in this process, including the first time on a team in a game jam, the first time using websockets, and the first time making a multiplayer game. Here's some of the things I learned:
- More logging on the server in a multiplayer game is almost always better. I know you can go overboard with this, but tracing logs when communicating between multiple clients can get crazy.
- A language-independant protocol in a team format is great, as I could both request and state what signals and data I was expecting and sending.
- There are pros and cons to using a very lightweight package like Tornado. It was very quick to set up and run, but I had to add in quite a bit, such as overriding headers on certain calls that were made. These tweaks can be harder than you think in a short time frame, especially if you don't use that package very often.