TimonPost / laminar

A simple semi-reliable UDP protocol for multiplayer games
815 stars 66 forks source link

Implement ordering streams. #109

Closed TimonPost closed 5 years ago

TimonPost commented 5 years ago

Streams

RakNet has a nice concept of how to order packets (check out ordering streams for more info)

So what are those ordering streams?

You can think of ordering streams as something to separate the ordering of packets that have totally no relations to one and another.

So when a game-developer sends data to all the clients it might want to send some data ordered; some data unordered while other data needs to be send sequenced etc.

Let's take a look at the following data the dev might be sending:

  1. Player movement, we want to order player movement because we don't care about old positions.
  2. Bullet movement, we want to order bullet movement because we don't care about old positions of bullets.
  3. Chat messages, we want to order chat messages because it is nice to see text in the correct order.

Player movement and chat messages are totally not related to one and another. You don't want the movement packets to be disturbed if a chat messages are dropped. It would be nice if we can order player movement, chat messages separated from each other.

This is exactly where ordering streams are for. We should let the user specify on which stream their packets should be ordered. The user can, for example, say: "Let me put all chat messages on ordering stream 1 and all movement packets on ordering stream 2". This way you can have different types of packets ordered separately from each other.

Why let the user control streams?

  1. Let's take for example a player who is shooting bullets at something. It makes absolute sense to order both player movement and bullet position in the same ordering stream. You wouldn't want the shot to originate from the wrong position. So you'd put player firing packets on the same stream as movement packets (stream 1), and that if a movement packet arrived later than a firing packet but was actually sent earlier the firing packet would not be given to you until the movement packet arrived.
  2. We can't interpreter what the contents of packets are whereon we decide where to order packets

Those ordering stream should be implemented so that both reliable and unreliable channels can make use of this. If this is implemented we support all delivery methods we have in our enum.

TimonPost commented 5 years ago

I am going to implement this and allow a user to have separate streams with ordering and sequencing.

@OvermindDL1 could you please elaborate on everything know about the ordering of packets. I know what 'order' is, however, I am not sure on how to handle this internally, I spend a lot of time in RakNet figuring this out however it is a very messy code base. The question here is more about how this should be handled internally like when are the messaged returned back to the user how should they be buffered etc..

Take for example the following incoming packets: 1,3,5,4,2 for ordering. How would this be transformed into 1,2,3,4,5 for the user?

OvermindDL1 commented 5 years ago

To hold packets for ordered you generally just use a circle queue array where you put each value at each part of the array that it belongs, with the array being allocated to be the size of the network window. Anytime 'early' value are received then they are popped and handled at the relevant poll function or event callback. Thus for your 1,3,5,4,2 example this would happen:

  1. Receive '1', notice it's at the front of the queue so immediately pop off (realistically you'd remove this intermediary step anyway and just increment the queue) and put into the poller buffer or push to the event receiver.
  2. Receive '3', notice the circuluar queue starts at '2' now, so put this '3' in the second place (in C++ speak just queue[1] = packet where the index operator automatically adjusts it to the correct place in the internal array). Then do nothing else as '3' is stored awaiting for '2'.
  3. Receive '5', same as 3 put it into the queue with 2 and 4 still empty.
  4. Receive '4', same as 3 and 5, put it into the queue with 2 still empty.
  5. Receive '2', which is the front of the circuluar queue, pop each packet off the front (thus incrementing the 'id' value of the queue each time) until you hit an unfilled value, which for this example will leave the 'front' of the queue set to '6'.

I.E. It's just a simple circular queue (can be dynamically sized so it scaled well if the window size can change, but if the window's static then can make it static to save a comparison/branch) that has the 'front' defined to be an incrementing index value. It's very fast, scales well to very large window sizes, etc... etc..

OvermindDL1 commented 5 years ago

In modern rust I'd imagine you'd make a new struct that has a circular queue struct as a field inside of it as well as an unsigned integer 'front_offset' field as well. Then define the indexing operator to take what's passed in as the index, add the front_offset, then take the modulus of the size of the circular queue as the index into it. You can actually pretty easily combine these operations with the circular queue's functionality itself if you bake it all together instead of using another circular queue library (circular queues are dreadfully simple to implement, so why not).

TimonPost commented 5 years ago

Great information thanks, I'll let you know when I need more information. Due some other work related to laminar I won't start at this until over 2 weeks I think

TimonPost commented 5 years ago

I implemented packet ordering. This will be merged into laminar it'self after #138 is merged.

TimonPost commented 5 years ago

@OvermindDL1 could you review #145 when you have time?