-
Notifications
You must be signed in to change notification settings - Fork 266
Add prediction guide #580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add prediction guide #580
Conversation
|
Very cool job! |
VerinSenpai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial review. Just some minor wording and grammar nitpicks. Its beautiful 🥺
|
Occurs to me that I reviewed this like a 500 word english essay that's due in 3 weeks. 😅 |
SlamBamActionman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Baller guide. You're the GOAT.
|
|
||
| With prediction each client runs its own game simulation of the game according to the local player's inputs and they will immediately see the results without having to wait for the server, hiding any latency. The server holds the authoritative game state, which is what all clients consider to be the "truth" in case they disagree on something. | ||
|
|
||
| During prediction the client will constantly time travel, reverting the game state repeatedly to the last known authoritative state sent by the server, and then reapplies the player's inputs, resimulating the game until the next server state comes in. This usually happens about 12 times per game tick. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Time travel" is a correct description but it kinda comes in out of nowhere, and for a beginner may seem out of left field.
I think the entire line should be reworded. It explains the process but the process isn't intuitive enough on its own. What does it mean that the client is repeatedly reverting and resimulating, and why does it do it?
| ## Prediction Checklist | ||
| Most of the steps explained above are automatically handled by the game engine for any systems in `Content.Shared`, but you will have to make sure to network your code correctly. | ||
| Follow these steps to make an existing unpredicted `EntitySystem` predicted: | ||
| - Move the relevant components and systems and from `Content.Server` to `Content.Shared`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - Move the relevant components and systems and from `Content.Server` to `Content.Shared`. | |
| - Move the relevant components and systems from `Content.Server` to `Content.Shared`. |
| Most of the steps explained above are automatically handled by the game engine for any systems in `Content.Shared`, but you will have to make sure to network your code correctly. | ||
| Follow these steps to make an existing unpredicted `EntitySystem` predicted: | ||
| - Move the relevant components and systems and from `Content.Server` to `Content.Shared`. | ||
| - Add the `NetworkedComponent` and `AutoGenerateComponentState` attributes to the components, and the `AutoNetworkedField` attribute to the datafields that the client needs to know about and that may change after spawning (if they never change from the value in the prototype there is no need to network them) or use manual component states if needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or use manual component states if needed. sounds like it should be a hyperlink to somewhere explaining what manual component states is.
| Follow these steps to make an existing unpredicted `EntitySystem` predicted: | ||
| - Move the relevant components and systems and from `Content.Server` to `Content.Shared`. | ||
| - Add the `NetworkedComponent` and `AutoGenerateComponentState` attributes to the components, and the `AutoNetworkedField` attribute to the datafields that the client needs to know about and that may change after spawning (if they never change from the value in the prototype there is no need to network them) or use manual component states if needed. | ||
| - `EntitySystem`s should either be a single non-abstract system in `Content.Shared`, or an abstract shared system with both the server and client inheriting from it. Even if the client system is empty, make sure it exists or the code won't run on the client and will not be predicted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - `EntitySystem`s should either be a single non-abstract system in `Content.Shared`, or an abstract shared system with both the server and client inheriting from it. Even if the client system is empty, make sure it exists or the code won't run on the client and will not be predicted. | |
| - `EntitySystem`s should either be a single non-abstract system in `Content.Shared`, or an abstract shared system with both the server and client inheriting from it if either of those rely on unique, non-shared code. Even if the client system is empty, make sure it exists or the code won't run on the client and will not be predicted. |
| - Move the relevant components and systems and from `Content.Server` to `Content.Shared`. | ||
| - Add the `NetworkedComponent` and `AutoGenerateComponentState` attributes to the components, and the `AutoNetworkedField` attribute to the datafields that the client needs to know about and that may change after spawning (if they never change from the value in the prototype there is no need to network them) or use manual component states if needed. | ||
| - `EntitySystem`s should either be a single non-abstract system in `Content.Shared`, or an abstract shared system with both the server and client inheriting from it. Even if the client system is empty, make sure it exists or the code won't run on the client and will not be predicted. | ||
| - To be able to make some code predicted, all its dependencies need to be made predicted first, since shared code cannot call server side code in a predicted way. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - To be able to make some code predicted, all its dependencies need to be made predicted first, since shared code cannot call server side code in a predicted way. | |
| - To be able to make some code predicted, all its dependencies need to be made predicted first, since shared code cannot call server-side code in a predicted way. |
| ## IGameTiming.ApplyingState | ||
| This returns `true` while the client either applies a game state networked from the server or rerolls its own game state to a previous one during prediction. This is commonly used in the form of a guard statement to allow server states to be applied properly and prevent code from running when it should not. | ||
|
|
||
| To understand why this is needed you have to know that some events are networked along with a components state and are always raised on both the server and client even if not predicted and they are only using `RaiseLocalEvent` and `SubscribeLocalEvent`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add "Which events do this is arbitrary and dependent on implementation."? (if I am not wrong)
This is said further down but I think giving that context is important for why a reader should care, that it may be something they run into.
| ``` | ||
|
|
||
| To prevent this from happening you have to set the datafield back to `null` if the entity is deleted somehow, however this is tricky to keep track of, requiring marker components and a lot of boilerplate code. | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is QueueEntityDeletion relevant to this section? Since that deletion happens at the end of the tick, i.e. no erroneous networking should take place?
| This is useful if you want to allow clients to modify the datafields of a component without being overwritten by the server's gamestate or keep a component unpredicted on purpose. | ||
|
|
||
| ## Predicting BUIs | ||
| `BoundUserInterfaces` usually send whatever information they need on the client using a `BoundUserInterfaceState`, which is similar to manual networking. But if we are already networking the relevant datafields then the client already has all this information available, and the BUI state is just duplicated networking. Instead we can completely remove the BUI state and just read the needed information directly from the component using a `TryComp` on the client. The userface can then be updated in an `AfterAutoHandleStateEvent` subscription (remember to activate it by setting the `raiseAfterAutoHandleState` parameter in the `AutoGenerateComponentStateAttribute`) so that it will be adjusted whenever a datafield in the relevant component is changed. If you need to update the BUI when and entity is inserted or removed from a container then you can do this with an `EntInsertedIntoContainerMessage` or `EntRemovedFromContainerMessage` subscription. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `BoundUserInterfaces` usually send whatever information they need on the client using a `BoundUserInterfaceState`, which is similar to manual networking. But if we are already networking the relevant datafields then the client already has all this information available, and the BUI state is just duplicated networking. Instead we can completely remove the BUI state and just read the needed information directly from the component using a `TryComp` on the client. The userface can then be updated in an `AfterAutoHandleStateEvent` subscription (remember to activate it by setting the `raiseAfterAutoHandleState` parameter in the `AutoGenerateComponentStateAttribute`) so that it will be adjusted whenever a datafield in the relevant component is changed. If you need to update the BUI when and entity is inserted or removed from a container then you can do this with an `EntInsertedIntoContainerMessage` or `EntRemovedFromContainerMessage` subscription. | |
| `BoundUserInterfaces` usually send whatever information they need on the client using a `BoundUserInterfaceState`, which is similar to manual networking. But if we are already networking the relevant datafields then the client already has all this information available, and the BUI state is just duplicated networking. Instead we can completely remove the BUI state and just read the needed information directly from the component using a `TryComp` on the client. The userface can then be updated in an `AfterAutoHandleStateEvent` subscription (remember to activate it by setting the `raiseAfterAutoHandleState` parameter in the `AutoGenerateComponentStateAttribute`) so that it will be adjusted whenever a datafield in the relevant component is changed. If you need to update the BUI when an entity is inserted or removed from a container then you can do this with an `EntInsertedIntoContainerMessage` or `EntRemovedFromContainerMessage` subscription. |
| ``` | ||
| Don't forget the client-side system, even if it's empty. Otherwise the client won't be able to instanciate the system class and it will remain unpredicted. | ||
|
|
||
| Avoid shared abstract components. Some ancient code is still doing this, but nowadays we just instead put the entire component into `Content.Shared`, even if some datafields are only used on the server or client. This is minimally worse for performance, but makes the code much more readable, simplifies any API methods and makes it easier to use `TryComp` and `Resolve`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good place to re-mention netSync and serverOnly here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait is serverOnly even mentioned? I think the only thing related to server-only datafields is when you imply not adding AutoNetworkedField to fields that shouldn't be networked.
Adds a guide to prediction, along with a few code examples and gifs showcasing the results.
I tried writing down everything that needs to be known, let me know if anything is wrong or unclear.
Draft until I had a few contributors and maintainers proofreading this and give feedback.
Especially the section on ApplyingState seems like it could be explained a little better, but it's a complicated topic.
ToDo in a future PR: Update the
Basic Networking and youdoc to include sections on field deltas,NetSerializableAttributeand howEntityUidsare converted intoNetEntitiesby the source generator.