Unity-Technologies / com.unity.netcode.gameobjects

Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.
MIT License
2.16k stars 435 forks source link

AnticipatedNetworkTransform 🫣 and RigidBody #2883

Open Maclay74 opened 7 months ago

Maclay74 commented 7 months ago

Hey, I know it's not released yet, but I decided to play with it, since it's merged to develop branch anyway

I read documentation from component's source code and it says that there are three ways it will handle anticipation. I'm not sure I understand how to switch between them, as they seem more like concepts of usage.

So I thought I could use it for very basic client-side prediction. I placed the component on a character and ran movement code on both client and server via NetworkVariables.

Well, the problem is, on server everything is butter smooth, whereas client jitters as it fails to predict state constantly and re-anticipate every frame.

I use physically-based movement, can it be a problem?

I can prepare a simple example project, if you will

Thanks.

P.S. For me personally, this "anticipation" seems to be the latest piece of the puzzle to make NGO the best networking solution out there. Greatly appreciate that.

NoelStephensUnity commented 7 months ago

@Maclay74 A simple example of your implementation would be the fastest way for us to help you prior to releasing v1.9.1. We do plan on having some samples that will follow the release, but would be more than happy to look over your implementation and provide any adjustments/pointers.

ShadauxCat commented 7 months ago

Hi @Maclay74

There is a very basic example of how to use AnticipatedNetworkTransform to handle client-side-controlled movement here: https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize/tree/main/Experimental/Anticipation%20Sample

Be aware, though, that this is a very basic example and does not fully handle all networking edge cases and race conditions. It's more of a tech demo than a fully realized prediction solution.

Anticipation is just one building block of client prediction. To realize a full prediction implementation, you need:

The example I linked shows interpolation and smoothing, and a very basic implementation of latency compensation and input handling, but I want to stress that that code is not ready for production use and should not be copy/pasted exactly as it is. A full prediction system would need to resimulate the entire game one frame at a time all together to handle interactions between objects and so on.

Client Anticipation does provide the scaffolding to enable you to implement all of that, but only the scaffolding... a lot more is still required to turn it into a full prediction system.

Hope that helps!

Maclay74 commented 7 months ago

Hey @NoelStephensUnity and @ShadauxCat! Thanks for your replies! And thanks for this list of things to remember, it definitely helps.

Client Anticipation does provide the scaffolding to enable you to implement all of that, but only the scaffolding... a lot more is still required to turn it into a full prediction system.

Not sure if you can answer this, but do you plan to develop it further into a prediction system or it was intended to be a framework for developers?

The example you provided shows usage of the feature very clear, although I don't think I understand how to handle physically-based movement.

I apply forces to player's rigidbody, which moves is gameobject consequently. It's clear that I'd have to apply forces on both sides, but I don't think I understand what I need to pass to AnticipatedMove method.

The only thing I can think of is triggering physics step manually and passing the difference, is that the only way?

Maclay74 commented 6 months ago

Hey @ShadauxCat any update on that matter?

Agoxandr commented 6 months ago

I implemented this for transforms and it works pretty well, but the moment rigidbodies are involved that can move in arbitrary ways it becomes very difficult. My best result with rigidbodies is glitchy, rubberbanding and randomly teleporting. Changing ownership can also be a huge issue.

sjordan8 commented 5 months ago

My biggest issue with rigidbodies in NGO is the fact that FixedUpdate and NetworkTickSystem.Tick do not always execute on the same frame even if your NetworkTime.FixedDeltaTime and Time.fixedDeltaTime are the same.

If they do happen to line up on the same frame, NetworkTickSystem.Tick will occur after the FixedUpdate (in PreUpdate) which causes it to be off by at least one (more if there are multiple calls to FixedUpdate in a single frame)

Maclay74 commented 5 months ago

@sjordan8 it looks like rigidbodies would require some internal changes to make it play nicely with multiplayer. There are some new API in Unity 6 related to simulation, I hope this will bear fruit.

NoelStephensUnity commented 5 months ago

@sjordan8 @Maclay74 Maclay74 is correct. A side note: the pre-release (release\2.0.0-pre.1) is in the process of submission so you an opt to use that branch until it is public in package manager (you will need to enable viewing pre-released packages to see it once it is officially submitted).

There is a bit of documentation that I need to put together for using NetworkRigidbody with NetworkTransform, but basically:

Either case, a lot of the issues you might be encountering while using a NetworkRigidbody and NetworkTransform in v1.x of NGO have been resolved and improved in v2.0.0.

sjordan8 commented 5 months ago

Thanks for the reply and it seems like this is exactly what I was looking for! I just have a couple questions

  • NetworkTransform will then be updated during the NetworkUpdateStage.FixedUpdate stage on the non-authoritative side which assures it will always be invoked at least once per frame.

From my testing on ngo/v1.8.1 it seems like NetworkUpdateStage.FixedUpdate only executes on a frame where a FixedUpdate is scheduled to execute, has that changed?

  • The PreUpdate stage (when the NetworkTick occurs) actually happens after the FixedUpdate stage, so any updates (beyond the threshold) to the Rigidbody transform for that frame on the authority side will be detected and state updates queued to be sent at the end of the frame.

    • It is "ok" for the non-authority to run 1 frame behind the time update (i.e. time updates after FixedUpdate) since non-authority interpolation is always running at least 1-2 ticks behind the authority anyways.

My issue is that I use NetworkTransformState's position and tick data for player reconciliation on non-authoritative clients. Because NetworkTransform sends position data OnNetworkTick it seems that I can end up in situations like the one below where NetworkTransform sends the incorrect position of the Rigidbody for that tick because FixedUpdate hasn't occurred yet which causes a reconcile on the client:

Frame 1 ...
EarlyUpdate() <-- Tick = 1
PreUpdate() <-- Tick = 2
Update() <-- Tick = 2
...

Frame 2 ...
EarlyUpdate() <-- Tick = 2
FixedUpdate() <-- Tick = 2 PreUpdate() <-- Tick = 2
Update() <-- Tick = 2
...

It seems like this happens pretty often which causes reconciles very often. It'd be nice to have a "Physics Setting" on the NetworkManager which invokes NetworkManager.NetworkTickSystem.Tick during NetworkUpdate's FixedUpdate phase. Not sure how hard that would be to implement though, I think you would have to change NetworkTickSystem to only execute one tick at a time and NetworkTimeSystem could only increment time in FixedUpdate.

There are also some workarounds, the way that I fixed this was by changing FixedUpdate to be handled via script in the project settings and I manually call Physics.Simulate(NetworkManager.LocalTime.FixedDeltaTime); during the NetworkTick event. I think you can also lock the frame rate to the tick rate, that would probably work too.

This would be nice to use with Animator and InputSystem's FixedUpdate modes.

NoelStephensUnity commented 5 months ago

@sjordan8 Since you are using NGO v1.8.1, then you might be experiencing the "tug-of-war" between NetworkTransform applying updates during the MonoBehaviour.Update and applying them directly to the Unity transform and not the Rigidbody/PhysX transform.

The network tick event happens when the NetworkTimeSystem.UpdateTime() is invoked during the PreUpdate which happens after FixedUpdate. So, it is understandable that it could seem like there is some kind of tick relative/timing issue... but in reality it has more to do with what values are being checked, when they are being applied, and what they are being applied to:

So what really is happening (relative to the two frames you provided):

NGO v1.x


(Authority) Frame (tick: n) ... EarlyUpdate() <-- Tick = n FixedUpdate() <-- Tick = n PreUpdate() <-- Tick = n+1 (checks for state updates and if any sends them for tick n+1) Update() <-- Tick = n+1 ...

(Non-Authority) Frame (tick: n) not realistic as non-authority really is n-1 but for simplicity sake ... EarlyUpdate() <-- Tick = n --> Receives and processes state updates for n+1 (does not interpolate towards this yet) FixedUpdate() <-- Tick = n --> Does nothing (it is kinematic) PreUpdate() <-- Tick = n + 1 --> Receives tick update and increments tick Update() <-- Tick = n + 1 --> if done with any previous (n) state update, then interpolates towards n+1 state update (applying to GameObject.transform). Rigidbody.position and rotation are out of sync with GameObject.transform. ... (Non-Authority) Next Frame (tick: n+1) ... EarlyUpdate() <-- Tick = n + 1 FixedUpdate() <-- Tick = n +1 --> synchronizes with GameObject.transform update from previous frame PreUpdate() <-- Tick = n + 1 --> no new tick updates Update() <-- Tick = n + 1 --> Continues interpolating (GameObject.transform) towards n + 1 state update. GameObject.transform and Rigidbody (position/rotation) continue to be out of synch until next frame when FixedUpdate occurs. ...


There are a few issues with the above, but two important ones are:

  1. Applying state updates to GameObject.transform end up being problematic for kinematic bodies and collisions.
  2. The GameObject.transform leads the Rigidbody/PhysX transform (which it should be the other way around). This doesn't get corrected until the next frame.

NGO v2.0.0


(Authority) Frame (tick: n) ... EarlyUpdate() <-- Tick = n FixedUpdate() <-- Tick = n PreUpdate() <-- Tick = n+1 (checks for state updates and if any sends them for tick n+1) Update() <-- Tick = n+1 ...

(Non-Authority) Frame (tick: n) not realistic as non-authority really is n-1 but for simplicity sake ... EarlyUpdate() <-- Tick = n --> Receives and processes state updates for n+1 (added to Buffer interpolator) FixedUpdate() <-- Tick = n --> Continues any interpolation towards any state update for tick (n) PreUpdate() <-- Tick = n + 1 --> Receives tick update and increments tick Update() <-- Tick = n + 1 --> (Rigidbody and GameObject are in sync) ...

(Non-Authority) Next Frame (tick: n + 1) ... EarlyUpdate() <-- Tick = n + 1 FixedUpdate() <-- Tick = n + 1 --> If done with n state update, then starts interpolating towards n+1 update PreUpdate() <-- Tick = n + 1 --> no new tick updates Update() <-- Tick = n + 1 --> (Rigidbody and GameObject are in sync) ...

(Non-Authority) Next Frame (tick: n + 1) ... EarlyUpdate() <-- Tick = n + 1 FixedUpdate() <-- Tick = n + 1 --> Continues interpolating towards n+1 update PreUpdate() <-- Tick = n + 1 --> no new tick updates Update() <-- Tick = n + 1 --> (Rigidbody and GameObject are in sync) ...


You might just try creating a separate branch/copy of your project and update to Unity 6 (pre-release) and to the NGO v2.0.0 pre-release to see if that resolves your timing issue(s) and then decide which path is best for your project's needs.

sjordan8 commented 5 months ago

@NoelStephensUnity Thank you again for the reply and the in depth explanation, the frame by frame really helps a lot.

Although now after reading your reply I realize that I'm talking about a different issue specifically related to client prediction/reconciliation which I'm pretty sure isn't fully supported yet but I'm going to explain it anyways in case someone else runs into this problem and comes across this post.

It was present on both host and client for the versions I tested (1.8.1 and (release\2.0.0-pre.1) )

The issue is that FixedUpdate and the Tick event (in PreUpdate) don't always occur in the same order and this causes incorrect position state to be sent to non-authoritative clients which usually causes a reconcile

NGO v1.8.1

(Authority) Frame (Tick: n) ... EarlyUpdate() <-- Tick = n

PreUpdate() <-- Tick = n + 1 --> Tick updated, NetworkTransform sends data for Tick n+1 without processing Rigidbody.MovePosition() during Tick n, essentially skipping a tick Update() <-- Tick = n + 1 ... (Authority) Frame+1 (Tick: n+1) ... EarlyUpdate() <-- Tick = n+1 FixedUpdate() <-- Tick = n + 1 --> FixedUpdate executes a frame late, Rigidbody.MovePosition() is processed for Tick n+1 PreUpdate() <-- Tick = n + 1 --> No tick update Update() <-- Tick = n + 1 ...

...(Authority) Frame+2 (tick: n+1)... <-- No tick updates, no FixedUpdate calls

(Authority) Frame+3 (Tick: n+1) ... EarlyUpdate() <-- Tick = n + 1 FixedUpdate() <-- Tick = n + 1 --> FixedUpdate executes a second time for Tick n+1, PreUpdate() <-- Tick = n + 2 --> Tick updated, NetworkTransform sends data for Tick = n+2 Update() <-- Tick = n + 2 ...

...(Authority) Frame+4 (Tick: n+2)...<-- No tick update, no FixedUpdate call

(Authority) Frame+5 (Tick: n+2) ... EarlyUpdate() <-- Tick = n+2

PreUpdate() <-- Tick = n+3 --> Tick updated, NetworkTransform sends data for Tick n+3, doesn't process Rigidbody.MovePosition() during Tick n+2, skipping it Update() <-- Tick = n+3 ...

(Non-Authority Owner) Frame (Tick: n+2) ... EarlyUpdate() <-- Tick = n +2 --> Receives data from authority for Tick n+1, this causes a reconcile because the authority calculated the position incorrectly FixedUpdate() <-- Tick = n + 2 --> FixedUpdate executes, Rigidbody.MovePosition() is processed locally for Tick n+2 PreUpdate() <-- Tick = n + 3 --> Tick updated Update() <-- Tick = n + 3 ... (Non-Authority Owner) Frame+1 (Tick: n+3) ... EarlyUpdate() <-- Tick = n+3 --> Receives data from authority for Tick n+2, this position is correct but it still causes a second reconcile because our local position is incorrect due to a false reconcile last frame FixedUpdate() <-- Tick = n+3 --> FixedUpdate executes, Rigidbody.MovePosition() is processed locally for Tick n+3 PreUpdate() <-- Tick = n+4 --> Tick updated Update() <-- Tick = n+4 ... (Non-Authority Owner) Frame+2 (Tick: n+4) ... EarlyUpdate() <-- Tick = n+4 --> Receives data from authority for Tick n+3, this position is incorrect, reconcile locally FixedUpdate() <-- Tick = n+4 --> FixedUpdate executes, Rigidbody.MovePosition() is processed locally for Tick n+4 PreUpdate() <-- Tick = n+5 --> Tick updated Update() <-- Tick = n+5 ...

That's generally how the pattern works on both client and host/server. The issue is the inconsistent timing of FixedUpdate messing with a tick based reconcile or a server rollback for something like hit detection. It's still a problem even if you use a CharacterController instead of a Rigidbody.

NoelStephensUnity commented 5 months ago

@sjordan8 Ahhh... so you are probably running at a much higher frame rate (render) than the physics frame rate.

So, you have the choice of locking into a fixed frame rate (i.e. 60 fps or even 50fps) where physics typically runs at 50fps and you will still end up with a slightly skewed ratio between the two but it will be more aligned to almost a 1:1 like you are looking for.

Not having seen how you have implemented your prediction/reconciliation code, makes it a bit challenging to provide a precise answer.

Out of curiosity, have you tried adjusting your NetworkManger's TickRate to 50 (i.e the same general time frame as the physics system)?

If that doesn't work, then you might need to create a more customized version (i.e. fork from NGO) where you can control when the tick event is triggered to better match your prediction/reconciliation approach.

sjordan8 commented 5 months ago

Yeah locking the framerate to 50fps fixes the issue completely.

Out of curiosity, have you tried adjusting your NetworkManger's TickRate to 50 (i.e the same general time frame as the physics system)?

I've always kept NetworkManager's TIckRate and Time.fixedDeltaTime the same at 50 and 0.02s respectively. That's why I was wondering why they weren't lining up.

Thanks again for the help!