(Solved) Custom UCharacterMovementComponent, need help with rotations (Advanced Networking)

Hi there!

If you have some experience overriding the UCharacterMovementComponent to extend its functionality (or you have extensive knowledge of the overall Character replication system, including how the PlayerController handles rotations based on input), I could certainly use your help.

I’ve been implementing a custom child class in which I’ve overridden the networking functionality to allow for the addition of robust new movement mechanics. By this, I mean new abilities and things that make use of the FSavedMove class and related functions to get smooth movement even with higher latencies. I required a bit more information to be sent between server and client than what the current CompressedFlags approach allows (limited to a few booleans). I needed to send a few floats and whatnot across as part of the move, which the current system isn’t designed for.

I know this isn’t nearly as performant as sending a single uint8 across, like the base system does with the compressed flags, but it is essential to the prototype I’m making (this data needed to be sent across every tick as an RPC regardless, so I doubt I’m destroying performance any more than I would have). Therefore, I created my own ServerMove functions and have overridden any related functions to make use of them. It all works super well. The custom system feels as smooth and responsive as the base system, and I’ve opened it up to endless expansion with new movement functionality (within reason, so as not to flood the servermove RPC with too large a payload).

However, the system only works 100% correctly when making use of the built-in rotation logic for Characters, which involves calling AddControllerYawInput and AddControllerPitchInput. Instead of using these, I have taken the same float values that would have been sent into these functions and used them in my own methods within the new movement component. I think this is where my problem lies.

The problem is that while the custom system works wonderfully (with my own rotation logic) when the ping is lower than 200, I suddenly have a major issue with move replaying or some other function at any higher latency. Actual movement works perfectly, even with my custom movement logic, but if I perform any kind of rotation, it has an issue. Well, more correctly, my rotations seem to work fine and the moves replay as expected right up until I interact with stairs, pass too close to other characters (like the server’s pawn), or bump into walls.

It still looks somewhat smooth for a tiny bit, but, all of a sudden, the rotations wig out and my character does random 180 turns (yaw) and the camera flies up and down (pitch) for about 2-3 seconds. This occurs even when I let go of all input. It can sometimes occur without bumping into anything, but it is most commonly reproduced when interacting with objects.

I investigated the functions that handle interactions with stairs (StepUp) and collisions, but they seem relatively harmless. They do, however, set the rotation equal to the UpdatedComponent’s current rotation. Upon further inspection and doing some pretty wild stuff with rotations, I found that my client’s rotation and what the server sees can fall out of sync. I think this could be a part of the problem.

As I said, this doesn’t occur when using the AddControllerYaw/Pitch logic. Why not just use those methods? I need to use my own rotation logic due to the constraints of the controller. I’ve been searching endlessly for the main difference between my own logic and what the AddControllerYaw/Pitch does:

By following the trail of functions (from AddControllerYaw/Pitch) through the inheritance hierarchy, you can see the values for yaw and pitch eventually being sent to the PlayerController. Here, these values are used to set an FRotator called RotationInput. This variable is used every tick to update the rotation of the pawn in UpdateRotation(). These values basically just rotate the current control rotation (the orientation of the controller. As you know, you can change the rotation of your character by directly calling SetControlRotation with a view offset, for doing a quick 180 turn or something). The view rotator is sent through a PlayerCameraManager to enforce certain constraints before calling SetControlRotation(ViewRotation) and Pawn->FaceRotation(ViewRotation). As far as I can tell, both of these functions eventually just end up setting the rotation of the rootcomponent of the actor.

My custom method takes the values for yaw and pitch to create an FRotator, much like how the PlayerController does it. I use these values to call AddActorLocalRotation. I followed the chain of function calls to find that this function also updates the rootcomponent. Now, most of the networking logic and whatnot inside of the UCharacterMoveComp uses the rotation of the UpdatedComponent (which basically translates into the rotation of the rootcomponent) to perform most of its rotation interpolation and prediction, so I don’t see too much of a difference here between how my methods and the Controller set rotations.

However, in the actual ServerMove implementation, I do see that the current view (yaw, pitch, rot) is sent through as a parameter in the main RPC. From what I can tell, these values are the current control rotation’s yaw and pitch related to each move. This is used to call SetControlRotation in the ServerMove_Implementation. In fact, SetControlRotation or its buddies are used to change the actor’s rotation in basically any situation that involves player input. I send through my own yaw and pitch values, which update the rotation using AddActorLocalRotation wherever the rotation is set by the usual methods.

When it comes to my rotations being able to fall out of sync between client and server, I thought this was handled by the actor being set back to the server’s authoritative transform on every update, followed by a correction on the client’s side. This is usually how it works from what I’ve researched and when I’ve made my own prediction/move replaying/interpolation code. The client is set back to server’s authoritative transform and you replay unacknowledged moves on top of this to ensure the client doesn’t notice being set back too much when they have a higher latency.

I cannot for the life of me figure out where this occurs in the UCharacterMovementComponent. I see where the correction function is called, but it doesn’t help much; ClientUpdatePositionAfterServerUpdate is where move replaying occurs, but I’ve searched around it and can’t see where the server forces the client to reset to its own authoritative transform before replaying; and I’ve even seen that there’s an onrep_replicatedmovement function in the ACharacter class, but it doesn’t seem to occur there either (dunno if I’m maybe just blind).

As a result, I think there’s something else going on with how the Controller handles rotations and interacts with the base ServerMove logic, ACharacter class, and other parts of this crazy web of interconnected classes.

So, does anyone know how I should be handling the rotations to keep things in sync? Is there a check or function somewhere that uses the controller’s rotation logic that I need to override? Should I be using something other than AddActorLocalRotation? Or, alternatively, does anyone know the best way to change the orientation of the controller to be relative to the actor rather than constrained to a plane?

I truly appreciate anyone who has read through all of this. It’s been rather tricky and difficult to customise the UCharacterMovementComponent, and I feel like I’m so close to getting it right.

Thanks!

I’m not sure if this will help.

AActor::AddActorLocalRotation will pass value to USceneComponent.

APlayerController::AddYawInput will eventually pass value to UCharacterMovementComponent where performs collision checking.

For SimulatedProxy, Another thing to look out is USceneComponent::PostRepNotifies. In here, It will update received transform replication from server.

I remember that Client will overwrite its transform using correction from server and then re-apply the input. It’s probably in client RPC function somewhere.

Rotations aren’t corrected from the server, they assume that a capsule based character doesn’t care about rotation since it doesn’t effect collision and leave it client authed. Also the client isn’t “sent back” to the servers transform unless the deviation gets out of a set range.

I changed this up in my vr plugin and made rotations also be server authed, you can look at the repository around the client correction area to see how I at least did it.

You could also change the character over to use direct rotations instead of control rotation by passing in the rot instead of control rot where it gathers the data and applies it.

Regardless the client sends up its rotation every move to the server to use, if you require set rotations between movement steps you need to play that back on the server in the same sequence.

Thanks so much for the replies! I’ll take a look and let you know how it goes.

Yes! Thank you for the info, mordentral! You’re absolutely right, the current system doesn’t correct rotations whatsoever. I was being a complete knob when I was replaying my rotation moves. I clearly did not fully understand how the base system worked. Now, the rotations occur locally and the actor’s rotation is sent through to the server via ServerMove in a similar way to the control rotation view data. It simply sets the actor’s rotation where the player controller’s rotation would usually be updated in the function. This is how the base system actually does it (which I was somehow blind to before), followed by the rotation interpolation for simulated proxies to fill in the gaps. The system now works pretty well.

I incorrectly assumed that the full transform would be set back to server’s authoritative transform whenever a correction was required, as that is how I did it when writing my own netcode in another project (kept both location and rotation server authed). It makes sense that the system focuses on positional authorisation rather then rotations from a gameplay perspective (cheating, hit detection, etc).

Now I just need to do a bit of refactoring to clean up the redundant code. Thanks again! I can now complete work on my prototype.