Advertisement

How to have server and clients agree on new object IDs with client-side prediction and mutlthreaded updates

Started by February 11, 2018 11:21 PM
8 comments, last by hplus0603 6 years, 9 months ago

What I need is to be able to create objects/components and assign them unique IDs in both the server and clients, at the same time (so, client-side prediction), and given that objects are created on multiple worker threads (order of creation is not deterministic) in such a way that the server and clients all agree on this ID.

I want to use the ID to sync the object/component data from the server to the clients, so clearly when a new object is created they all need to (independently) come up with the same ID. But, the fact that these objects are created on multiple threads complicates things, since I cant rely on them being created in any order even within a single frame update.

How is this situation normally handled in existing games? Any ideas on how I can do this? Is there a way that doesnt rely on object IDs at all?

Thanks in advance.

How this is handled in games varies greatly.

The question is: How can the object be created on client and server AT THE SAME TIME? Generally, the reason to create objects is that the player gives some command ("fire grenade" or "build dispenser" or whatever) which means that the client knows about the new object when the command is generated, but the server doesn't know about it until it hears the command. Thus, each player could be given a range of IDs that are allocated to objects created by that player, and the player would pick an unused object in its range, and use that, and tell the server which one it used.

Another option is to use different object IDs in the client/server protocol, than you use on the server for main object persistence. Then, the player simply picks an ID, and says "object type X created by player Y on step Z has my-ID 33" and the server the updates its player/object/ID mapping table to match.

Most common, though, is that objects are only created on the server, and any temporary object that is displayed ahead of time, is simply a sham that's made real when the server sends the create-object message. If you create many objects, you can use a temporary player-side name for the object that you include in the request for the server to create the object, and the server sends this back saying "what you thought was temp-id-22 is now permanent-id-87."

enum Bool { True, False, FileNotFound };
Advertisement
1 hour ago, hplus0603 said:

The question is: How can the object be created on client and server AT THE SAME TIME? Generally, the reason to create objects is that the player gives some command ("fire grenade" or "build dispenser" or whatever) which means that the client knows about the new object when the command is generated, but the server doesn't know about it until it hears the command. Thus, each player could be given a range of IDs that are allocated to objects created by that player, and the player would pick an unused object in its range, and use that, and tell the server which one it used.

Unfortunately there are other cases where objects get created, such as by AIs or other things.

Another option is to use different object IDs in the client/server protocol, than you use on the server for main object persistence. Then, the player simply picks an ID, and says "object type X created by player Y on step Z has my-ID 33" and the server the updates its player/object/ID mapping table to match.

Yes this occurred to me, but the problem is that in order for the client to let the server know what specific object it's talking about, it basically needs the shared ID.  I mean, they both need to agree on which object the client is referring to.  And whatever information they use for that, I could have just hashed into an ID to begin with, and then both would agree to it without any explicit communication.

Most common, though, is that objects are only created on the server, and any temporary object that is displayed ahead of time, is simply a sham that's made real when the server sends the create-object message. If you create many objects, you can use a temporary player-side name for the object that you include in the request for the server to create the object, and the server sends this back saying "what you thought was temp-id-22 is now permanent-id-87."

Also thought of something like this, but I think the problem is still there... they both need to know which object is which.  For example, an AI on the client spawns 10 bullets, all identical in every way (including the transform) except they all have a script component attached that determines their behavior after being spawned.  Then it could get worse... imagine after creation other objects are created that are watching those, and maybe other objects are keeping pointers to those bullets, etc, etc.   Never underestimate the mess that a game programmer or scripter can create.  I'm not even sure how to start sorting all this out and how to have the server figure out which object is which and how to sync all of that.  

1 hour ago, 0r0d said:

there are other cases where objects get created, such as by AIs or other things

Just how far out of sync do you plan to allow the client and server to be? It's a little... unorthodox to be running full copies of the AI on the client.

Even if you are doing that, there shouldn't be any need to reconcile specific objects created by the two AIs. There's no guarantee that they'll even both decide to create objects (since the simulations are by definition out of sync). When you receive the authoritative actions performed by the server-side AI, use those and throw away everything the local AI did.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

1 hour ago, swiftcoder said:

Just how far out of sync do you plan to allow the client and server to be? It's a little... unorthodox to be running full copies of the AI on the client.

Even if you are doing that, there shouldn't be any need to reconcile specific objects created by the two AIs. There's no guarantee that they'll even both decide to create objects (since the simulations are by definition out of sync). When you receive the authoritative actions performed by the server-side AI, use those and throw away everything the local AI did.

Thanks for your input.  I'm not sure whether anything I'm planning to do is unorthodox or not, because this is the first time I've ever had to implement a networking system.  That's why I need some good input on this.

I might have a lot of independent objects, with possibly up to thousands of AIs.  So I'm hoping to keep the server and clients in sync as long as possible so I can spread out the synchronization process for things that get out of sync on the clients.  So running all the logic for the AIs on the clients only seems to make sense.  I cant be updating all those AIs and other stuff including physics from the server without client-side prediction.

So what I'd like to do, ideally, is have new objects and components being created independently and in sync, and only deal with the occasional out-of-sync situation.   If the clients and server are all creating things with IDs that they both agree to, then this would (should) work.   The idea of just doing things temporarily on the client, as far as new objects/components, scares me because of all the other dependencies that would have to get fixed up... and I dont think that keeping the entire state of the game back for even a few frames will be a viable solution.

But maybe I'm misunderstanding what your solution implies.  It just sounds like a very problematic thing to implement.

8 hours ago, 0r0d said:

But, the fact that these objects are created on multiple threads complicates things, since I cant rely on them being created in any order even within a single frame update.

There's no reason a multi-threaded update can't be just as deterministic as a single-threaded one. It helps if you do batch processing instead of "spaghetti flow" where sending a message (e.g. Calling a function) results in immediately sending and handling another message (calling another function), and so on. Alternatively if you write the results of a large batch of operations into array, then that gives you another batch to handle/process next. 

If you distribute work amongst threads by giving each a range in a batch, then you can keep their results in order (range 1's results come before range 2's results). 

Another strategy for generating unique IDs is to base them on the parent/creating object's ID, especially if you can defer assignment of IDs. e.g. At the end of a frame, once all threads are done creating new objects, use one thread to stable-sort all the new objects by the ID of the object that created them, and then assign new IDs in that order. 

In your case of an object created based on a prediction, it's ID could be derived from the user input /request that resulted in it's creation. e.g. If user fires a grenade, they send a 'shoot' message to the server, with a sequence ID attached. The server and the predicting client spawn a grenade that references this sequence ID. The client can then later match up the server's spawn message with their local proxy/prediction object, or use responses that reference higher sequence numbers to deduce that the server ignored the shoot request and didn't spawn a grenade. 

Advertisement
19 minutes ago, Hodgman said:

There's no reason a multi-threaded update can't be just as deterministic as a single-threaded one. It helps if you do batch processing instead of "spaghetti flow" where sending a message (e.g. Calling a function) results in immediately sending and handling another message (calling another function), and so on. Alternatively if you write the results of a large batch of operations into array, then that gives you another batch to handle/process next. 

If you distribute work amongst threads by giving each a range in a batch, then you can keep their results in order (range 1's results come before range 2's results). 

Another strategy for generating unique IDs is to base them on the parent/creating object's ID, especially if you can defer assignment of IDs. e.g. At the end of a frame, once all threads are done creating new objects, use one thread to stable-sort all the new objects by the ID of the object that created them, and then assign new IDs in that order. 

In your case of an object created based on a prediction, it's ID could be derived from the user input /request that resulted in it's creation. e.g. If user fires a grenade, they send a 'shoot' message to the server, with a sequence ID attached. The server and the predicting client spawn a grenade that references this sequence ID. The client can then later match up the server's spawn message with their local proxy/prediction object, or use responses that reference higher sequence numbers to deduce that the server ignored the shoot request and didn't spawn a grenade. 

I'll re-read and digest all this when I get a chance later, but I wanted to say that what I'm currently trying is the "use the creating object's ID" approach.   I'm not entirely happy with what I have now, but maybe I can get some ideas here to fix that.

I should also point out that since I havent really started on the networking stuff, the reason why I'm doing this came up because I was trying to make my engine/game deterministic just from run to run in the same machine.  Basically it's not deterministic in consecutive runs due to the random number generator.  So, ok, I figured I could create a RNG class that objects could use internally, rather than using my global mathRand() type functions, and then set the seed for that RNG class instance with the ID of the owner object, and that's when I realized that this was a problem.  No deterministic ID (because objects are created in worker threads) means no deterministic random number generation (at least with this approach I chose),  and that means no determinism at all.

So if anyone has ideas on how to better deal with generating deterministic random numbers, given the multithreading of object/AI updates, I'd love to hear them as well.

Ok, I had assumed you were building a server-authoritative game with client-side prediction, but it sounds like this is actually a fully deterministic distributed simulation. My previous post is irrelevant in that scenario.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Quote

"there are other cases where objects get created, such as by AIs or other things"

If you run a fully deterministic simulation (RTS-style,) then the server and client will come up with the same ID at the same time, just like they come up with the same battle outcome at the same time, etc.

If you don't run a fully deterministic simulation, then you probably shouldn't be creating objects on the client. Although if you really have to, then using "third sub-object created by super-object 22" as the ID (or temp-ID before permanent-Id is allocated) will work,

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement