Lidgren.Network – explaining NetDeliveryMethod and sequence channels
Last week I posted a little tutorial on how to get get started with Lidgren.Network, the popular C# networking library. We covered the basics on how to set up a connection and how to send and receive messages.
Today I want to cover another useful feature of the library.
We will first discuss Lidgren’s usage of UDP, and then look into delivery methods and sequence channels, which allow us to send messages with different sorts of guarantees for arrival and ordering.
Why UDP?
To make optimal use of Lidgren.Network, we have to know that it uses the User Datagram Protocol (UDP) to send all its network packages. UDP is a transport layer protocol which, unlike the ubiquitous Transmission Control Protocol (TCP) provides very little reliability.
With the complexity of network architecture, especially considering intercontinental internet connections, there are a number of things that can go wrong:
- UDP packages can be lost, if any network node between source and destination fails to transmit it;
- UDP packages can be duplicated and arrive more than once at the destination;
- multiple UDP packages sent to the same destination can arrive in a different order than they were sent in;
- UDP provides no congestion control: If too much data is sent too quickly, some of it it bound to be lost in transit, as network nodes do not keep arbitrarily long queues of packages.
It is easy to see how any of these faults can result in a variety of undefined behaviour, from players in an FPS missing the message that they were killed, to units in an RTS game being built twice, or being destroyed before they are built.
Clearly, something needs to be done.
If we look at the aforementioned TCP, it looks like we have found the solution. TCP guarantees that data is never lost, duplicated, or arrives in the wrong order, and it also provides congestion control.
However, while TCP is a great protocol when it comes to browsing the web, downloading files, and many other applications, for real-time games, using TCP is almost always a bad idea. For a very in-depth explanation for why that is the case, I refer to this post by Glenn Fiedler.
Lidgren’s NetDeliveryMethod
But if we use UDP, what can we do about these problems?
First, it is important to know that in practise things are rarely as bad as I made it seem above. As long as the network is not congested, UDP is very reliable. And if it is not, Lidgren.Network can take care of things for us.
In fact, the library already takes care of duplicate packages by discarding all but the first of them in any case. Further, when sending a message, we can ask it to make sure that our message will arrive, which will make it resend that package if it needs to. We can also ask it to guarantee that our messages will not arrive out of order, which will make it ignore old messages that arrive too late.
If we combine the two, we get a fully reliable method of transmitting messages, where all our messages will arrive, and will do so in order. In that case, the library will keep track of all received messages and only let us know about them once all previous messages have been received as well.
The way we use these features is by specifying the correct NetDeliveryMethod
for the method
parameter of all SendMessage
calls.
The valid values are:
Unreliable
No guarantees, except for preventing duplicates.UnreliableSequenced
Late messages will be dropped if newer ones were already received.ReliableUnordered
All packages will arrive, but not necessarily in the same order.ReliableSequenced
All packages will arrive, but late ones will be dropped.
This means that we will always receive the latest message eventually, but may miss older ones.ReliableOrdered
All packages will arrive, and they will do so in the same order.
Unlike all the other methods, here the library will hold back messages until all previous ones are received, before handing them to us.
Given these possibilities, why would we ever send a message with one of the less reliable methods?
There are two answers to this question.
First, in real-time games there often is a lot of information that will be transmitted over the network that will be useless after even just a split second, and certainly once more up-to-date information is received. This includes for example player positions in most cases.
In fact, with that kind of information, it is not critical that all updates are received, just as long as enough are to allow for interpolation and prediction of smooth movement.
Second, if we insist on all messages arriving, we may cause a lot of unnecessary network usage, which can cause congestion. If we send more than the connection can handle, it will not be able to catch up with the newest packages. The delay between the actual game and the received network messages would grow ever larger until the game becomes unplayable.
If we however only insist on the arrival of critical messages, this is much less likely to happen. Even if a few – or many – less important messages are lost here and there, the important ones are still likely to be received in reasonable time.
Overall, it is thus advisable to choose a delivery method as unreliable as possible, but as reliable as necessary.
Sequence Channels
When looking at the different overloads of SendMessage
, there is yet another parameter we have not looked at: int sequenceChannel
This parameter can be used to specify a channel for the net delivery methods that preserve order (whether by holding back, or dropping messages received out of order). Any messages sent with the same channel will respect each other’s order, while messages sent with different channels will not interfere with each other.
This can be useful to not have unrelated kinds of messages interfere with each other. For example, player health point and position updates should probably not be send using the same channel, since they are in essence independent of each other, and we care about receiving up-to-date information of both.
Note that the number of channels you can use is limited to 32, as specified in the internal NetConstants.NetChannelsPerDeliveryMethod
constant. While you can of course change this number in principle, if you need that many channels, you may want to consider simplifying or redesigning your protocol.
Conclusion
I hope this post gives a good overview of how to use Lidgren.Network’s NetDeliveryMethod
and channels to prioritise and control how different kinds of messages are sent.
How to best use these features will of course depend very much on the respective application or game.
Feel free to let me know if this post has been helpful to you, or if you have any questions or other thoughts on the subject, in the comments.
Enjoy the pixels!
Reference: | Lidgren.Network – explaining NetDeliveryMethod and sequence channels from our NCG partner Paul Scharf at the GameDev<T> blog. |