So this time I wanted to take an extended look to the networking system we’ve implemented. Some users have already asked about it and we think it’s time to explain more deeply how it works, the initial thought we had about how to implement it and how it has evolved.
Why did we add online multiplayer to the game?
Mainly, we wanted to extend the capabilities of the game. We all love this couch games, and they are awesome to play with friends, but only having local multiplayer is not enough nowadays. Adding online multiplayer opens the game to a greater audience, and it gives great replayability to the game, allowing for extra features like leaderboards, special events, global stats…
How is it different from other online multiplayer implementations?
Being new indie developers, we had no enough money to maintain servers running all the time, so we had to think of a new way of doing this. The obvious way was to use P2P, and replace the role of the server with something different. We had two options in here; P2P with a client acting as a host, or a total P2P implementation, where everyone would have the same information and would act the same.
That’s when we discovered GGPO (En.wikipedia.org), a network implementation model widely used in fighting games. Our network implementation is based on GGPO, and we’ve called it the Rollback system. Plus, using P2P also reduces latencies between the players, as the messages do not have to reach the server and then be sent to the other players, but they go directly from player to player.
What does it solve? How does it work?
The main objective of this implementation is to get an almost lagless game experience when playing online.
Overall this is what the Rollback system does. Instead of waiting for the input of the rest of the players to compute the next frame, we predict what they are going to send based on the previous inputs. Then, when the original input arrives, if it differs from what the system predicted, we roll back to that frame, insert the new input, and then simulate again the match until the present frame.
What problems does it bring to the table?
One of the main problems we had when implementing this system was the lack of control over everything related to the Unity engine. When re-simulating the frames, we had to approach what Unity did, and try to reproduce step by step what the engine does to every object and script in the scene. This means collision detection, collision resolution, physics update…
Unfortunately, Unity does not give you access to forcing any of these checks I’ve mentioned before. This means we’ve had to implement our own collision detection, collision resolution and physics systems.
Another detail of how this system works. Our online matches are defined by which inputs have been pressed or released at which frames. The simulations have to be completely deterministic, same scenario with same input should create the same new state of the game in all computers. So this means that my frame 100 needs to be the same frame 100 of every other player. In order to achieve this, we’ve made use of Unity’s FixedUpdate, which assures us that every FixedUpdate will be called every Time.FixedDeltaTime, thus making every frame take the same time to complete in every computer.
Rollback system: step by step
Now that we’ve seen what the rollback system does, and how we use it, I wanted to show every step of a frame in our game: beginning with what a normal frame looks like, and ending with how our game behaves if an input arrives.
Unity updates all scripts in the scene. If any input action was pressed or released, we send that packet over the network for the rest of the players to know, saving bandwidth and reducing the number of packets sent.
Now comes the time of the rollback system. It’s time to save the whole state of the level. To achieve that, we are going to iterate through every single object and script in the level and save every variable related to gameplay in a special structure.
This is a much bigger problem that it seems, so let’s look at the image above to better understand what we’re talking about. In that image I’ve highlighted every object that is affected by the system. Every single one of those has at least one component that needs to save data into the structure. You see those arrows surrounded by the blue ellipse? All of them need to save their position, rotation and scale; all physics data related to that instance (velocity, acceleration, gravity…) and any other info that defines its current state (flying, projectile owner…). In the case of the red ellipses, it’s simpler, they just need to save their position and some physics info.
By the end of this process, we’ll have a structure full of data which will be capable of restoring the level to this very frame.
Ok. So if no input packet arrives, this is the end of the frame. We’ve saved the whole state of the level, and we can continue to the next frame and repeat the process. But let’s see what happens if an input from another player arrives.
Let me show two videos of the same scene, but each one seen from a different perspective, first one being player 1 and second one being (obviously) player 2. Player 1 will shoot, and player 2 will jump in order to avoid that bullet. We’ve exaggerated latencies to better see how the system works. Here are the videos!
As you can see in the first video, something strange is happening when the second player jumps and it avoids the bullet. It’s actually doing the correct job, but the exaggerated latencies better show the process. Let’s look step by step in a timeline what is actually happening.
Player 1 shoots. The input message is sent.
Some time later, Player 2 receives the input. (Here the Rollback system also does a process, but let's ignore it and go to the important one of this example).
Player 2 jumps in order to avoid the bullet. The network message is sent.
Player 2 achieves to avoid the bullet and not die. But as the network connection of Player 2 is terrible, it takes some time to reach the computer of Player 1. This means the Player 2 dies in the Player 1’s game.
Then the jump of the Player 2 arrives. Player 2 has already been killed in Player 1’s computer, but we know when this input was executed.
Everything that happens in the next two images is executed in a single frame.
So the Rollback system kicks in. As we know the frame in which the Player 2 jumped, we can restore the state of the level in that exact frame. So we “roll back” to that frame, and restore everything. Restoring implies a bunch of things; first, we need to destroy everything that has been created since the frame; second, we need to create everything that was destroyed since that frame; and last, we need to update the state of every object that is still active since that frame. Once all of this is done, our game will be in the same state as it was in the specific frame.
Now that we are back there, we can insert the input of the Player 2 in the system. We’ve rolled back to this point because a change so small as a jump can have enormous changes in the state of the level, so the frames that we had computed after this frame are not valid. Now that the input has been inserted, we can now re-compute every single frame until the present one. If everything was done correctly, we should now be in the present, but the Player 2 should still be alive.
And that’s how the Rollback system works. I did not want to add code to this post, and tried to get a bit more into detail without making it boring. There are still some other systems to explain, but we’ll do them in another post not to bore you with so much text.
If there’s anything you did not understand or are curious to know, please ask me and I’ll be happy to answer.
Thanks so much for sticking with me!