Advertisement

How to best optimise networking of many objects with brief lifetimes.

Started by December 31, 2018 11:11 PM
10 comments, last by hplus0603 5 years, 10 months ago

Hello.

I'm working on a project to teach myself network programming and the little game I have in mind is a coop arena shooter.  A fast moving game, lots of simple enemies and projectiles frequently spawning and dying. I've been reading what I can find about synchronising the game state (snapshots, delta compression, update prioritisation), but this mostly relates to objects that already exist, not how to deal with a high volume of new objects coming and going.

Does anyone have any suggestions/advice relating to this kind of problem?

My thoughts so far are based on using what determinism I can and playing the game back as as sequence of events as best I can. Eg if I want to spawn a spread of a dozen projectiles I can reconstruct all of them from the initial condition of the first plus a random seed or index to some kind of predefined pattern that might deterministically recreate the whole group. This combined with trying to do as much movement as possible via paths that can be interpolated (so a spawn event can be played up to the right position on the client when received and just played forward with every tick without direct synchronisation except in the case of death) so even if the event is received late it can be easily wound forward

If I'm barking up the wrong alley here I'd love to know! Thanks.

A "new object created" isn't that different from an important event like "player X shot a bullet."

Typically, for important messages, you will stuff the message into each UDP packet you send, until you get an acknowledgement from the other side for a UDP packet that you know contained that message. Then the receiving side will de-duplicate multiple copies of the same message (not just same UDP datagram!) to make that work out.

Typically, you will have serial numbers for messages, and when you first send a message, tag it with a serial number, and re-use that for each copy; that way the receiving end can know whether a particular message is handled or not.

enum Bool { True, False, FileNotFound };
Advertisement
On 12/31/2018 at 11:46 PM, hplus0603 said:

Typically, you will have serial numbers for messages, and when you first send a message, tag it with a serial number, and re-use that for each copy; that way the receiving end can know whether a particular message is handled or not.

Would this serial number effectively be another sequence number? Seems like you wouldn't want such events to be out of order (eg a spawn being read after it's own death, given how fast things are moving) ?

Yes, that's what serial numbers are.  Serial numbers are serial.  A serial number tells you the position in a series.

A common pattern is to track the current item number; if you receive something older than current you discard it. If you receive something newer than current you capture it, holding it until you reach there (or run low on buffer space). And if you receive the next item in the series, you process it and advance the current item number.

Basically you're implementing your own sliding window protocol rather than relying on the one in TCP.  While TCP is normally wonderful, the retransmit time is problematic for fast-paced networking. Generally the retransmission delay is 3 seconds before the retry, then doubling to six more seconds, then double again to twelve more seconds, then give up.  It can be configured on the operating system, newer Linux systems often use a value depending on previous round trip times but no less than one second, and on Windows there are registry values you can tweak to modify retry times and retry attempts.  But 3 seconds is a LONG time for an action game.

Games that are not bandwidth limited will usually implement their own that retransmits almost immediately.  For example, something like this:

send -> {A}
send -> {A, B}
send -> {A, B, C}
recv <- {Ack A}
send -> { B, C }
send -> { B, C, D }
recv <- { Ack C }
send -> { D }
send -> { D, E }
...

The code may have longer delays before retransmit, or retransmit less frequently based on bandwidth, round trip times, and other factors you can tune to your game to give you a good signal-to-noise ratio.  Patterns change over time as bandwith and latency needs change.  The patterns used in 1997 are different than the patterns used in 2003 or in 2019 because typical player bandwith and latency conditions have changed.  As networks continue to evolve, patterns will evolve as well.

Yes, it's effectively another sequence number. However, you can have many separate sequence series -- for example, "projectiles spawned by player X" don't need to be in-sequence with "projectiles spawned by player Y" (depending on gameplay and simulation specifics.)

Also, for sequence numbers, you can often get away with only sending the lowest byte, or the lowest two bytes, of the sequence number, because you'll know when it's supposed to "wrap over" -- if you drop a client if it hasn't sent you 30 packets in a row, and you can't create more than 4 entities per packet, then you can't create more than 120 entities in a packet outage time, and thus a single byte is enough to unambiguously keep a sequence number in sync. (If this doesn't make sense, go look at the bit representation of unsigned integers keeping serial numbers, and what happens as they increment -- very important feature!)

enum Bool { True, False, FileNotFound };
4 hours ago, frob said:

Yes, that's what serial numbers are.  Serial numbers are serial.  A serial number tells you the position in a series.

Fair enough :)

Quote

Patterns change over time as bandwith and latency needs change.

I understand there is the UDP MTU which dictates the maximum safe size for a single packet, but how many packets would generally be sent. Is it generally one per client per server tick okay?

Quote

(If this doesn't make sense, go look at the bit representation of unsigned integers keeping serial numbers, and what happens as they increment -- very important feature!)

Interesting. I'll look into that.

Advertisement

UDP and TCP don't know about MTU; that's an underlying IP thing. And fragmentation isn't bad these days; the network stack will fragment for you and reassemble at the other end, both for IPv4 and IPv6. (for v6, the network implementation in the kernel is responsible for detecting and fragmenting; in v4, any router along the path can do this.)

The maximum UDP packet size is 65535 bytes, so don't send a datagram bigger than that!

Most games send packets anywhere at the even-multiples-of-60 rate, with multiple "steps" bunched into a single packet. I e, common send frequencies are 10, 12, 15, 20, 30, and 60 Hz.

enum Bool { True, False, FileNotFound };

When you say multiple steps are you referring to the frame updates the server has run?

As I understand so far I need a queue of events each with a serial number and frame number to play back on the client, with events transmitted redundantly until acknowledgment?


As Sir jwatte suggested piggyback all your data into a single packet, assuming you already have gathered 10 events data or whatever network data you need for your game sample as listed below:

[1data]
[2data]
[3data]
[4data]
[5data]
[6data]
[7data]
[8data]
[9data]
[10data]

Construct your datagram that can piggyback all of your available network  data that will fit on your desired send buffer size. If you set your sending frequency to 33ms which is roughly 30x a seconds, those 10 data can be send only in 5 iteration or less on a 33ms frequencies (depends on data size); send only after receiving an ACK from the other endpoints if your data setting is reliable and in order.

E.G:

// Only serial 1,2,3,4 data will fit on the send buffer size.
//
send { HEADERINFO <ACK No. 301 ,etc..> |  [1data] [2data] [3data] [4data] | [Filler] }    

.. did i received an acknowledgement for ACK No. 301 and elapse time on last send is > 33ms ?

// Only serial 5 and 6 data will fit on the send buffer size.
//
send { HEADERINFO <ACK ID 302 .,etc..> | [5data] [6data]  [Filler] }    

.. did i received an acknowledgement for ACK No. 302 and elapse time on last send is > 33ms ?

// Only serial 7,8,9 data will fit on the send buffer size.
//
send { HEADERINFO <ACK ID 303.,etc..> | [7data] [8data] [9data] [Filler] }    

.. did i received an acknowledgement for ACK No. 303 and elapse time on last send is > 33ms ?

// Serail No. 10 data is too big to fit on send buffer you may split it up
// and reconstruct at the other end if received the whole part.
//
send { HEADERINFO <ACK ID 304 ,etc..> | [ 10data|Part1/2 ] [Filler] }    

.. did i received an acknowledgement for ACK No. 304|1 and elapse time on last send is > 33ms ?

// Send the other half
//
send { HEADERINFO <ACK ID 305 ,etc..> | [ 10data|Part2/2 ] [Filler] }  


^_^Y Cheers

hmm, okay. I will have a go at implementing something like that and see

Thanks!

This topic is closed to new replies.

Advertisement