How do I achieve smooth player velocity on all clients?

I’m working on client-driven movement for steerable dashes. The performing client uses LinearVelocity, while the server validates the action.

For other (observing) clients, I currently:

  • Send position, velocity, and facing samples every 0.02s through an UnreliableRemoteEvent (which goes to the server from the preforming client then back to all observing clients).
  • A normal RemoteEvent announces when the movement begins and sends the the first position, velocity, and facing sample. Another reliable packet sends the exact final sample when movement ends.
  • Observing clients intentionally play the received movement about 0.06s behind the performing client, giving them several future samples to interpolate between and tolerate dropped unreliable packets.
  • Use Hermite interpolation for paths between points and set the HumanoidRootPart.CFrame every frame.

The movement itself looks very smooth, but there is a visible pause/snap at the end when control returns to normal Roblox replication. However, this is only noticeable if the player keeps walking after dashing.

TLDR:

What is the standard approach for smoothly presenting client-owned, steerable movement to other clients without fighting Roblox character replication or snapping during the handoff?

I’ll attach a video showing the current behavior (notice the pause at the end):

3 Likes

possibly swap to the new server authority model if your game is early enough in dev to have it still be an option. I was having somewhat similar issues and server auth fixed it

2 Likes

That does mean he has to change logic entirely, though
But yeah, server authority is absolutely worth it for games where you fight other players

The problem is that PlayerModule on server authority is really awful
So I suggest writing your own. You can look at my FPX to see how logic is structured, but FPX is a first-person controller, so you will have to implement much more logic (client side)
That’s sad that there is no (yet*) general-purpose PlayerModule written yet that is not filled with bloat like the original :skull:

2 Likes

i still find it crazy that some incompetent aa engineer thought it would be even remotely reasonable or acceptable to have, like, at minimum 50 metamethod calls PER FRAME
in a module that serves as the base for almost every roblox game, for the record …

2 Likes

Is it hard transitioning to it? Are there any major pros or cons you experienced?

1 Like

it was quite annoying transitioning I won’t lie. The main thing you need to be careful of is that you are binding to simulation correctly. There is a function called “BindToSimulation” that gets called every tick, if the client and server disagree the client rolls back and reruns all the bind to simulation events from the moment it disagreed to the present. There is a lot of hard to diagnose behavior with this but the main advice I’d give is only call stuff on a single simulation bind. I had an issue where I was setting the humanoid state every simulation and it caused a rollback loop. I believe what was happening was the server would change to a state (e.g. physics) and then the client sees the change and rolls back. Because in the time the client was catching up the server is still continuously setting the state to physics the client will rollback to be not physics on the initial disagreement, see it is wrong, rollback again. This then loops. I fixed it through calling a function every simulation that then checks if setting humanoid state is needed (if state == physics don’t set) so it only rolls back once and doesn’t infinitely loop.

You also will need a smoothing module. Even with my logic being sound there still is an immense amount of rollbacks simply because of stuff like floating point errors in physics. The actual player models are hidden and the visual aspect is just CFramed to the real player and then on rollback it takes its current position and lerps to the new position from rollback so instead of teleporting it smoothly interpolates.

I HIGHLY recommend checking out this article by MrChickenRobot. You don’t have to use custom player controllers like him (I still use humanoids) but seeing a proof of concept for BindToSimulation was very helpful for me. Also read the roblox docs on server authority, they are a bit thin right now but still useful

@me7474 @Yarik_superpro @rudesodacan

Thank you for your insight. Unfortunately, I don’t think I want to switch to ServerAuthority this late into development. Games like combat warriors and battleground games managed to pull this off, so there is a definitely a way with the traditional engine. Maybe down the line when it becomes more stable and there is more documentation, I could give it a try. Thanks though.

hi, sorry for the late reply. I believe games like combat warriors aren’t using server authoritive systems (such as you have, not the Roblox official one I mentioned) and so their movement doesn’t have the same issue of staggering at the end.

When reading through your logic again it seems pretty clear to me the 0.06 delay is what’s causing this. My theory is because the player is delayed on other clients they exit the dash locally and are able to move a bit, by the time the other clients exit the dash they have moved away from the end point and so the snap happens. I’d recommend cranking the delay up just to verify this, if you set it to say 0.25 and the snap is much more pronounced then we know that is the cause.

Anyways you likely need some form of interpolation from the end of the dash back into normal movement. The way server authority normally does this is via having a different visual render than the physical hitbox. Set the player to invisible, create a clone of their rig that is the visual aspect, let the invisible real player move choppily and lerp the visual rig between its positions. You could CFrame each limb of the visual rig to the real character every frame so it is 1:1 normally, then go into custom interpolation and smooth the movements on dash. This would make it so at the end of the dash you could have say a 0.06 second duration lerp that goes from the end to the real position to counteract the delay, then the limbs are simply CFramed to their real counterparts again once caught up.

If you don’t want to use a render rig then I would try broadcasting the players “real” position near the end of the dash from the server. After the server broadcasts the final movement end sample also broadcast the position, then lerp between end position and current position. The issue with this is depending on ping this could desync and still snap.

Basically your issue is that you are transitioning from a player 0.06 seconds in the past directly to the real time player. I hope this helps!

What do you do when it’s an action button, like a double jump with charges, which shouldn’t fire for a long time? Like, it should fire once and not loop on the bindtosimulation. Would you get workspace:GetServerTimeNow() and register the last time the action triggered from false to true?
I used InputAction.Pressed and InputAction.Released before and it worked smoothly on studio, but while actually running the game half of the times its smooth, half of the times it isn’t.
If you want to see it yourself, that’s the place ROB | Play on Roblox. You should open the Givers (looks like arcade machines) and get a bunch of baloons. You get a double jump charge for each stack of it.

I’m not 100% sure I understand what you are asking but I believe it’s about debounces. If it is use time(). time() is the same on the server and the client so it will sync up if you use it to handle debounces. Say you want a 500 millisecond delay between each jump, you could have a variable called canJump that on input action only processes the jump if it’s true. If the jump is processed set it to false and store a timestamp and in bind to simulation check if time() - timestamp is greater than 500 ms, if it is set canJump back to true

edit: forgot it would also be best practice to store canJump as an attribute. As long as the client and server both start the time() debounce on the same bindtosimulation cycle they should both set the attribute to true at the same time and it won’t rollback

I mean, like one press = one double jump. So it doesn’t loop while you hold the double jump, unless you press it again. I tried inputaction.Pressed and it kinda worked. Tried with the BindToSimulation and it seems to rollback more now, with a code like this:

local jumpAction = charContexts:FindFirstChild("JumpAction")
local jump = jumpAction and jumpAction:GetState()
local previous = char:GetAttribute("Jump")
if jump ~= previous then
		--print(jump)
		--print(previous)
		if jump == true then
			if char.Humanoid.Health > 0 then
				--print("alive")
				local runStats = char:FindFirstChild("Stats")
				if runStats then
					--print("stats")
					if char.Humanoid.FloorMaterial == Enum.Material.Air then
						--print("on air")
						
						if not charges then
							charges = Instance.new("IntValue")
							charges.Name = name
							charges.Parent = char
						end
						local doubleJumpItem = runStats.Inventory:FindFirstChild("7")
						if doubleJumpItem and doubleJumpItem.Value > charges.Value then
							--print("oncooldown")
							charges.Value+=1
							char.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
							--if isServer then
							--	local att = Instance.new("Attachment")
							--	att.Parent = char.HumanoidRootPart
							--	att.CFrame = CFrame.new(0,-char.Humanoid.HipHeight,0)
								--local eff = script.JumpEff:Clone()
								--eff.Parent = att
								--eff:Emit(30)
								--debris:AddItem(att,eff.Lifetime.Max+1)
							--end
						end
						--char.Humanoid:GetPropertyChangedSignal("FloorMaterial"):Once(function()
						--	if charges then charges.Value = 0 end
						--end)
					end
				end
			end
		end
		char:SetAttribute("Jump",jump)
	end

Dude somehow I missed this, the forum just gave me this notification. Right now I am using something similar to the second solution that you mentioned, but down the road I was planning to use something like the first one (thank you for describing it in such a pellucid manner). This helps very much, and once again thank you.

1 Like