Projectile desync between clients causing projectiles to enact different physics depending on FPS

I am making a projectile system that is currently specified for a dodgeball. The initial throw is enacted through shapecasts to ensure that the throw is consistent and responsive, as well as having a bit of customizability. After the first bounce, it exits from the custom shapecast physics and utilizes Roblox’s physics, as only the visual appeal of the bounce matters.

Prior to the switch, the ball is made invisible on the server, and is cloned and applied force on every client so the bounce is smooth. Since they all enact the same force from the same position, they should logically end up in the same position, regardless of framerate.

The issue is that this not the case, as projectiles tend to end up in different positions across the clients depending on their current framerate, despite undergoing the same forces and collisions. This causes projectiles to end up even more disjointed overtime as other projectiles collide with them.

Here’s 2 screenshots of the same projectiles being thrown across 2 separate clients, one in focus and consistently running at 60fps and the other not in focus and averaging around 15fps.

60fps:

15fps:

All of the projectiles are in entirely different positions, implying that they all enacted different physics.

I’ve tried a handful of things to overcome this issue:

  1. Having the bounce also on the server: This is undesired as movement would be clunky on at least one player’s end, regardless of network ownership.

  2. Operating the bounce with shapecasts / raycasts: Shapecasts don’t operate properly at low distances, making decay and rolling very difficult to operate without the projectile clipping through surfaces.

  3. Enacting force to the server and updating the client projectile’s position every frame: This makes the client projectile appear to be as clunky as if it was on the server.

  4. Using bodypositions to naturally move the client projectile to the server: ApplyImpulse and other means of enacting force have a delay on the server, resulting in a weird rubber band effect in the client’s movement. Also, greater velocities result in the movement looking more like a tween.

Is there any other way I can make sure they end up in the same position whilst still making it look appealing?

Here’s the logic behind this for reference:

Server (just the logic after the shapecast disconnects):

    self.Object.Position = current_pos
	self.Object.Transparency = 1
	self.Object.ProximityPrompt.Enabled = false
	
	self.Object.Owner.Changed:Connect(function()
		self.Object.Transparency = 0
	end)
	
	remotes.Client.BounceProjectile:FireAllClients(self.Object, self.Object.Position, velocity * 8)

Client (Bounce remote event):

client_remotes.BounceProjectile.OnClientEvent:Connect(function(proj : BasePart, pos : Vector3, force : Vector3)
		if not proj then return end
		
		local new_proj = proj:Clone()
		new_proj.Position = pos
		
		proj.CollisionGroup = "ServerSteadyBall"
		
		new_proj.Transparency = 0
		new_proj.ProximityPrompt.Enabled = true
		new_proj.Parent = proj.Parent
		new_proj:ApplyImpulse(force) 

		local decay_force = Instance.new("BodyForce")
		decay_force.Parent = new_proj

		local decay = RunService.Heartbeat:Connect(function()
			decay_force.Force = Vector3.new(-new_proj.AssemblyLinearVelocity.X, 0, -new_proj.AssemblyLinearVelocity.Z) * 15
		end)
		
		local halt = new_proj:GetPropertyChangedSignal("AssemblyLinearVelocity"):Connect(function()
			if Vector3.new(new_proj.AssemblyLinearVelocity.X, 0, new_proj.AssemblyLinearVelocity.Z).Magnitude < 5 then
				decay:Disconnect()
				if decay_force.Parent then
					decay_force:Destroy()
				end
			end
		end)
		
		new_proj.Destroying:Connect(function()
			decay:Disconnect()
			if decay_force.Parent then
				decay_force:Destroy()
			end
		end)
		
		proj.Owner.Changed:Connect(function(value)
			if value then
				new_proj:Destroy()
			end
		end)
		
		new_proj.ProximityPrompt.Triggered:Connect(function(player)
			proj.Collect:FireServer()
		end)
		
		proj.Destroying:Connect(function()
			new_proj:Destroy()
		end)
	end)

srry for how long this btw, this is just hella complicated

have you accounted for dt, aka delta time, for the projectile calculation?

dt gives u the time since the previous frame

for the shapecast on the server yeah but not for the client because it uses roblox’s physics

well, do it on the client as well

that wouldnt work as the issue is mainly applyimpulse and other things that have to do with roblox’s physics, the only way this would be possible is if i switched the bounce to the custom projectile physics which uses the runservice, only that is undesired becuase shapecasts aren’t accurate enough

Yes, but you’re using run service on the client, which is dependent on the client’s frame rates. You provided pics indicating that the results were different at different fps. Dt is used for that very problem, unless what you’re updating is always the same value.

Dodgeballs shouldn’t be too big, so maybe I think you can get away with imprecise hit boxes if you CFrame it

Also, what is decay_force? Bc you applied an initial force with ApplyImpulse, but then use bodyForce to apply another? I’m a bit confused. Is it supposed to be like a slowing down force?

1 Like

yeah decay_force is a slowing down force, because manually changing the friction didn’t show any results

i figured that that could be a contributor since that doesn’t use dt but i figure that it wouldn’t be much of a difference if I just found a direct solution

i’m most likely gonna go with the cframe idea and try to migrate the shapecast physics to use raycasts, and move the projectile along the raycast until it touches something, cuz all that really matters is that it doesn’t clip through anything after the first collision

ill post here how it works, thanks for the help!

it could be a a contributor. Usually with ApplyImpulse, you’d let the roblox physics do the slowing down and all that, since it’s just a push; the rest of the movement is handled by Roblox.

But yeah, I’d try the cframe option for now. Just don’t delete your old code, because it’s good to go back to your other solution and compare the two

1 Like

Your decay force is basically a drag force proportional to velocity. This is fps dependent becsuse velocity changes 240 hz and you are only accessing and updating the force on heartbeat 60 hz.

The optiom is to rewrite the physics engine and use physics substep and control the timestep yourself pr try out roblox’s aerodynamic forcez. Or you could use linear velocity to 0,0,0 as that ses robloxs 240 hz calculation.

Or you could use a different system.

1 Like