Announcement

Collapse
No announcement yet.

[Networking] Interpolating replicated movement data in Pawn, without modifying source

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

    [Networking] Interpolating replicated movement data in Pawn, without modifying source

    Hey guys,

    When I play my simple space shooter with a dedicated server (or as a client connected to a listen), the desync between keypress and replicated movement info is rather jarring (if the ship is rolling, then you stop rolling, the ship will jitter into a slightly different position, but not if listen / standalone). This occurs even on the simulated UE4 editor server, and cross-PC.

    When a key is pressed (for pitch, roll, yaw, and moving in all axes), the event is run on the server, which adds forces and torques to the pawn. A local copy is also run, so the client receives immediate feedback.

    Click image for larger version

Name:	073e30dbe9a36c1ff82bcd25fb1c4814.png
Views:	1
Size:	177.9 KB
ID:	1136643

    I'm trying to interpolate between the client's physical model and the returning server variables with the following code, in my Ship class, which inherits from Pawn.

    Code:
    void AShip::Tick(float DeltaSeconds)
    {
            FVector PreTickPos;
            FRotator PreTickRot;
            FVector PreTickVel;
            FVector PreTickAngVel;
     
            if (IsLocallyControlled())
            {
                    PreTickPos = GetActorLocation();
                    PreTickRot = GetActorRotation();
                    PreTickVel = MeshComponent->GetPhysicsLinearVelocity();
                    PreTickAngVel = MeshComponent->GetPhysicsAngularVelocity();
            }
     
            // Update AActor, including replication of movement.
            Super::Tick(DeltaSeconds);
     
            if (IsLocallyControlled())
            {
                    // Replicated movement comes from AActor, updated on Tick(), and only replicated when "Replicate Movement" ticked.
     
                    FVector ReplicatedPos = ReplicatedMovement.Location;
                    FRotator ReplicatedRot = ReplicatedMovement.Rotation;
                    FVector ReplicatedVelocity = ReplicatedMovement.LinearVelocity;
                    FVector ReplicatedAngVelocity = ReplicatedMovement.AngularVelocity;
                   
                    // Current, Target, DeltaTime, Speed
                    FVector LerpPos = FMath::VInterpConstantTo(PreTickPos, ReplicatedPos, DeltaSeconds, 0.001f);
                    FRotator LerpRot = FMath::RInterpConstantTo(PreTickRot, ReplicatedRot, DeltaSeconds, 0.001f);
                    FVector LerpVelocity = FMath::VInterpConstantTo(PreTickVel, ReplicatedVelocity, DeltaSeconds, 0.001f);
                    FVector LerpAngVelocity = FMath::VInterpConstantTo(PreTickAngVel, ReplicatedAngVelocity, DeltaSeconds, 0.001f);
     
                    SetActorLocation(LerpPos);
                    SetActorRotation(LerpRot);
                    MeshComponent->SetPhysicsLinearVelocity(LerpVelocity);
                    MeshComponent->SetAllPhysicsAngularVelocity(LerpAngVelocity);
            }
    }
    The problem with the above is that the client will only run its physical simulation on Tick, which is also when the replication occurs, so really I'm just interpolating between two frames of server input, and totally ignoring my client.

    Additionally, when playing over a LAN, the client appears to 'fight' with the server's decision, where the server wants the player to be at 0,0,0, with no rotation, regardless of the physics-based events I send (or maybe the client does, and it's overriding the server, but that shouldn't happen).

    Any insight? I know I could crack open the source and/or re-implement AActor's replication behaviour, but I'd rather avoid that where possible, and use pre-existing functionality.

    Thanks!

    #2
    Okay, so I've added my own tickbox to the defaults tab, known as bCustomLocationReplication, which forces the normal bReplicateMovement to be false (this replaces it). This is my way of controlling when the replication occurs - I can update the client's physics simulation of its own controlled pawn via Super::Tick (as per the above screenshots of my BP_PlayerController blueprint), and store the result, and then manually trigger the replication, and interpolate between the new result and the stored (local) result. Is this an acceptable way to approach the problem?

    Code:
    void AShip::Tick(float DeltaSeconds)
    {
    	// Update AActor, including replication of movement.
    	Super::Tick(DeltaSeconds);
    
    	if (Role >= ROLE_Authority)
    	{
    		/* Some server-tracking stuff here [not relevant]. */
    	}
    
    	if (IsLocallyControlled())
    	{
    		FRigidBodyState RBState;
    
    		FVector PreTickPos;
    		FRotator PreTickRot;
    		FVector PreTickVel;
    		FVector PreTickAngVel;
    
    		if (bReplicates && bCustomLocationReplication)
    		{
    			MeshComponent->GetRigidBodyState(RBState);
    
    			PreTickPos = RBState.Position;
    			PreTickRot = RBState.Quaternion.Rotator();
    			PreTickVel = RBState.LinVel;
    			PreTickAngVel = RBState.AngVel;
    
    			GatherCurrentMovement();
    		}
    
    		FVector ReplicatedPos = ReplicatedMovement.Location;
    		FRotator ReplicatedRot = ReplicatedMovement.Rotation;
    		FVector ReplicatedVelocity = ReplicatedMovement.LinearVelocity;
    		FVector ReplicatedAngVelocity = ReplicatedMovement.AngularVelocity;
    
    		FVector LerpPos = FMath::VInterpConstantTo(PreTickPos, ReplicatedPos, DeltaSeconds, 0.001f);
    		FRotator LerpRot = FMath::RInterpConstantTo(PreTickRot, ReplicatedRot, DeltaSeconds, 0.001f);
    		FVector LerpVelocity = FMath::VInterpConstantTo(PreTickVel, ReplicatedVelocity, DeltaSeconds, 0.001f);
    		FVector LerpAngVelocity = FMath::VInterpConstantTo(PreTickAngVel, ReplicatedAngVelocity, DeltaSeconds, 0.001f);
    
    		SetActorLocation(LerpPos);
    		SetActorRotation(LerpRot);
    		MeshComponent->SetPhysicsLinearVelocity(LerpVelocity);
    		MeshComponent->SetAllPhysicsAngularVelocity(LerpAngVelocity);
    	}
    	else // Should run on remote clients that don't own this Ship object?
    	{
    		if ((bReplicates && bCustomLocationReplication) || (bReplicates && bReplicateMovement))
    		{
    			GatherCurrentMovement();
    
    			SetActorLocation(ReplicatedMovement.Location);
    			SetActorRotation(ReplicatedMovement.Rotation);
    			MeshComponent->SetPhysicsLinearVelocity(ReplicatedMovement.LinearVelocity);
    			MeshComponent->SetPhysicsAngularVelocity(ReplicatedMovement.AngularVelocity);
    		}
    	}
    }
    The client control of the Ship is fantastic and smooth, even across the LAN. I'm worried that this isn't actually a realistic representation, and the client is really just doing its own simulation, ignoring the server.

    Additionally, clients do not receive updates about other clients, despite the last 'else' statement above, which _should_ run on remote clients ticking the Ship (this is how it works, right? If the 'Replicates' tickbox is checked, all clients will run this tick function?

    Some help would be appreciated!

    Thanks

    Comment


      #3
      What you're trying to do (simulating client movement locally AND on the server) is called client prediction, and I've been trying to get it working with the UE physics for a while.
      The hard part is letting the client trust the server and fix their own simulation with the server's, as that will be in the past in the client's perspective and snap / jitter like you explained in your first post.
      The common way to do it is remember and store all the movement and input that happens with timestamps, then when you receive and update from the server (with client timestamp), snap to the received state, reapply all movement and input since that time and delete all locally stored information before that state.

      I gave up for a while as I couldn't find a way to force an actor to simulate physics to reapply the movement, but if Super::Tick does what I think it does I might have to give it another shot...

      Some relevant AH posts of mine...:
      https://answers.unrealengine.com/que...or-physic.html
      https://answers.unrealengine.com/que...-for-an-a.html

      I'm not sure how effective your lerping solution is - I thought of doing that too initially, but the simulation the client runs would still be in the past compared to the player's input, blended with the client not acknowledging server's simulation at all. (Since that's what a lerp is) You may want to draw a debug point on the position that the client receives from the server before you interp to it, so that you can compare them, and then test it out on a high latency connection, maybe fire some impulses on the server or client to try and desync them. An effective method will acknowledge the server's simulation but still feel responsive to the player as if he/she were playing on the server itself.

      If you're happy to trust your players not to cheat, you could use Rama's method of letting the clients send their location to the server, and the server trusting that, however I can't give so much trust to my players and would rather a full authoritative approach like the rewind-replay method :P
      https://forums.unrealengine.com/show...lating-Physics!
      [Plugin] Auto Settings - Game options and input binding toolkit
      [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

      Comment


        #4
        Originally posted by Acren View Post
        What you're trying to do (simulating client movement locally AND on the server) is called client prediction, and I've been trying to get it working with the UE physics for a while.
        The hard part is letting the client trust the server and fix their own simulation with the server's, as that will be in the past in the client's perspective and snap / jitter like you explained in your first post.
        The common way to do it is remember and store all the movement and input that happens with timestamps, then when you receive and update from the server (with client timestamp), snap to the received state, reapply all movement and input since that time and delete all locally stored information before that state.

        I gave up for a while as I couldn't find a way to force an actor to simulate physics to reapply the movement, but if Super::Tick does what I think it does I might have to give it another shot...

        Some relevant AH posts of mine...:
        https://answers.unrealengine.com/que...or-physic.html
        https://answers.unrealengine.com/que...-for-an-a.html

        I'm not sure how effective your lerping solution is - I thought of doing that too initially, but the simulation the client runs would still be in the past compared to the player's input, blended with the client not acknowledging server's simulation at all. (Since that's what a lerp is) You may want to draw a debug point on the position that the client receives from the server before you interp to it, so that you can compare them, and then test it out on a high latency connection, maybe fire some impulses on the server or client to try and desync them. An effective method will acknowledge the server's simulation but still feel responsive to the player as if he/she were playing on the server itself.

        If you're happy to trust your players not to cheat, you could use Rama's method of letting the clients send their location to the server, and the server trusting that, however I can't give so much trust to my players and would rather a full authoritative approach like the rewind-replay method :P
        https://forums.unrealengine.com/show...lating-Physics!
        Hey Acren,

        So the part that's frustrating me at the moment is actually figuring out how to run the local simulation, as you mentioned, and be able to manually invoke the replicated version. If I use the 'Replicate Movement' box, the location data in the Actor/Pawn is automatically overwritten, no? I then lose any track of my 'previous' location / local physics location. I feel paralyzed by a lack of documentation of how all of this actually _occurs_.

        Comment


          #5
          Yeah, I don't use Replicate Movement. It just doesn't work for me. Causes desync right off the bat.
          So what I do is disable that and just replicate the properties I need or call RPCs.

          You said clients send their input events to the server? Just do whatever the server does with them but locally when you send them.

          In the client's RPC or OnRep, if you do nothing, should just cause the client and server simulations to run completely independent of each other, which will eventually desync.
          [Plugin] Auto Settings - Game options and input binding toolkit
          [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

          Comment


            #6
            Originally posted by Acren View Post
            Yeah, I don't use Replicate Movement. It just doesn't work for me. Causes desync right off the bat.
            So what I do is disable that and just replicate the properties I need or call RPCs.

            You said clients send their input events to the server? Just do whatever the server does with them but locally when you send them.

            In the client's RPC or OnRep, if you do nothing, should just cause the client and server simulations to run completely independent of each other, which will eventually desync.
            Right, but I don't see how to intercept and utilise the physics calculations.

            Imagine we're only doing the local physics interpolation to server physics (and not keeping track of past stats and so on... that'll come later). I definitely do send the input event to the server, but also execute it locally, so my client is definitely doing physics simulations with the same inputs. Should I then tick the Pawn on the client, to get the physics updated, and store this locally, then tick on the server to get the replicated/changed version, then next client Pawn tick, interpolate? I don't yet know how to control who/what/when ticks the Pawn to this detail, yet. Any ideas?

            Comment


              #7
              Originally posted by HateDread View Post
              Right, but I don't see how to intercept and utilise the physics calculations.

              Imagine we're only doing the local physics interpolation to server physics (and not keeping track of past stats and so on... that'll come later). I definitely do send the input event to the server, but also execute it locally, so my client is definitely doing physics simulations with the same inputs. Should I then tick the Pawn on the client, to get the physics updated, and store this locally, then tick on the server to get the replicated/changed version, then next client Pawn tick, interpolate? I don't yet know how to control who/what/when ticks the Pawn to this detail, yet. Any ideas?
              I'm not sure exactly what you're aiming for... do you want to just lerp the client towards what the server is doing?
              [Plugin] Auto Settings - Game options and input binding toolkit
              [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

              Comment


                #8
                Originally posted by Acren View Post
                I'm not sure exactly what you're aiming for... do you want to just lerp the client towards what the server is doing?
                Sorry, I mean to have my local client physics simulation for my controlled Pawn, so there's immediate reaction to input, interpolated towards the server's version of the physics update.

                Comment


                  #9
                  Originally posted by HateDread View Post
                  Sorry, I mean to have my local client physics simulation for my controlled Pawn, so there's immediate reaction to input, interpolated towards the server's version of the physics update.
                  Sure, have you tried having the server constantly sending a TargetPosition, TargetVelocity etc to the client via replication or RPC, then on the client's tick (doesn't matter if it's before or after physics) lerping from the current to the target with deltaTime * speed as the alpha, speed being how fast you want it to be?

                  It will kind of override the local prediction any further than the initial key press, but that's how to do it I think.
                  [Plugin] Auto Settings - Game options and input binding toolkit
                  [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

                  Comment


                    #10
                    Originally posted by Acren View Post
                    Sure, have you tried having the server constantly sending a TargetPosition, TargetVelocity etc to the client via replication or RPC, then on the client's tick (doesn't matter if it's before or after physics) lerping from the current to the target with deltaTime * speed as the alpha, speed being how fast you want it to be?

                    It will kind of override the local prediction any further than the initial key press, but that's how to do it I think.
                    Just to clarify - I control whether we're talking client vs server tick via the *if (Role < ROLE_Authority)* type conditions inside the Tick method? Do all remote clients execute a tick call on each others' pawns, too?

                    Comment


                      #11
                      Originally posted by HateDread View Post
                      Just to clarify - I control whether we're talking client vs server tick via the *if (Role < ROLE_Authority)* type conditions inside the Tick method? Do all remote clients execute a tick call on each others' pawns, too?
                      Yeah, I just use HasAuthority() as a shortcut but it's the same thing. Clients should be ticking too, yes.
                      [Plugin] Auto Settings - Game options and input binding toolkit
                      [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

                      Comment


                        #12
                        Originally posted by Acren View Post
                        Yeah, I just use HasAuthority() as a shortcut but it's the same thing. Clients should be ticking too, yes.
                        HasAuthority() returns true for dedicated server and for listen server, when viewing the Pawn in question? Or do clients have authority over their controlled pawn?

                        If all clients tick the pawns of all other clients, what do they tick inside it? Exactly the same stuff? (Obviously except for the HasAuthority sections).

                        Comment


                          #13
                          Originally posted by HateDread View Post
                          HasAuthority() returns true for dedicated server and for listen server, when viewing the Pawn in question? Or do clients have authority over their controlled pawn?

                          If all clients tick the pawns of all other clients, what do they tick inside it? Exactly the same stuff? (Obviously except for the HasAuthority sections).
                          HasAuthority

                          Yes, in all or most cases authority is just the server.
                          All clients tick all actors if they have ticking enabled.
                          They should tick exactly the same. Completely independent of each other apart from any replicating, or spawning and destroying. I think.
                          [Plugin] Auto Settings - Game options and input binding toolkit
                          [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

                          Comment


                            #14
                            Originally posted by Acren View Post
                            HasAuthority

                            Yes, in all or most cases authority is just the server.
                            All clients tick all actors if they have ticking enabled.
                            They should tick exactly the same. Completely independent of each other apart from any replicating, or spawning and destroying. I think.
                            Code:
                            void AShip::Tick(float DeltaSeconds)
                            {
                            	// Update AActor, including physics.
                            	Super::Tick(DeltaSeconds);
                            
                            	if (HasAuthority())
                            	{
                            		TargetMovementData.Location = GetActorLocation();
                            		TargetMovementData.Rotation = GetActorRotation();
                            		TargetMovementData.LinearVelocity = MeshComponent->GetPhysicsLinearVelocity();
                            		TargetMovementData.AngularVelocity = MeshComponent->GetPhysicsAngularVelocity();
                            	}
                            	else
                            	{
                            		if (IsLocallyControlled())
                            		{
                            
                            			FVector LerpPos;
                            			FRotator LerpRot;
                            			FVector LerpVel;
                            			FVector LerpAngVel;
                            
                            
                            			LerpPos = FMath::VInterpConstantTo(GetActorLocation(), TargetMovementData.Location, DeltaSeconds, 0.01f); // have tried different values for this 'speed' argument (0.1 to 0.001). Same results.
                            			LerpRot = FMath::RInterpConstantTo(GetActorRotation(), TargetMovementData.Rotation, DeltaSeconds, 0.01f);
                            			LerpVel = FMath::VInterpConstantTo(MeshComponent->GetPhysicsLinearVelocity(), TargetMovementData.LinearVelocity, DeltaSeconds, 0.01f);
                            			LerpAngVel = FMath::VInterpConstantTo(MeshComponent->GetPhysicsAngularVelocity(), TargetMovementData.AngularVelocity, DeltaSeconds, 0.01f);
                            
                            			SetActorLocation(LerpPos);
                            			SetActorRotation(LerpRot);
                            			MeshComponent->SetPhysicsLinearVelocity(LerpVel);
                            			MeshComponent->SetAllPhysicsAngularVelocity(LerpAngVel);
                            		}
                            		else
                            		{
                            			SetActorLocation(TargetMovementData.Location);
                            			SetActorRotation(TargetMovementData.Rotation);
                            			MeshComponent->SetPhysicsLinearVelocity(TargetMovementData.LinearVelocity);
                            			MeshComponent->SetAllPhysicsAngularVelocity(TargetMovementData.AngularVelocity);
                            		}
                            	}
                            }
                            With the above, my clients often see their own movement really slowly (they rotate much slower than they do on the screen of the listen server, and slower than they should in general), and thus desynced from the server - their 'up' direction is not used for physics, but rather the server's opinion on their 'up' direction, and so on.

                            Additionally, I can't see the rotations of other clients, and I think the location stuff is slightly out of sync, too.

                            I'm not sure how to correct this, or to do something like having collisions work only on the server.

                            Any ideas?

                            Comment


                              #15
                              Originally posted by HateDread View Post
                              Code:
                              void AShip::Tick(float DeltaSeconds)
                              {
                              	// Update AActor, including physics.
                              	Super::Tick(DeltaSeconds);
                              
                              	if (HasAuthority())
                              	{
                              		TargetMovementData.Location = GetActorLocation();
                              		TargetMovementData.Rotation = GetActorRotation();
                              		TargetMovementData.LinearVelocity = MeshComponent->GetPhysicsLinearVelocity();
                              		TargetMovementData.AngularVelocity = MeshComponent->GetPhysicsAngularVelocity();
                              	}
                              	else
                              	{
                              		if (IsLocallyControlled())
                              		{
                              
                              			FVector LerpPos;
                              			FRotator LerpRot;
                              			FVector LerpVel;
                              			FVector LerpAngVel;
                              
                              
                              			LerpPos = FMath::VInterpConstantTo(GetActorLocation(), TargetMovementData.Location, DeltaSeconds, 0.01f); // have tried different values for this 'speed' argument (0.1 to 0.001). Same results.
                              			LerpRot = FMath::RInterpConstantTo(GetActorRotation(), TargetMovementData.Rotation, DeltaSeconds, 0.01f);
                              			LerpVel = FMath::VInterpConstantTo(MeshComponent->GetPhysicsLinearVelocity(), TargetMovementData.LinearVelocity, DeltaSeconds, 0.01f);
                              			LerpAngVel = FMath::VInterpConstantTo(MeshComponent->GetPhysicsAngularVelocity(), TargetMovementData.AngularVelocity, DeltaSeconds, 0.01f);
                              
                              			SetActorLocation(LerpPos);
                              			SetActorRotation(LerpRot);
                              			MeshComponent->SetPhysicsLinearVelocity(LerpVel);
                              			MeshComponent->SetAllPhysicsAngularVelocity(LerpAngVel);
                              		}
                              		else
                              		{
                              			SetActorLocation(TargetMovementData.Location);
                              			SetActorRotation(TargetMovementData.Rotation);
                              			MeshComponent->SetPhysicsLinearVelocity(TargetMovementData.LinearVelocity);
                              			MeshComponent->SetAllPhysicsAngularVelocity(TargetMovementData.AngularVelocity);
                              		}
                              	}
                              }
                              With the above, my clients often see their own movement really slowly (they rotate much slower than they do on the screen of the listen server, and slower than they should in general), and thus desynced from the server - their 'up' direction is not used for physics, but rather the server's opinion on their 'up' direction, and so on.

                              Additionally, I can't see the rotations of other clients, and I think the location stuff is slightly out of sync, too.

                              I'm not sure how to correct this, or to do something like having collisions work only on the server.

                              Any ideas?
                              Try higher interp speeds maybe, 0.1 seems very low. See if 1-10 makes a difference. I assume TargetMovementData is replicated?
                              [Plugin] Auto Settings - Game options and input binding toolkit
                              [Plugin] [Free] Inline Styling Decorator - Allow inline styling on RichTextBlock

                              Comment

                              Working...
                              X