Heyo! Been a while since I've written a post on here, but this time, it's not completely due to inactivity. While I've put Bipp, my music game engine, on hold for a bit, I've found a new game to work on that I'm really excited about!
Manygolf is a game inspired by my love of multiplayer time trial games like Trackmania, and, rather obviously, the mobile hit Desert Golfing. In Manygolf, you play procedurally-generated 2D golf courses alongside anyone else currently playing the game.
The initial prototype came together in about a week, and I've spent another few weeks polishing and refining the game. I'm lucky enough to have a a chat room I can easily recruit playtesters from - all I have to do is say “hey, I published an update” and post the link, and I can get a session going with 4-5 other players for 5-10 minutes - so it's been easy to rapidly iterate on the game.
While the game's design mostly speaks for itself and doesn't require a lot of special insight or explanation, I did want to talk a bit about the technical architecture of the game.
Universal JS & Netcode
As soon as I started to write the backend, I realized how crucial this was. Like most online multiplayer games, Manygolf's server is in charge of maintaining the state of the session - in this case, the scores of the players, the current level, and the position and velocity of each ball. This means that when you hit the ball, you send the direction and power of your hit, and the server calculates the physics and sends the new position of the ball to each connected client. However, this takes a significant amount of time, and if you had to wait for the server to tell you the new position of your ball on each frame, it would result in your ball having a very “jittery” movement due to the inherent network lag.
So, instead, the client uses predictive netcode, which is just what it sounds like - it runs the same physics simulation the server does on the client, “predicting” where the ball is going to end up on each frame. This is usually not an easy task.
Imagine, for example, a first-person shooter game, where multiple players are moving and firing - the client may not always correctly predict where an enemy player is going to end up on the next frame, because that player may decide to stop and turn around instead of continuing on their current path. In this case, the client would have to be able to listen to an update from the server with the “real” state and then sync with the server. If you've ever played a game on a bad connection and seen another player seem to be stuck running into a wall and then suddenly warp elsewhere, you know that this doesn't always work smoothly.
Thankfully, in Manygolf, players don't collide into each other, so I don't have to actually worry about this! Since players only collide with the static ground, I can generally trust that the player is in sync with the server.
Some day, I'd like to make the netcode more robust. The most glaring issue is that the server currently tells you the location of other players by sending you their updated locations on every frame - while your own ball uses predictive netcode, other players' do not. This is due to laziness on my part - really, you should be able to receive only the swings of other players, and use that to run the physics simulation locally. However, adding this sort of thing increases the chance of your local state becoming completely out of sync due to a bad connection - e.g., a dropped “swing” message means that your client thinks another player's ball is in a completely different location than it actually is.
Solving this is far from impossible, and involves a regular check to make you're in sync and guarantees that you're not getting messages out of order (thanks @fritzy for some suggestions on this). I definitely want to return to this soon, now that the first “version” of the game is mostly done. It'll be necessary to make sure the game can scale beyond a handful of players, since eventually sending X number of players the positions of X balls 60 times a second is going to be far more than my poor Digital Ocean box can handle.
Physics with p2.js
With p2, I didn't have to worry about implementing collision detection or resolution for complex shapes, and got intuitive and fun physics mechanics like rolling and bouncing for free. It would have been really, really hard to build this part of the game on my own - the most complex collision handling I've done in the past has been simple bounding-box collisions for a platformer, and even that was a complete nightmare - so I'm glad I didn't have to worry about it!
State Management with Redux + Immutable
I once again turned to Redux and Immutable JS as my state management of choice. Both the server and client have their own Redux reducers (which share a significant amount of common code, particularly for the physics simulation) that handle all of the state going through the game.
Not a lot to say about this, other than that it made the client's rendering code ridiculously simple - it's just a big function that takes the current game state and draws to a canvas.
So, about halfway into this project, I found myself wanting some kind of static typing. I'd previously used Flow for some games, but wanted to give TypeScript a shot because it's significantly more popular and seems to have more community momentum.
It took a long time to get TypeScript building correctly in my existing workflow, which is my biggest complaint about it. Flow is a type checker that runs parallel to your existing build process - the only “compilation” required is a Babel plugin that strips the types, easy enough to integrate into an existing project that uses Babel - while TypeScript is a compiler through and through. I honestly hate this aspect of it, and wish it better integrated with Babel.
After many, many, many false starts, I eventually got to a point where both the client and server were building through a TypeScript -> Babel pipeline handled by Webpack. I plan to write more about this in a separate article soon, since it is such a complicated and difficult thing to set up.
Parallel to deciding to use TypeScript, I decided to give a new text editor a shot to make coding TypeScript easier. I've been using Vim for a few years now, but have never learned the “IDE-like” features, such as autocomplete, and the main TypeScript plugin seemed to have a lot of new keybindings and commands I'd have to learn.
First, I tried VS Code, which I quite liked. It seemed to have very good TypeScript support (not surprisingly, being made by the same company), but unfortunately, its Vim mode plugin is terrible - probably the worst I've used - and there was no way I was returning to non-Vim editing, especially given that my home keyboard doesn't even have arrow keys.
Thankfully, I found salvation in Atom, which has made huge improvements since I last tried to use it two years ago. Atom is awesome now, more than fast enough for editing on my 2013 Macbook Air (I have no idea where the speed complaints come from, other than web haters looking to complain - certainly it feels faster than any “IDE” I've ever used). I expected to hate Atom but instead was stunned by its full-featured Vim mode and excellent TypeScript plugin. There was still some pain setting up (mostly caused by TypeScript, not surprisingly), but I anticipate using Atom for most of my text editing going forward, TypeScript or not.
I'm really, really happy with how this project has gone. I made a few bold technical decisions that really paid off, and I find it easy to add and extend the game with new features that make it more playable. And, of course, I'm really happy that I've been able to play it with a bunch of people who also seem to have a lot of fun playing it.
The best feeling I've had since I started working on games was when, about a half hour after I posted a link to the game in my playtesting chat room, I realized that there were still multiple people in the current session playing it and talking about it. Of course, I can't claim to have designed the addicting gameplay loop - that goes to Trackmania and Desert Golfing - but I can at least claim to have implemented it, and that's good enough for me.
I'm not sure what I'm going to work on next with Manygolf. Of course, there's the netcode issue I mentioned previously, which is an interesting technical task. Then, there's a few game design tasks to tackle - I'm not happy with the very naive level generation code, and I've had many requests for a persistent leaderboard in the game, so players can track how they're doing over multiple holes of golf. Either way, I think I'll be working on this for a few weeks more at the least.