Multiplayer Stability


Note: You can also watch the video where I narrate this and you can see footage of the game and other details 

Intro: Multiplayer gaming is a complex problem and each game requires its own unique solution. In this devlog I will talk about how I achieved stability for players with lower end machines utilizing some tricks I created to enhance performance in a multiplayer setting. This video will be about high level concepts and geared toward some of the early-to-mid-level game developers out there wanting to make a multiplayer game but struggle to know where to start or even how to think about this topic. Maybe I can give some insight into my thought process as well as some resources for getting started A note about me: Pandarunium is my first game I will be releasing, I do have a few others I’ve worked on and I have gotten a good amount of experience before this but none of those have made it to release. Some are pretty quirky and maybe I’ll make a video showing those ideas, their concepts, and why I dropped them. I think there’s some fun stories and advice in there. Let me know if you’d be interested in seeing some of those.

Body: In my game Pandarunium (which you should go wishlist and try the demo on steam btw)  is a game that requires lots of enemies, and I mean hundreds, to be on a single level in a tightly contested amount of area. The premise of my game was a panda running through tons of enemies to get to the middle of the spiral maze. If the player is hit by an enemy he drops dead on the spot. A dead player would only be allowed back into the game if the other players ran over his body to revive him or the other players complete the level - respawning everyone. This was directly inspired by a custom game from Warcraft 3 that had a bunch of players spawned as these Cats and you run through a maze of wolves with a high pitch version of Linkin’ Park’s Numb playing in the background. This was one of my favorites to play with my friends in my high school days. I decided early on that I wanted to make a multiplayer game but didn’t want the cost of paying for servers, so Peer-to-peer networking became my ultimate solution. So one player will be acting as the server. All the other players send input to the Host player, the host performs all the validations and calculations then sends back where everything happens. The hosting player will also be the only one running logic for the enemies and will send updates to other players about the true state of the game.

Just for reference, I am using Unity with GameObjects (Sorry no DOTS info here), a custom networking solution utilizing Facepunch.Steamworks (which is a C# wrapper around the Steamworks.NET C++ implementation).

Just a note: These decisions I made for this game may not be good for all games, or even most games. They were specific to the needs of Pandarunium. So feel free to use them, but don’t expect this to be the be-all-end-all of solutions. And heck, there are probably even better solutions than this current one. So lets get started.

Working towards the Current Solution:

Moving Enemies

Moving enemies is a big task for this game. There are almost 2,000 in the small scene I had created. I needed to balance several things but two of them being: the games framerate to ensure there was no stuttering or missed frames for processing, and the smooth movement of the all characters. I naturally started with the easiest solution, which was making each enemy responsible for sending out its own packet of data to the clients for every frame.

For those who don't know what a packet is - a packet is just a block of data with a header on it saying who the packet came from and what type of packet it is and with data agreed to by both the sender and receiver.

Having each enemy send out it own packet came with some big drawbacks. The first of which was that the host would be send out SOOO many packets per frame that it caused a whole slew of things were wrong such as missed processing frames, and late or missed incoming packets from players. The reason for this is because the game engine would run important processing 60 times per second. On the higher levels 2000 enemies would send a packet a single packet 60 times per second. That means 12000 messages were sent out to each client a second. This was far beyond what mine-or any normal computer would reasonably be able to do. The solution I came up with is called the PacketBundler.

The PacketBundler allowed enemy positions to be grouped into a single packet. When the enemy needed to move, it would tell the PacketBundler which enemy is moving and to where, then at the end of the frame, the PacketBundler would send off those pieces of data in a single packet to clients. This was great! The enemies were moving fine but there was another problem.

This worked for a small number of enemies, but when I scaled up to larger numbers I noticed the packets would fail to send. After some research, I found out that there are size limits and fragmentation of packets once you get to a certain size. So I decided to build a Chunker. Because I knew the limit of the packet I wrote some code to break up the enemy positions into groups up to, but not over the byte limit. And voila, its works great…ish until I needed a bit more...

Enemy Regions

The next thing I did was to control which enemies really needed to be moving at a time. The game doesnt need enemies in the middle of the spiral moving, animating, and performing physics calculation when no player is even close. The way I decided who should be moving was by creating regions in the map. Each region would be responsible for knowing which players and enemies are in each region. I wanted to make it so all regions adjacent to one a player was in, would have active enemies - active meaning they would display animations and perform movement, detection, and path calculations. This tremendously reduces the load on the system especially for lower tier computers like mine.

Enemies and Framerate

The decision to update each gameobject at 60 FPS was originally decided because I saw it gave the clients the smoothest movement. On the level with hundreds of active enemies, the game would still spend way too much time performing physics calculations and rendering for enemies.

Using the profiler built into Unity, I was able to see that I was missing the 16.666 ms time often. I knew that the game did not need to be updating at such a high frame rate for this type of game. So I decided to lower the framerate down to 30.

MapRegions

Player Movement

And now we get to player movement. I have made so many iterations on this that I wont go into all them, but I will tell you the biggest changes I did.

So, initially I started the players gameObjecct having rigidbodies. The current movement system requires the player to click a location and the character will follow - similar to RTS games like Warcraft, Starcraft, etc. I first started by Lerping or Linear Interpolating the player to the position. This immediately became a problem because when I clicked across the wall the player would get stuck on the wall. So I needed a pathfinding solution.

I found two great solutions for this - one was using the A* algorithm. A* is a way for the computer to calculate a path given the set of areas the player is capable of walking. The second solution was using Unity’s built in NavMesh and NavMeshAgents. They both seemed great but, I decided to use the NavMeshAgent mostly because I wanted to learn how to use them. Really there was no great reason haha.

I hooked it up and it worked great for single player, but now I needed to tell clients where they were. So I decided to add the player positions to the PacketBundler as well to avoid the same problem I was having with the enemies. Every frame I would have each player tell the PacketBundler  and it would send off all player’s movement data in a single packet. The client would forgo using the navmesh agent to move and solely rely on the position packets from the host. 

I ultimately decided that I was ok if the player didnt have the absolute most perfect position state. And so I decided to remove the PacketBundler sending the positions of each player from the server. I would instead let the player's NavMeshAgent do the movement for each of the clients. What this basically means is that the client and server have two separate states and their positions could be slightly off. This was a tradeoff I was okay making for the smooth movement of player. 

I was able to do this because the client is always sending the target location to the server and the server relays that target for the rest of the other clients. The other clients will then have the navmeshagent calculate and move to the target position. The only thing I truly have the server calculate is the collision between the gameObjects. And for right now the gameplay is very smooth

Something I may need in the near future is to synchronize the client's player position with the server. This can probably be done on either a time basis or event basis but I will get to that when I am able. This would be needed for example when there are missed packets. It would cause a small jitter, but would still be better than what I previously had.

Hopefully this was entertaining and maybe you learned something from it. Like the video and let me know what you think in the comments. Also, go wishlist Pandarunium on Steam and try out the demo. I’ll leave a link down below.

Thanks for coming to see me and Yeah curtis!

Get Pandarunium

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.