📚

How It Works - Theory!

I can’t help but write out as much theory as I can since I’ve enjoyed this topic. You won’t need to know it this deeply, but at least look at the architecture to get a feel how the project works as a whole.

This is the architecture model for this project:

For a quick run down:

The Master Server in this diagram helps our servers and clients connect to each other. Clients and Servers join the Master Server, the Master Server maintains an active list of currently running servers and passes server information over to clients. Clients can then ask to join these servers and the Master Server helps with this whole process.

UDP Packets

Packets hold data and a header address (IP address and port number of who to send it to). UDP is connectionless and unreliable compared to TCP packets, but they trade those out for speed.

Reliability has been coded in rather then the hardware level within the LiteNetLib library used on this project to help with sending data around.

However we don’t always want reliability with packets anyway. Currently server world snapshots are being sent many times per second to describe where things are in the world. If some of those packets get lost, it doesn’t matter because a new one will show up soon enough with up to date information.

Some things you want reliable though, like telling players to migrate from sort of lobby scene to a gameplay scene or that the game has finished and who the winners are. LiteNetLib lets us choose between reliable and unreliable packets, which is awesome!

Clients

These are your players. They join the master server to request information about what game servers exist and then can join those game servers.

Once they joined the server they too run a copy of the games code to help with client side prediction. So clients collect inputs from the player and relay those to inputs to the character they control on screen to act on those inputs immediately, but also they send these inputs to the server so the server can action these inputs on their character on the servers side.

When packets come in from the server the world is updated or corrected. If the client side prediction was incorrect, the player’s character is corrected using a rollback process. Information about other players and things in the world will be updated and interpolated where appropriate.

Clients send their inputs(button presses, mouse movements etc) because if they send their own coordinate data, we could see some weird cheating! We can’t fully trust clients, so that is why we send inputs from client to server and server also plays those inputs out as if they were done locally to verify the outcome.

Server

This is where most of the games logic lives, this is the game. I’ve set it up so that the server instance can also be a player (think Borderlands, the party host is the server also) or non player and be a dedicated server (think Halo Slayer deathmatch instances, the server is hosted without being a player). Servers run the game, receive connection requests from players, receive inputs from connected players and send snapshot and game relevant packets to the clients.

Master Server

Disclaimer: I understand we’re trying to move away from the word master socially, but at the time of writing this is still the common name for this part of the stack. I’ve named it such to help for hunting down any theory you need, but definitely feel free to rename it to something else. Maybe Management or Main Server or something?

Not a super exciting part of the architecture but an important one. Clients and servers join the master server. The master server keeps a list of the servers and general information about them (e.g server name, how many players are in there, you can add more though). Clients request a list of available game servers from the master server. When the client picks a server to join the master server helps them connect via Nat Punchthrough.

Currently this is how the master server works. If you wanted to manage databases for character save data e.g skins unlocked/equipped or manage matchmaking, this is the place to add that code.

Snapshot Packet

In simple terms, this is the xyz, rotation, animation info etc about each thing in the game world. Clients use this data to correct their own player characters and update the other characters and things in the game world around them. The server takes these snapshots multiple times per second and sends them out to all clients connected to them.

Client-Side Prediction and Rollback

The main thing that makes networking confusing is that you have to account for packet travel time. A client presses a button and actions it on their instance and sends the button press to the server. Time passes. Server receives the button press and actions it, takes a snapshot of the world and sends it back to the client. Time passes. Client receives it. It may be insignificantly small like 1ms or quite large 200+ms, but its worth noting by the time the client receives the snapshot, its technically already outdated/from the past.

Clients are always guessing what the future is and must use information from the server to correct their world view. But that data is from the past?! This is where rollback comes in. We set the player to where they were in the server snapshot, but then you need to quickly re-simulate all inputs on client that haven’t been processed by the server yet to push us back into the current future. In most cases this is exactly where they were, but in some unpredictable cases (e.g something exploded and shoved the player sideways but our prediction on the client didn’t know about the bomb and carried on assuming there was none) things need to be corrected.

Entity Interpolation

When the client receives snapshots from the server, it describes where and how everything is in the world around them. If the other players we see only changed positions when a snapshot came in, just jumping to the next position, it may look janky if there is a lot of latency between client and server.

To mitigate this, the other things in the world the client sees are interpolated. Fancy word but basically it takes the current snapshot and the previous one and using lerp math move the things from previous to current over a short period of time.

This makes the things in the world appear as though they move about the world smoothly, despite changes in latency or lost packets.

Nat Punchthrough

Routers/modems will block unexpected UDP packets coming in, its a security feature, but that makes connecting a client to a server annoying. We could enforce all servers have to do port forwarding OR we can use this weird work around approach.

The master server has specific ports forwarded so that anyone can join it. That way servers and clients can join it without issue. When a client tries to join a server, it tells the master server about it. The master server then informs the client of the servers IP and port address and the server is given the clients IP and port address too.

Now, both the client and server will repeatedly send packets to each other. The repetition is important, because the first packets from each will be blocked, but when you try to send a packet to a particular IP+port combo, then you train your modem/router that it should expect an incoming one from that address too. So eventually packets get through and our client and server can send data freely between each other.

Think of when you visit a website, like Youtube. You try to go to a url, a packet is sent from your computer to the url(at this moment your router is going ok, we’ll likely expect a message back from this and allow data from this address to come in), the website receives the packet and sends you a packet back containing the html or whatever for the web page.

Further Reading

If you enjoyed this topic, here are some resources I find useful when working all this stuff out: https://www.gabrielgambetta.com/client-server-game-architecture.html https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking https://gafferongames.com/tags/networking/