• Register

Team up with friends in this top-down stealth-runner game to get to Rock n' Roll paradise. Use grit, abilities, and your friends to make it through the hordes of enemies.

Post feature Report RSS Pandarunium Stability: Multiplayer and LOTS of Enemies

This update provides stability for players with lower end machines utilizing some tricks I created to enhance performance in a multiplayer setting.

Posted by on

Pandarunium is a game that requires lots of enemies (and I mean up to 2,000 on a certain level) to be on a single level in a tightly contested amount of area. This game is also going to be using a Peer-to-peer networking 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.


In this devlog, I will detail how I got to where I am today with my current networking solution. Just for reference, I am using Unity with GameObjects (Sorry no DOTS info here), a custom networking solution utilizing Facepunch.Steamworks (This is a C# wrapper around the Steamworks.NET C++ implementation).

So the premise of my game was a panda running through tons of enemies to get to the middle of the spiral maze. If a player runs into an enemy, they die on the spot and another player must run over their body to revive them. This is directly inspired by a custom game called Run Kitty Run from Warcraft 3.

RunKittyRunScreenshot

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 my current game. 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. What should be done is to get the idea and find solutions that work for that idea. When you see problems, get data, analyze the data, theorize what could have gone wrong, and make changes and do it all again. Or in other words, do the scientific method.

Working towards the Current Solution:

  1. Moving Enemies
    Moving enemies is a big task for this game. There are almost 2,000 in the small scene that I had created. I needed to balance several things two of them being: the games framerate to ensure there was no stuttering, 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 every frame that it was moving.

    For those who don't know when I say packet - 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.

    This solution came with a huge problem which was that the host would be send out SOOO many packets per frame that it caused my whole game to not be able to display in time, and I would miss incoming packets from players, and a whole slew of things were wrong. The solution I came up with to fix that was what I called the PacketBundler.

    The PacketBundler was a way for lots of enemy positions to be packed up into a single packet and sent to the clients. When the enemy needed to move, I would tell the enemy to tell the PacketBundler the data that needed to be sent, then at the end of the frame, the PacketBundler would send off those pieces of data. 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. 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...

  2. Enemy Regions
    The next thing I decided to do was to control which enemies really needed to be moving at a time. 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. The enemies nowhere near and players should not be active. When a player teleports to a region that has inactive enemies, all enemies in that region and the ones adjacent to it will now activate. This tremendously reduces the load on the system especially for lower tier computers like mine.

    MapRegions

  3. 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 with the players with 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 simply 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.

    This 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. So, every frame I would have each moving player tell the packet bundler to add the player position data to the packet and have it sent off at the end of each frame with all players data and the client would forgo using the navmesh agent and simply update the position of each player. This somewhat worked, but when I had the framerate for updating the player positions too low, the position would look very jittery on each of the clients. So I cranked up the framerate for each fixed update to 60. The position was just one of the things I sent as part of the PacketBundler. I would also send things like the location of where the player was going, its target location, as well as the state for things like animations (should the player on the clients machine be idle, moving, dying)


    The decision to update at 60 FPS ultimately caused me a lot more problems. When going to level 10 the one with nearly 2,000 enemies the game would spend way too much time performing physics calculations , and rendering that the engine would miss frames completely. This meant major stuttering and missed packets from clients.

    So I knew I needed to drop the framerate down to 30 in order for this to be a playable game - especially with 8 players. I ultimately decided that I was ok if the player didnt have the absolute most perfect position state. And so I decided to remove sending the positions of each player at each frame from the server, and 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. The player(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

    Now 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 cases where there is missed packets for example.

Hopefully this was entertaining to read and maybe you learned something from it. I really enjoy doing game development and hope to do it for many more years.

Tenax Studios

P.S. Go wishlist and play the demo of Pandarunium on Store.steampowered.com

Post comment Comments
Tenaxstudios Author
Tenaxstudios - - 2 comments

Im thinking about making a youtube video about this with side-by-sie examples of what the server and client see. Let me know if that sounds like something you would be interested in

Reply Good karma+1 vote
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: