Learn to code in multiplayer game part 2
Yes to “single source of truth” until it starts working against you.
Conventions are great and I used to follow them so closely as if they were the undeniable law in programming. However as I grew and encountered more different problems as an engineer in my professional work and in my hobby projects, I found conventions are a great place to start, but each app or sometimes even each feature has its own quirks and nuances where conventions begin to work against what you’re trying to do.
The problem with WaddleWaddle is that there is a lot of code running during every second of gameplay. Your troops need to move across the map while keeping tabs of enemy units’ positions and buildings, some characters only care to collect resources, while other combat units need to attack when the enemy is within range. That’s only half of it because this is a multiplayer game, which means for all those actions above, the game also needs to run the same code for your opponent.
To keep the game in sync, I needed a way for both players to share the same data which brings us to the topic of “single source of truth”, which is the concept that we should only have one central location where information exists so that we can trust that it is the correct and most up to date information every time we reference it.
Naturally as a believer in “single source of truth”, I placed all of that logic and responsibility in the backend where I immediately calculate every change from both players and store the data to my one server then send the updated values to both players via WebSockets. The flow looked something like this:
(F) represents Frontend
(B) represents Backend[1] > (F) player1 tells server to create some unit
[2] > (B) server calculates that this unit is ready to move
> (B) calculate new location
> (B) save to DB
> (B) send message to player1
> (F) update UI
> (B) send message to player2
> (F) update UI
* Repeat until step 3[3] > (B) server calculates that some enemy is within attack range, attack!!
> (B) calculate new hp
> (B) save to DB
> (B) send message to player1
> (F) update UI
> (B) send message to player2
> (F) update UI
* Repeat until either this unit or enemy unit has 0 hp, then go back to step[2]* Rinse and repeat this whole thing for every unit that gets created
See a potential problem yet? If you do, amazing! You could’ve saved me 2 weeks of headache because I didn’t foresee it so I kept going with this model and even brought it to a playable state. I was able to beta test with several individuals and the game played perfectly fine. Then when I went to beta test with a group of people, that’s when the underlying architectural flaw surfaced itself.
Between a group of 6 people, two minutes into our simultaneous 1v1 matches, all three games began lagging to a point where it was unplayable. I checked the server logs and it was immediately apparent to me what had gone wrong. My free server couldn’t handle the stress of 3 games running at the same time. The server was trying to process hundreds of actions per second and it pooped itself.
The obvious solution would’ve been to upgrade the server so that it could handle 3 games at the same time, but I would have a really hard time making this scale. What if I wanted to support 300 games simultaneously? Or 10,000 games? How fast would the server need to be, and how expensive would that be? So I thanked my friends for wasting their time and got back to thinking.
I had one fact and two suspicions at this point:
Fact: The server is doing too much work
Suspect: I could offload some of the stress to the frontend
Suspect: That would mean having multiple sources of truth 😲
I thought it was a terrible plan, but I was also very curious to see if it could work so I gave it a chance with the expectation that it would fail. To my surprise, it worked much better than anticipated.
I realized 99% of the heavy calculations could be offloaded to the frontend since the game is designed on the same premise as a pinball machine — as in each time you create a unit, it abides by strict rules of where to walk, how fast it walks, how far it can attack, and how quickly it can attack again. Except I would have to get rid of the character’s “accuracy” property since that’s a random value and I’d be willing to trade that off. Theoretically, this would mean that even if two versions of the game were running in two clients, the results should always be the same. Here’s an updated illustration of the concept:
(F) represents Frontend
(B) represents Backend[1] > (F) player1 tells server to create some unit, meanwhile updating the server of player1’s game's current state
> (B) create the new unit as requested
> (B) save the updated version of the state of the game coming from the frontend to the DB
> (B) send message to player1
> (F) updateUI
> (B) send message to player2
> (F) updateUI[2] > (F) player1 calculates that this unit is ready to move
> (F) calculate new location
> (F) update UI
> (F) player2 calculates that this enemy unit is ready to move
> (F) calculate new location
> (F) update UI
* Repeat until step 3[3] > (F) player1 calculates that some enemy is within attack range, attack!!
> (F) calculate new hp
> (F) update UI
> (F) player2 calculates that one of its units is within enemy attack range, got attacked!!
> (F) calculate new hp
> (F) update UI
* Repeat until either this unit or enemy unit has 0 hp, then go back to step[2]Rinse and repeat this whole thing for every unit that gets created, but notice how many more (F) we see this time? Which is a good thing in this case. That means all the heavy lifting is distributed across all players computers as opposed to the one server that the backend is talking to. Making this transition ensures a smoother playing experience for all!
To some degree, the two versions of the game ran in sync. Unfortunately, if enough units are running rampant, the game could run out of sync due to differences that were calculated by only milliseconds apart. However, this could easily be solved by syncing the game periodically through the server. Lucky for me, I already did this because whenever a player creates a unit, that action must be routed through the server to update the game on the opponent’s screen.
Two weekends later, I had a new working prototype that supports at minimum 100 games simultaneously that’s still running on the same free server. We’re now averaging 1 action per 20 seconds per game instead of 50+ actions per 1 second per game which is incredibly better for my wallet ✌️
The prototype is not without flaw though, I ported all the logic from the backend to the frontend without messing with it as much as I could. As a result, I’m doing recursive setTimeout
to achieve unit movements and attacks which is less than ideal because that's annoying to build on. There are better tools to accomplish that in the frontend with React Hooks. Stay tuned for an update on that!
The premise for the WaddleWaddle app is to embody a combination of comprehensive learning platforms such as Codecademy and good old video games that keep you entertained and wanting to do more.
I’ve always enjoyed strategy games since I was four and more recently I had a really fun time playing Clash Royale, so I’m putting these two very different systems together in as complimentary of a way as possible to bring you a totally new learning experience.
The Ambition:
- To make learning code less intimidating and more enjoyable, entertaining, and approachable.
- To raise awareness that anyone who wants to be a software engineer can be a software engineer.
- To make lessons more accessible for more people.
We are opening up our prototype to the public for early alpha access! Bring your friends and come learn JavaScript in a fun never before seen way!
waddlegame.com
I do tutoring for JavaScript on the first Monday every Month on Meetup
We will also be live-streaming the events on YouTube and Twitch
Join our Discord community, I’m always happy to chat about code!
I will be blogging about technical challenges, mistakes made, and lessons learned while building the WaddleWaddle app. Follow me here if that’s a journey you’d like to read about!