[Networking] Relay a rotation from the client to the server

I’m pretty new to networking and making multiplayer games in general but I kind of understand the basics of how things should work (I think), just don’t know how to implement it!

I’m making a custom movement system, similar to an airplane. I have the standard force working as it should through the character movement components “add force” node, as that replicates without me needing to think more about it. Now however I’m trying to make the rotation work. I’ve tried to use control rotation but that is choppy on both the client and server for some reason. I understand that I need to do the rotation on the client and have the server listen to that somehow for the client to see a smooth version of what’s happening but I’m unsure how to do it! RepNotify variables don’t seem to work as I want them to in this case.

My current setup is this:
This happens every tick

and this happens when pressing a movement input

I do understand that this doesn’t work for clients and also understand why so what I need help with is how I can actually get it working! I’ve read about client prediction and having the server softly correct it as well but am not sure how to implement that sort of thing!

you’d need an RPC if going from client to server

however its better to RPC the input rather than the result (rotation)

I did get this working with control rotation but wouldn’t it be better to RPC the result to avoid discrepancies? Getting packet loss or something similar would cause a tick or so to not apply so if using the input they would become unsynced.

I recommend using the new MoverComponent, which includes built-in network prediction. When creating a floating or flying pawn for multiplayer, you won’t need to worry about manual server RPCs or complex custom physics configurations.

Just remember to enable the Mover plugin first.

You have to accept in multiplayer its always out of sync due to lag, however if you use client values you open yourself to cheating and/or server correction which leads to ‘client jitter’

You and Auran131 are arguing input vs result, but that’s not really the axis that matters here. The one that matters is delta vs absolute, and once you look at it that way your packet loss objection answers itself.

Your objection is correct, by the way

You said if a packet goes missing you lose a tick and things drift apart. That’s exactly right, and it’s true whether you send the input or the result. It’s true because of what you’re sending, not which end you’re sending it from.

Look at your tick graph. You’re feeding deltas into Add Actor Local Rotation. Every frame says “turn me 2 more degrees”. If one of those goes missing, that 2 degrees is gone forever. Nothing ever corrects it. Send a thousand of those and lose a few percent and the two machines are visibly in different places, with no way back.

Now send the absolute value instead. “My roll is 37.5 degrees right now.” Lose that packet and nothing breaks, because the next one says “my roll is 41.2 degrees” and whoever received it is instantly correct again. One dropped packet costs you one frame of being slightly stale rather than a permanent error.

That’s the whole thing. Deltas accumulate error, absolute state heals itself. It applies in both directions and it’s why nobody sends per-frame deltas over the wire.

What that means for your graph

Keep all the rotation math exactly where it is. It’s on the owning client, it has zero latency, it feels good, don’t move it.

Then send the result to the server as an absolute rotation, not the delta, and not every tick. Somewhere around 20 to 30 times a second is plenty, and you can go lower with interpolation on the other end. The server sets the rotation and replicates it out normally.

One thing to be careful about: make that RPC unreliable. People’s instinct is to mark it reliable so it can’t be lost, and on a channel that fires 20+ times a second that’s how you overflow the reliable buffer and get clients kicked off the server. You don’t need reliable, that’s the point of sending absolute values. Losing one genuinely does not matter.

On the other clients, interpolate towards the replicated rotation rather than snapping to it. Replication arrives at a lower rate than your framerate, so without interpolation you get exactly the choppiness you described.

Why control rotation felt choppy

Same reason. It wasn’t the wrong mechanism, it just arrives in steps and you were seeing the steps.

And this is why your movement “just worked”

Worth knowing because it’s the thing that’s been confusing you. Add Force went through the Character Movement Component, and CMC has client prediction built into it for movement. It predicts locally, sends input to the server, and reconciles if the server disagrees. All of that is already written.

CMC does not do that for arbitrary rotation you apply yourself. So one half of your system had a whole prediction system behind it and the other half had nothing, which is exactly why one felt fine and the other didn’t. You weren’t doing anything wrong, you just hit the edge of what CMC covers.

On the cheating point

Auran131 is right that trusting client rotation is exploitable, but I’d weigh it against what you’re building. For a plane’s visual orientation in a co-op or casual game it’s usually fine.

If you do want to tighten it without the jitter that comes from hard server correction, there’s a cheap middle ground. On the server, compare the incoming rotation against the last one you accepted, and check that delta against your maximum turn rate times the time elapsed. Anything physically impossible gets clamped. That catches the obvious abuse and costs you almost nothing, and because you’re clamping rather than overwriting, honest players never feel it.

Vahab’s suggestion of Mover is reasonable too and it does solve this properly, just be aware it’s still experimental and it’s a bigger change than the above.

send the absolute rotation instead of the delta, quantized to a byte per axis, and replicate it with the client timestamp. the server accepts it only if the step from the last accepted value is under a threshold, so a dropped packet costs one update instead of permanent drift, and you get one place to clamp for cheating. apply the accepted value on the server with the same smoothing you use for remote proxies so the correction is not a visible snap. the reason deltas keep drifting is covered here: https://www.youtube.com/playlist?list=PLO5I1x5y5XZ8