A couple months ago, very bored and still between jobs, I was itching for a new project. I wanted to make another game, since it had been a long while since I’d worked on one - Manygolf having stalled, and the couple of projects I’d tried to build since not having gone anywhere.
I complained about having nothing to work on in a Discord I hang out in, and someone out of the blue mentioned Crossroads, an old C64 game, and asked if I could make that with online multiplayer. I’ve made games on worse pretenses - Manygolf only exists because I thought “hey, what if you combined Desert Golfing and Trackmania” - so I started working on it, under the working title Sledgehammer, because I was listening to a lot of Peter Gabriel.
Crossroads (and its sequel Crossroads 2) is a maze shooter with an interesting gimmick - there are various types of enemies that can have various interactions with each other. Certain enemies attack others, some will team up, etc. The maze is full of enemies, to an extent that was actually really impressive on the C64. You can see a video of the sequel here that shows off how complex things could get.
While my previous multiplayer games had been built around a server-client system using Websockets, this seemed like an interesting candidate for peer to peer networking. While doing netcode research for Manygolf, I had come across a few articles mentioning that WebRTC could be used for such a purpose, and spent a few days just getting my head around it. I created a tiny signaling server, which coordinates connecting peer to peer clients (similar to a centralized lobby server in many peer-to-peer games), and started work on a quick and dirty prototype.
There were lots of interesting pieces to tackle. Unlike previous games I’d built, which had players as free-roaming pieces in a world using traditional AABB collision detection, Sledgehammer has tile-based movement for everything, more similar to Pac-Man. Just implementing this correctly (along with wrapping the player, and enemies, from one side of the screen to the other) took a lot of work.
The original prototype of Sledgehammer was just written in vanilla TypeScript with what I thought would end up being a functional-ish style. There was a big tree of state, which made serializing data from the host player (who computes everything) and sending it to the client to render really easy. However, I found myself having a lot of trouble figuring out how to split up the code that update the game on every frame into isolated functions, and also couldn’t figure out a clean way to have the client have its own runloop for handling non-synced data, such as the particle effects emitted when a bullet collides with a player.
Eventually, these problems lead me back to Pearl, a framework I had originally built a couple years ago, when working on my first post-Manygolf projects. Pearl is an entity-component framework, similar to Unity, which makes it relatively easy to structure more complex games.
However, entities in Pearl are represented as entity objects that contain component objects, which are not nearly as easy to serialize as just slamming a state tree composed of POJOs into
JSON.stringify() and sending it over the wire. Instead, I built some very fun but very dirty code for
NetworkedHost is responsible for creating entities, serializing their state, and sending them to the client, and
NetworkedClient then takes these serialized entities and constructs entity objects with the correct components and component objects. This is currently a very manual process - each entity type has a prefab object that holds how to construct the entity, as well as how to serialize and deserialize them. A simple example of this is the prefab for a bullet’s explosion effect, which syncs the location of the object, but not the explosion state, since that’s computed separately on each client as a visual effect).
In addition to syncing entities, there’s also a utility for the server to call an arbitrary method on a component in each client. For example, when a player dies and the death animation (a particle explosion) needs to be played, the server calls
Player.rpcDie(). Each method that starts with
rpc is monkey-patched on the server to send a WebRTC message to each client ensuring the method is called.
Obviously there’s a lot more to do with the hacky netcode here - way more data is being sent over the wire each update than needs to be right now - but this was good enough to get started, and is good enough for playing online with another player on a reliable, low-latency connection.
In addition to netcode, the other interesting challenge is implementing the enemy AI. For example, some can see ahead a certain distance and shoot at players in their eyeline, and others start chasing players when they see them, pathfinding across the map to catch them.
I’m hoping to get the game to a point where it’s playable with a nice game flow, but for now, it’s a little awkward. You can play around with it in its current state at sledgehammer.surge.sh, though you’ll note there’s not much of an objective - in fact, as of this writing you spawn next to the pickup that moves you on to the next level! Still, it’s functional as a tech demo. The code is also available on my GitHub.
Working on Sledgehammer has brought me back to working on Pearl, and I’ve been working on polishing it to a 0.1 release, including a few other demo games - a simple overhead puzzler, and a side-scrolling platformer, both of which I hope to write more about soon. Now that Pearl’s gone through drastic changes, I want to return to Sledgehammer, update it for the changes, polish up the netcode a bit, and release a fully functional game. Hopefully this all happens in the next few weeks to months! Even if not, this has been a fascinating learning experience, and I hope to write more about WebRTC and Pearl on this blog.