Blorb World

Blorb World is a multiplayer co-op action RPG where you and other players can take quests and fight enemies to level up, get stronger, and fight the mighty Minotaur.

This is a Unity game made in my senior year at DigiPen with a team of 4 people, consisting of 3 programmers and a technical designer over a 13 week development cycle. This game was developed remotely, as COVID-19 lockdown rules were still being enforced. This is a “sequel” to our last project, Blorb Island, in the fact that it shares the same art style and some assets, but the main bulk of the game is very different.

Since this project was remote, it was integral for the networking to function correctly over spotty internet and at least 100 ms of ping since I stayed in my hometown in California, while DigiPen is in Washington. To ensure this goal, I had 3 sub goals to make sure that my code worked. In no particular order, I wanted to:

  1. Create seamless gameplay focusing on client side latency over consistency

  2. Actualize my research for latency reducing techniques

  3. Utilize C# low level sockets for the first time in Unity

Let’s quickly go over why each goal was important for the project.

  1. Create seamless gameplay focusing on client side latency over consistency

Since this game is a non-competitive PvE (player vs. environment) game, I only really had to care about how the players felt. In a game like Counter Strike, the networking engineers must make sure both sides feel that the game is fair. For example, if someone is lagging behind and shoots someone on their screen, the game servers will check to see if that bullet actually hit.

In most cases, the server will tell the lagging player that they did not in fact hit the player they thought they hit, making them feel cheated out of their shot. They could fix this by having the client tell the server if they hit or not, but this would then make the other player feel cheated, as they got shot when they clearly did not on their screen. In both cases, one of the players feel cheated. However, as stated before, this is not an issue for our game, and we can focus solely on the player’s perspective.

2. Actualize my research for latency reducing techniques

I knew going into this project that I would most likely not be able to just throw in some socket code and get the game working as well as I would like it to. I knew that I had to at the very least attempt to utilize some of the techniques I had learned in class and in my own research to make the game work.

3. Utilize C Sharp low level sockets for the first time in Unity

This was my first time using low level sockets in C Sharp, so I had to learn the basics before even starting. This is somewhat of a given goal for the project.

The rest of this page will be going over the various networking techniques I used and my thought process throughout the development of our game.

Networking Architecture

For our game, I decided to forgo the Unity networking API, as I have heard from many others that it is more hassle that it is worth. I wanted to use a server client structure as this game would support more than two players, with one player being the host that others would join. I went with a built in server rather than a separate executable as I had done in other projects for user simplicity. I used the built in C Sharp UdpClient class as the basis for our networking. I will go over how I implemented UDP and why I think UDP is important for our game later in this page. This gave me an API similar to the C++ socket API with some helpful functions built-in to the class, such as a constructor that automatically binds to my local IP. I was familiar with the C++ socket API from my previous work on Battle on Big Island, so I felt like this was a good starting point.

The UdpClient class had blocking functions for both send and receive, and I had 2 options to get around this: multithreading, and asynchronous functions. I chose to go with multithreading, as I had experience with multithreading non-blocking networking code from Battle on Big Island as well. I wanted to save as much time as I could for this project to get to the real meat and bones of the project, the gameplay systems, so I set up a system very similar to the Battle on Big Island code base. I made a Packet class that contained an enum to differentiate what kind of packet it was, and a byte array for data in the packet. Then I spun up two threads in the constructor for the Client class, one for receiving packets and one for sending packets, and then used lists of packets and mutexes to synchronously add and remove packets from both lists. The client would then iterate through the list of packets received in the last frame and use them according to their packet type. If you would like a very in-depth look at how this system worked, click here and look for the “Networking Architecture” section.

One difference between this system and the BoBI (Battle on Big Island) system is how packets that effect players, such as player animations, are handled. In BoBI, all packets were sent through the update loop in the client, and I would find the player object that is affected by the packet. In this system, I instead added a list of packets to the player class. Instead of finding the player manually and updating everything in the client, I could just add the packet to the list in the player class and have it update itself when Unity gets to updating that object. This led to a lot more decoupling of the systems, along with cleaner code.

Client-Side Prediction

One of the major techniques I used to reduce latency in our game was use client-side prediction/authority for almost everything in our game. As stated in the paragraph about the first goal, I did not care about favoring the enemies in our game. With that in mind, I wanted to always favor the client; if they land a shot on their screen, that will always go through, no matter of their target’s “true” position on the host.  This is similar to the “favor the shooter” ideology that Overwatch has, but in this case, there is no server-side check to see if your shot actually hit as it is not needed.  To implement this, I did all hit detection on the client and sent packets to the server if anything collided and did damage.  This strategy has one major drawback, however: if they were lagging or desynced, every other player will see them miss but still see that they did damage.  In other games, I think this could potentially be a major problem, but for us, the players are completely focused on making sure their own character.  If they see seemingly random damage here and there, they will most likely not even notice it.

This would also be applied to enemy hitboxes and the client.  Rather than the host checking hitboxes, the client would just take damage if they get hit on their screen.  Once again, this would make it so that the clients never feel like they did not get hit, with the same problem that sometimes they will get hit on the host screen but not the client screen.  However, in our game, we did not replicate player health, so this is not an issue that would affect normal gameplay.

Replicating Players

I wanted to touch on this briefly because I will reference this further down on this page. I replicate players by sending their position along with their x and z rotation every frame. Each client sends those attributes to the server, and the server sends all rotations to all players. I only send the x and z rotation because it would look silly if players looked up and rotated their whole body up. I replicate the animations of players by

Skill Replication Systems

The main gameplay mechanics I worked on was the skills that players could use. This was the core gameplay element in Blorb World, as combat was the main draw of our game and the combat revolved around the different skills you could use. I collaborated with our designer to iterate and make sure the skills were correctly replicated over the internet. Skills in Blorb World are activated by pressing the hotkey related to the skill, which then plays an animation and spawns a hit box. My first idea to replicate skills was to send the skill name to the other players when you hit the key. The other clients would then simulate the skill on their screen by spawning a replica of the prefab, found by using the skill name given in the packet. This solution would be scalable to include any new skills as we came up with as well, so it seemed like a good idea. However, I realized that this would not work due to the way our camera works. I only replicate the x and z axis of rotation for each player, and since some skills utilize the yaw and pitch of the camera, just sending the skill packet would not work. I thought about sending the player’s current camera configuration, but this still would not work, as some skills have startup times. Players can move during this cast time, so the skill would spawn too early and be in the wrong position. For example, the lightning spell takes one second to cast, so if we were to send the skill packet when the player pressed the key, the other clients would simulate the skill immediately while the actual player is still casting the spell.

This idea could have worked if we had planned for it since the beginning of our development, but in order to flesh this idea out to production we would have needed to implement skills in a way that we could simulate the code to cast the spell. While we could have just hard coded in each the timer for each skill, we had a lot of planned skills and wanted a scalable solution. This solution would have also needed to send all the camera data which would have more than tripled the amount of data I would need to send per frame per player, so I thought about a different idea instead.

The solution I came up with in the end was to just send a packet whenever the hitbox was created.

Client-Side Enemy AI

One of the things I wanted to avoid in this project was enemy AI replication.  Our AI is not deterministic, so the only real way to replicate their AI would be to send over all the changes in their FSM to all clients.  While this could potentially work, lag spikes could easily desync their positions, leading to animations not syncing and hitboxes being in the wrong position.  While these issues are fixable, it would take a ton of work and time, and I knew there was a better solution.  However, I think the idea behind this is solid, so I approached it in a slightly different way: I made a client-side version of each enemy.  This client-side version of the enemy AI would only control the FSM of the enemy without affecting their position at all.  These client-side enemies then also have the ability to animate themselves and create hitboxes.  With this, I could then network enemies by sending the clients only the positions of the enemies.  As their positions update, the client-side version of the enemy would update their FSM to act as they should given their current position on the client.

Let us look at an enemy going from the wander state attacking a nearby player as an example.  An enemy is wandering around, and the host is constantly updating the clients with its position every frame.  When an enemy gets close enough to a player, the enemy should change their state to “attack”, starting their animation and creating a hitbox.  Rather than sending this new state to the clients, we can instead just wait for the client-side enemy to get to the same position, then start attacking the player.

While this saves a lot of data being sent over the wire, this strategy has problems.  The main issue is that if the client were to lag or drop the position packet for a few frames and move out of the attack radius, the enemy would attack on the host but not the client.  The client’s enemy would also stop moving, as it is attacking on the host side.  However, in my testing, even with around 50-100 ping, this problem only happened a handful of times during normal gameplay.  I decided to keep this issue, as successfully replicating the enemy AI would both take up a lot more bandwidth.  I would have to send a lot more information, but also make the enemies look stupid if the player lagged.  If the enemies always attacked where the host says they attack, sometimes the client would be much further away, making the enemy attack for seemingly no reason.

Utilizing UDP

I felt that it was necessary to use UDP since we are going to be testing over the internet rather than on LAN.  In video games, UDP can be much faster than TCP because TCP has to wait for all previous packets before receiving the newest one, even if they are not necessary.

tcp.png

Message 3 gets dropped, and Host 2 must wait until message 3 is received. 

Let us go over why this can be a problem in video games.  In the example above, let’s say that the messages being sent over are positions of a player moving in a straight line.  If we drop the third message, we most likely do not care about that one frame drop.  It would be much better to just drop one frame of movement and move on, but TCP will wait and attempt to get the third message before moving on.  This can cause much bigger lag spikes to happen when they do not need to, and because of this problem, I decided to implement UDP in our game.

I implemented UDP with sequence numbers to know the order of the packets being sent.  I can then use these to take the newest position packet on any given frame and use that over the older ones.  This gives the clients the newest position from the server, and we fix the problem that TCP has.  I also implemented reliability for UDP because there are a lot of packets that do need to get to the clients no matter what, such as the connection packet.  I used ACK packets and retransmission timers to emulate the way TCP works without ordering, as I felt it was not needed for our game.  I used a list of packets that had been sent, and if the packets were ACKed, they would be removed.  Each frame I would decrement a timer for each packet, and if the timer reached 0, I would resend the packet and reset the timer. I most likely could have also used a separate TcpClient class to do the same thing, but I thought getting experience writing a TCP wrapper for UDP was useful.

Interpolating Snapshots

                Our game is networked by just sending the positions of all the enemies and players currently loaded on the map, so if you lag, everything will teleport to their new locations.  Instead of having this jarring teleport, I interpolate between the current location and the newest location received from the host.  I use the object’s speed to calculate how far they should have moved in the lerp.

     //   counter is incremented once per frame

float length = (pos1 - pos2).magnitude;

lerpT = ((float)counter * ai.mySpeed) / length;

transform.localPosition = Vector3.Lerp(pos1, pos2, lerpT);

This code sample makes the enemy move at its normal speed until it reaches the destination.

There is a small bug with this code, however.  If the player lags for a substantial amount of time, the enemy’s new position would be very far away from its current position.  The enemy would then walk to the new position at the same speed, potentially never catching up if the enemy is moving.  So, I also check to see if the distance between the two points is too large; if it is, then I teleport them to their corrected location.

In a game like League of Legends, this cannot be done because knowing the exact location of an enemy is extremely important.  If they are not, I will have hit them on my client but not on the server.  This is not an issue for our game, as there is no server-side validation for hits that the client sends out.  I valued the smoothness of interpolation over the consistency of the player and enemy positions.  Not only did it look better, but with hit detection and enemy AI being client-side, players will feel no difference even if the enemy positions are slightly off.

Conclusion/Postmortem

I learned a lot more about network gameplay than I felt like I have in any other because I had the time to implement and try out a few interesting networking techniques. Our game works pretty well over the network, but there are times where it can be jittery/buggy with high ping. The game uses about 500kbps up/down with two players, with each additional player adding an extra 200kpbs up for the host. One of the major things I learned was how difficult some of the problems you face as a network engineer are; some of the problems I had to fix took me hours and hours to fix due to the complexity of the system.  Knowing how long something will take to implement and how helpful it will be are crucial to being a good network engineer. Lastly, I wanted to do a small postmortem on the project and how it went, as our circumstances were very different from the previous years.

  1. Communicating remotely is a lot harder. One of the biggest problems we faced was keeping everyone on schedule and making sure we knew where everyone was. It is a lot easier with a whiteboard in person to schedule stuff and make sure we are not behind. Being remote also makes simple tasks more annoying/difficult; even something simple like asking someone for help on their API can be harder. In person, I would just tap their shoulder and ask, or if they were gone, wait for them to come back.

  2. Making sure you’re on schedule is even more important online. It can be really difficult to tell if someone is falling behind on their work if you’re not really paying attention. Having a good producer helps a lot with this.

  3. On the networking side, the earlier you plan things out, the better. Planning is key to making sure all your systems interact with your networking systems in as smart a way as possible to minimize the work and bandwidth needed for your game.

Next
Next

Blorb Island