Very laggy/delayed movement

I have a server-sided script that changes the direction of a moving ball. However, the inputs feel quite delayed and not very instantaneous. I’ve tried a few solutions like setting the network owner of the ball to the player, and trying to make the application of the force instantaneous and at max power. In my debugging process, I measured the client-server delay and it averages 0.09 seconds (at least I think it’s seconds, let me know)
Here is the game link if anyone wants to try: SLANT - Roblox
Game works 100% fine in studio but is very laggy in game.

Server side snippet (handles the application of the force from the client)

RunService.Heartbeat:Connect(function(dt)
	for _, player in ipairs(Players:GetPlayers()) do
		local direction = playerDirections[player]
		if not direction then playerDirections[player] = 0 end

		local character = player.Character
		if not character then print("No char") continue end

		local ball = character:FindFirstChild("Ball")
		local attachment = ball and ball:FindFirstChild("Attachment")
		local vectorForce = ball and ball:FindFirstChild("VectorForce")

		if not (ball and attachment and vectorForce) then continue end
		setupBallCollision(player,ball)
		-- Apply proportional force to close the gap
		
		--local forwardSpeedRatio = math.min(math.abs(forwardSpeed) / maxSpeed, 1)
		--local dynamicForwardForce = math.clamp(forwardForce * (1 - forwardSpeedRatio), 0, forwardForce)
		
		local localVelocity = ball.CFrame:VectorToObjectSpace(ball.AssemblyLinearVelocity)
		local lateralSpeed = localVelocity.X
		local forwardSpeed = localVelocity.Z

		local speedMultiplier = playerSpeedMultipliers[player] or 1

		local desiredSideSpeed = direction * maxSideSpeed 
		local sideSpeedDiff = desiredSideSpeed - lateralSpeed
		local adjustedSideForce = sideSpeedDiff * maxSideSpeed

		local forwardSpeedDiff = (maxSpeed * speedMultiplier) - forwardSpeed
		local dynamicForwardForce = math.clamp(forwardSpeedDiff * 50, 0, forwardForce * speedMultiplier)

		local totalForce = Vector3.new(adjustedSideForce, 0, dynamicForwardForce)
		vectorForce.Force = totalForce
	end
end)

-- Update movement direction from clients
BallControlEvent.OnServerEvent:Connect(function(player, direction)
	if typeof(direction) == "number" then
		print("Moving "..tostring(direction))
		playerDirections[player] = direction
		BallControlEvent:FireClient(player)
	else
		warn("Invalid direction from", player.Name)
	end
end)

-- Cleanup
Players.PlayerRemoving:Connect(function(player)
	playerDirections[player] = nil
end)

Client side (Gets the movedirection from inputs and fires an event to the server)

-- Send direction to server if changed
local function updateDirection(newDirection)
	if moveDirection ~= newDirection then
		moveDirection = newDirection
		print("Inputted "..tostring(newDirection))
		BallControlEvent:FireServer(moveDirection)
	end
end

--When the server lets the client know that it receieved the call
BallControlEvent.OnClientEvent:Connect(function()
	plr.PlayerGui.GameGui.server.Visible = true
	wait(1)
	plr.PlayerGui.GameGui.server.Visible = false
end)

--  Keyboard input and showing the client-server delay
UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.A or input.KeyCode == Enum.KeyCode.Left then
		updateDirection(1)
		local dlay = 0
		plr.PlayerGui.GameGui.client.Visible = true
		repeat dlay+=task.wait() until plr.PlayerGui.GameGui.server.Visible == true
		dlay = math.floor(dlay*1000)
		dlay = dlay / 1000
		plr.PlayerGui.GameGui.delay.Text = "Delay: "..tostring(dlay)
		plr.PlayerGui.GameGui.client.Visible = false
	elseif input.KeyCode == Enum.KeyCode.D or input.KeyCode == Enum.KeyCode.Right then
		updateDirection(-1)
		local dlay = 0
		plr.PlayerGui.GameGui.client.Visible = true
		repeat dlay+=task.wait() until plr.PlayerGui.GameGui.server.Visible == true
		dlay = math.floor(dlay*1000)
		dlay = dlay / 1000
		plr.PlayerGui.GameGui.delay.Text = "Delay: "..tostring(dlay)
		plr.PlayerGui.GameGui.client.Visible = false
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.A or input.KeyCode == Enum.KeyCode.Left or
		input.KeyCode == Enum.KeyCode.D or input.KeyCode == Enum.KeyCode.Right then
		updateDirection(0)
		if UserInputService:IsKeyDown(Enum.KeyCode.A) or UserInputService:IsKeyDown(Enum.KeyCode.Left) then
			updateDirection(1)
		end
		if UserInputService:IsKeyDown(Enum.KeyCode.D) or UserInputService:IsKeyDown(Enum.KeyCode.Right) then
			updateDirection(-1)
		end
	end
end)

let me know if you need the full scripts.

Hey,

The reason it feels delayed is because you’re moving the ball on the server.

The server should not be handling the ball movement, as the server is slower, and in addition, latency will cause the more delay.

The reason it works better in studio is that you’re on a local server.

Instead of sending the movedirection and firing events to the server, I’d suggest firing an event to the server to set the network ownership to your client/player, and then completely handle the movement on the client once the network ownership has been set.

You’d find that it runs much smoother, and it also takes the load of handling the physics of all the balls off the server.

Could you please show how you implementing it?
I believe you might’ve set the network ownership to the client, but still handled the movement via the server (which wouldn’t work, since the ball’s physics are now handled by the specific client)

1 Like

Thank you, but I am skeptical about how to make it so player’s cant exploit and make their ball go super fast. In addition, I want to measure how far the ball travels using the server, and I want players to see other player’s balls :skull: (their character).

Currently, when the players character is loaded in, the network ownership of the ball is set to the player. The movement is still handled by the server.

I also have a different issue that I’ve implemented a “band-aid” fix for:
The character is supposed to die when the ball touches a killpart, but it seems like the client replication for physics is a little slow, so the character dies before it touches the killpart on the client-side. I’ve fixed it by firing a remote event from the client to let the server know when the client has touched the ball, but I’m wondering if there is a better solution.

I suggest you to add movement checks on the server to make sure the player is not exploiting, but still handle the movement on the client.

This is how the default Roblox characters are handled. All the animations and input are handled on the client, and on the server we check to make sure the player isn’t moving too quickly/teleporting (basically exploiting).

It’s definitely more challenging than to handle the movement on the server. But it’ll give a much smoother experience to players, will still replicate, and take the load of calculating the physics on the server.

TL;DR - handle the movement on the client, and secure the movement using the server.


Yeah that happens because the server is the one handling the physics, and in the server it hits the killpart before it replicates. Handling the movement using network ownership on the client will fix that too.

In your current solution, players can also exploit by not firing the event.

Hope that helps :slightly_smiling_face:

1 Like

For this type of game I recommend giving the client authority over the horizontal position, whilst having the server maintain the vertical and forward position. This is not a perfect solution but it should fix the cheat vector of just flying over obstacles and speeding yourself up.

I in general would not recommend making a game like this server-authoritative without implementing a rollbacked deterministic movement system as it would be incredibly frustrating to play a game of this speed and die to a killbrick you avoided on your client but maybe hit on the server, and as of right now this is what Roblox has as the standard

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.