Implementing a custom UCharacterMovementComponent. So far so good, but 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!

Thanks for the reply! Oh yes, that’s how I’ve been doing it. I’m avoiding the controller entirely. That’s where my current issue is coming from. By not using the controller, I seem to have missed some important aspect that keeps the rotations in sync, despite following similar logic to how the control rotations work in the base movement replication.

Try not using controller’s rotation. Instead handle your rotation in your pawn alone and say how that went.

I had several issues with inheriting the controller rotation. It has several limitations and I’m not sure why it has to be synchronized with the server but it is.

I’ve marked my comment as an answer.

Happy coding :slight_smile:

My issue has been resolved thanks to the info in this forum post:

https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1506076-solved-custom-ucharactermovementcomponent-need-help-with-rotations-advanced-networking

Thanks again to everyone who helped out!