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.