About the network model in games for beginners

About the network model in games for beginners
For the last two weeks I have been working on the networking engine for my game. Before that, I knew nothing about networking in games at all, so I read a lot of articles and did a lot of experiments to understand all the concepts and be able to write my own networking engine.

In this guide, I'd like to share with you the various concepts you need to learn before writing your own game engine, as well as the best resources and articles for learning them.

In general, there are two main types of network architectures: peer-to-peer and client-server. In a peer-to-peer (p2p) architecture, data is transferred between any pair of connected players, while in a client-server architecture, data is transferred only between the players and the server.

Although the peer-to-peer architecture is still used in some games, client-server is the standard: it is easier to implement, requires a smaller channel width, and makes it easier to protect against cheating. Therefore, in this guide, we will focus on the client-server architecture.

In particular, we are most interested in authoritarian servers: in such systems, the server is always right. For example, if the player thinks he is at (10, 5) and the server tells him he is at (5, 3), then the client should replace its position with the one the server is reporting, not the other way around. The use of authoritative servers makes it easier to recognize cheaters.

There are three main components in gaming network systems:

  • Transport protocol: how data is transferred between clients and the server.
  • Application protocol: what is transmitted from clients to the server and from the server to clients, and in what format.
  • Application logic: how the transmitted data is used to update the state of clients and the server.

It is very important to understand the role of each part and the difficulties associated with them.

Transport protocol

The first step is to choose a protocol for transporting data between the server and clients. There are two Internet protocols for this: TCP ΠΈ UDP. But you can create your own transport protocol based on one of them or use a library that uses them.

Comparison of TCP and UDP

Both TCP and UDP are based on IP. IP allows a packet to be transmitted from a source to a receiver, but it does not guarantee that the sent packet will reach the receiver sooner or later, that it will get to it at least once, and that the sequence of packets will arrive in the correct order. Moreover, a packet can only contain a limited data size, given by the value MTU.

UDP is just a thin layer on top of IP. Hence, it has the same limitations. In contrast, TCP has many features. It provides a reliable ordered connection between two nodes with error checking. Therefore, TCP is very convenient and is used in many other protocols, for example, in HTTP, FTP ΠΈ SMTP. But all of these features come at a price: delay.

To understand why these functions can cause latency, we need to understand how TCP works. When the sending host transmits a packet to the receiving host, it expects to receive an acknowledgment (ACK). If after a certain time it does not receive it (because the packet or confirmation was lost, or for some other reason), then it sends the packet again. Moreover, TCP guarantees that packets are received in the correct order, so until a lost packet is received, all other packets cannot be processed, even if they have already been received by the receiving node.

But as you probably understand, latency in multiplayer games is very important, especially in such active genres as FPS. That is why many games use UDP with its own protocol.

A native protocol based on UDP can be more efficient than TCP for various reasons. For example, it can mark some packages as trusted and others as untrusted. Therefore, he does not care if the unreliable packet has reached the recipient. Or it can process multiple data streams so that a packet lost in one stream does not slow down other streams. For example, there might be a thread for player input and another thread for chat messages. If a chat message that is not urgent data is lost, then it will not slow down the input that is urgent. Or a proprietary protocol might implement reliability differently than TCP to be more efficient in a video game environment.

So, if TCP sucks, then we're going to build our own transport protocol based on UDP?

Everything is a little more complicated. Even though TCP is almost sub-optimal for gaming network systems, it can work quite well for your specific game and save you valuable time. For example, latency may not be an issue for a turn-based game or a game that can only be played on LAN networks, where latency and packet loss are much less than on the Internet.

Many successful games, including World of Warcraft, Minecraft, and Terraria, use TCP. However, most FPSs use their own UDP-based protocols, so we'll talk more about them below.

If you choose to use TCP then make sure it is disabled Nagle's algorithm, because it buffers packets before sending, which means it increases the delay.

To learn more about the differences between UDP and TCP in the context of multiplayer games, see Glenn Fiedler's article UDP vs. TCP.

Proprietary protocol

So you want to create your own transport protocol but don't know where to start? You're in luck, because Glenn Fiedler wrote two amazing articles about it. You will find a lot of smart ideas in them.

First article Networking for Game Programmers 2008, easier than the second Building a Game Network Protocol 2016. I recommend that you start with the older one.

Be aware that Glenn Fiedler is a big proponent of using your own protocol based on UDP. And after reading his articles, you will probably adopt his opinion that TCP has serious drawbacks in video games, and you will want to implement your own protocol.

But if you're new to networking, do yourself a favor and use TCP or a library. To successfully implement your own transport protocol, you need to learn a lot beforehand.

Network Libraries

If you need something more efficient than TCP, but don't want to bother implementing your own protocol and going into a lot of details, you can use the net library. There are a lot of them:

I haven't tried them all, but I prefer ENet because it's easy to use and reliable. In addition, it has clear documentation and a tutorial for beginners.

Transport Protocol Conclusion

To summarize, there are two main transport protocols: TCP and UDP. TCP has many useful features: reliability, preservation of packet order, error detection. UDP doesn't have all of that, but TCP, by its very nature, has high latency that is unacceptable for some games. That is, to ensure low latency, you can create your own protocol based on UDP or use a library that implements the transport protocol on UDP and is adapted for multiplayer video games.

The choice between TCP, UDP, and the library depends on several factors. First, from the needs of the game: does it need low latency? Secondly, from the requirements of the application protocol: does it need a reliable protocol? As we will see in the next part, it is possible to create an application protocol for which an unreliable protocol is quite suitable. Finally, you also need to consider the experience of the network engine developer.

I have two tips:

  • Abstract the transport protocol as much as possible from the rest of the application so that it can be easily replaced without rewriting all the code.
  • Don't over-optimize. If you are not a network expert and are not sure if you need your own UDP-based transport protocol, you can start with TCP or a library that provides reliability, and then test and measure performance. If you're having problems and you're sure it's a transport protocol, then it might be time to create your own transport protocol.

At the end of this part, I recommend that you read Introduction to Multiplayer Game Programming Brian Hook, which covers many of the topics discussed here.

Application Protocol

Now that we can exchange data between clients and the server, we need to decide what data to transfer and in what format.

The classic scheme is that clients send input or actions to the server, and the server sends the current game state to the clients.

The server sends not the full, but the filtered state with entities that are near the player. He does this for three reasons. First, the total state may be too large to transmit at a high frequency. Secondly, clients are mainly interested in visual and audio data, because most of the game logic is simulated on the game server. Thirdly, in some games the player does not need to know certain data, such as the position of the enemy on the other side of the map, because otherwise he can sniff packets and know exactly where to move to kill him.

Serialization

The first step is to convert the data we want to send (input or game state) into a format suitable for transmission. This process is called serialization.

The idea immediately comes to mind to use a human-readable format, such as JSON or XML. But this will be completely inefficient and will take up most of the channel for nothing.

Instead, it is recommended to use the binary format, which is much more compact. That is, the packets will only contain a few bytes. Here we must take into account the problem byte order, which may differ on different computers.

To serialize data, you can use a library, for example:

Just make sure the library creates portable archives and takes care of endianness.

An alternative solution would be to implement it yourself, it's not that hard, especially if you're using a data-centric approach in your code. In addition, it will allow you to perform optimizations that are not always possible when using the library.

Glenn Fiedler has written two articles on serialization: Reading and Writing Packets ΠΈ Serialization Strategies.

Compression

The amount of data transferred between clients and the server is limited by the bandwidth of the channel. Data compression will allow you to transfer more data in each snapshot, increase the refresh rate, or simply reduce the bandwidth requirements.

Bit packing

The first technique is bit packing. It consists in using exactly the number of bits that is necessary to describe the desired value. For example, if you have an enum that can have 16 different values, then instead of a whole byte (8 bits), you can use just 4 bits.

Glenn Fiedler explains how to implement this in the second part of the article. Reading and Writing Packets.

Bit packing works particularly well with discretization, which will be the topic of the next section.

Sampling

Sampling is a lossy compression technique that uses only a subset of possible values ​​to encode a value. The easiest way to implement discretization is by rounding floating-point numbers.

Glenn Fiedler (again!) shows how to apply discretization in practice in his article Snapshot Compression.

Compression algorithms

The next technique will be lossless compression algorithms.

Here, in my opinion, are the three most interesting algorithms that you need to know:

  • Huffman coding with precomputed code, which is extremely fast and can produce good results. It was used to compress packets in the Quake3 network engine.
  • zlib is a general purpose compression algorithm that never increases the amount of data. How can you see here, it has been used in a variety of applications. For updating states, it may be redundant. But it can come in handy if you need to send assets, long texts or terrain to clients from the server.
  • Copying run lengths is probably the simplest compression algorithm, but it is very efficient for certain types of data, and can be used as a pre-processing step before zlib. It is especially suitable for compressing terrain consisting of tiles or voxels in which many neighboring elements are repeated.

delta compression

The final compression technique is delta compression. It lies in the fact that only the differences between the current game state and the last state received by the client are transmitted.

It was first used in the Quake3 network engine. Here are two articles explaining how to use it:

Glenn Fiedler also used it in the second part of his article. Snapshot Compression.

Encryption

In addition, you may need to encrypt the transmission of information between clients and the server. There are several reasons for this:

  • Privacy/Confidentiality: Messages can only be read by the recipient and no other network sniffer will be able to read them.
  • authentication: a person who wants to play the role of a player must know his key.
  • cheat prevention: it will be much more difficult for malicious players to create their own cheat packages, they will have to replicate the encryption scheme and find the key (which changes on each connection).

I strongly recommend using a library for this. I suggest using libsodium, because it's especially simple and has great tutorials. Of particular interest is the tutorial on key exchange, which allows you to generate new keys on each new connection.

Application Protocol: Conclusion

This concludes the application protocol. I believe that compression is completely optional and the decision to use it depends only on the game and the required bandwidth. Encryption, in my opinion, is mandatory, but in the first prototype you can do without it.

Application Logic

We are now able to update the state in the client, but we may run into latency issues. The player, after making an input, needs to wait for a game state update from the server to see what effect it has had on the world.

Moreover, between two state updates, the world is completely static. If the state update rate is low, then the movements will be very jerky.

There are several techniques to mitigate the impact of this problem, and I will discuss them in the next section.

Delay Smoothing Techniques

All the techniques described in this section are discussed in detail in the series. Fast Paced Multiplayer Gabriel Gambetta. I highly recommend reading this excellent series of articles. It also includes an interactive demo to see how these techniques work in practice.

The first technique is to apply the input result directly without waiting for a response from the server. It is called client-side prediction. However, when the client receives an update from the server, it must verify that its prediction was correct. If this is not the case, then he just needs to change his state according to what he received from the server, because the server is authoritarian. This technique was first used in Quake. You can read more about it in the article. Quake Engine code review Fabien Sanglars [translation on HabrΓ©].

The second set of techniques is used to smooth the movement of other entities between two state updates. There are two ways to solve this problem: interpolation and extrapolation. In the case of interpolation, the last two states are taken and the transition from one to the other is shown. Its disadvantage is that it causes a small fraction of the delay, because the client always sees what happened in the past. Extrapolation is about predicting where the entities should be now based on the last state received by the client. Its disadvantage is that if the entity completely changes the direction of movement, then there will be a large error between the forecast and the real position.

The last, most advanced technique, useful only in FPS, is lag compensation. When using lag compensation, the server takes into account the delays of the client when it fires at the target. For example, if a player performed a headshot on their screen, but in reality their target was in a different location due to the delay, then it would be unfair to deny the player the right to kill due to the delay. So the server rewinds time back to when the player fired to simulate what the player saw on their screen and check for a collision between their shot and the target.

Glenn Fiedler (as always!) wrote an article in 2004 Network Physics (2004), in which he laid the foundation for the synchronization of physics simulations between the server and the client. In 2014 he wrote a new series of articles networking physics, in which he described other techniques for synchronizing physics simulations.

There are also two articles on Valve's wiki, Source Multiplayer Networking ΠΈ Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization dealing with delay compensation.

Cheat Prevention

There are two main cheating prevention techniques.

First, making it harder for cheaters to send malicious packets. As mentioned above, a good way to implement it is encryption.

Second, the authoritative server should only receive commands/input/actions. The client should not be able to change the state on the server other than by sending input. Then the server, each time it receives input, must check it for validity before applying it.

Application Logic: Conclusion

I recommend that you implement a way to simulate high latency and low refresh rates so that you can test the behavior of your game in bad conditions, even when the client and server are running on the same machine. This greatly simplifies the implementation of delay smoothing techniques.

Other Helpful Resources

If you want to explore other network model resources, you can find them here:

Source: habr.com

Add a comment