Problems with client-sided body movers

  1. What do you want to achieve?
    I recently made my server-sided grappling script client-sided since there is much less lag with the client-sided one.
  2. What is the issue?
    The grappling and tweening works fine, but there is a bug within the script that seems to make my character freeze as shown here:
    https://gyazo.com/d8ba3f31bfb128ade2257707018ed65e

If you need any information on how the grappling script actually works, you may refer to this post.

Summary
--//Services
local InputService = game:GetService("UserInputService")
local RunService   = game:GetService("RunService")

--//Locals
local player     = game.Players.LocalPlayer
local camera     = workspace.CurrentCamera
local mouse      = player:GetMouse()
local character  = player.Character

local CameraShaker = require(game.ReplicatedStorage.CameraShaker)
local RightHook = false
local LeftHook = false

local leftHooked = false
local rightHooked = false

--.. gear stuff
local gearFolder      = script.Parent.Parent
local valuesFolder    = gearFolder:WaitForChild("Values")
local remotesFolder   = gearFolder:WaitForChild("Remotes")
local animationFolder = gearFolder:WaitForChild("Animations")

local burstCooldown = 2

local Multiplier = 1


local gasBoost = false
local Blades = false

--..body movers
local bodyGyro     = character.HumanoidRootPart:WaitForChild("BodyGyro")
local bodyVelocity = character.HumanoidRootPart:WaitForChild("BodyVelocity")

--..debounce
local debounce  = false
local cooldown  = 0.05

--..camera stuff
local tiltAngle = 0

local canReload = true

local BladesAmount = valuesFolder.BladeAmount.Value
local GasAmount = valuesFolder.GasAmount.Value

local ODMGui = gearFolder.ODMGui:Clone()
ODMGui.Parent = player.PlayerGui

local gasBar = ODMGui.GasFrame.count

local function UpdateBar()
	gasBar:TweenSize(UDim2.new(2,0,GasAmount/-1526.71756, 0))
end

UpdateBar()

local Grapple = animationFolder["Grapple"]
local BladeChange = animationFolder["BladeChange"]
local RightOrbit = animationFolder["RightOrbit"]
local LeftOrbit = animationFolder["LeftOrbit"]
local VerticalSpin = animationFolder["VerticalSpin"]
local HorizontalSpin = animationFolder["HorizontalSpin"]



--..animation stuff
--local leftCycle  = character.Humanoid:LoadAnimation(animationFolder["LeftCycle"])
--local rightCycle = character.Humanoid:LoadAnimation(animationFolder["RightCycle"])

--//Functions

local TweenService = game:GetService("TweenService")
--//Locals
--..tweens tuff
local tweenInfo  = TweenInfo.new(0.2, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
local tweenTable = {}

--//Functions
local function createAttachment(_ancestor)
	local attachment = Instance.new("Attachment")
	attachment.Parent = _ancestor
	attachment.Name = "RopeAttachment"
	return attachment
end

local function createPseudoHook(_ancestor)
	local hook = Instance.new("Part")
	hook.Anchored = true
	hook.Size = Vector3.new(0.1,0.1,0.1)
	hook.CFrame = _ancestor.CFrame
	hook.Parent = _ancestor
	hook.Name = "PseudoHook"
	return hook
end


--.. really and truly its a spring but it has the cool effect we want
local function createRope(_ancestor, attahcment0, attachment1)
	local rope = Instance.new("SpringConstraint")
	rope.Name = "Rope"
	rope.Thickness = 0.1
	rope.Coils = 12
	rope.Radius = 2
	rope.LimitsEnabled = true
	rope.Color = BrickColor.Black()
	rope.Visible = true
	rope.MaxLength = valuesFolder["MaxHookDistance"].Value
	rope.Attachment0 = attahcment0
	rope.Attachment1 = attachment1
	rope.Parent = _ancestor
	return rope
end

local function createWeld(part0, part1)
	local weld = Instance.new("WeldConstraint")
	weld.Part0 = part0
	weld.Part1 = part1
	weld.Parent = part1
	weld.Name = "HookWeld"
end


local function lerpUnit(a ,b ,c)
	return a + (b - a) * c
end

local function checkHookPlayerDistance()
	return math.abs((character.HumanoidRootPart.CFrame.Position - mouse.Hit.Position).Magnitude)
end

local function returnHookDirection(side)
	if valuesFolder[side.."Position"].Value == Vector3.new() then 
		return Vector3.new() 
	end
	return (valuesFolder[side.."Position"].Value - character.HumanoidRootPart.CFrame.Position).Unit
end

local function onServerEvent(array)
	local character = player.Character
	if array[1] == "Eject" then
		if array[2] == "Right" then
			if valuesFolder["RightEjected"].Value then return end
			if valuesFolder["RightHooked"].Value then return end

			valuesFolder["RightEjected"].Value = true
			character["Torso"].rightgrapple.Eject.Enabled = true
			local hook = createPseudoHook(character["Torso"].rightgrapple)
			local attachment = createAttachment(hook)
			local rope = createRope(hook, hook.Parent["RopeAttachment"], attachment)

			tweenTable.tweenHookCFrameRight = TweenService:Create(hook, tweenInfo, {CFrame = CFrame.new(array[3])})
			tweenTable.tweenRopeRadiusRight = TweenService:Create(rope, tweenInfo, {Radius = 0})
			tweenTable.tweenHookCFrameRight:Play()
			tweenTable.tweenRopeRadiusRight:Play()
			character["Torso"].rightgrapple.Grapple:Play()

			tweenTable.tweenHookCFrameRight.Completed:Connect(function(state)
				if state == Enum.PlaybackState.Completed then
					valuesFolder["RightHooked"].Value = true
					rightHooked = true
					valuesFolder["RightEjected"].Value = false
					valuesFolder["RightPosition"].Value = hook.Position
					createWeld(array[4], hook)	
					character["Torso"].Ejector.Gas.Enabled = true
				end
				tweenTable.tweenHookCFrameRight = nil
				tweenTable.tweenRopeRadiusRight = nil
			end)
			character["Torso"].rightgrapple.Eject.Enabled = false
		elseif array[2] == "Left" then
			if valuesFolder["LeftEjected"].Value then return end
			if valuesFolder["LeftHooked"].Value then return end

			valuesFolder["LeftEjected"].Value = true
			character["Torso"].leftgrapple.Eject.Enabled = true
			local hook = createPseudoHook(character["Torso"].leftgrapple)
			local attachment = createAttachment(hook)
			local rope = createRope(hook, hook.Parent["RopeAttachment"], attachment)

			tweenTable.tweenHookCFrameLeft = TweenService:Create(hook, tweenInfo, {CFrame = CFrame.new(array[3])})
			tweenTable.tweenRopeRadiusLeft = TweenService:Create(rope, tweenInfo, {Radius = 0})
			tweenTable.tweenHookCFrameLeft:Play()
			tweenTable.tweenRopeRadiusLeft:Play()
			character["Torso"].leftgrapple.Grapple:Play()

			tweenTable.tweenHookCFrameLeft.Completed:Connect(function(state)
				if state == Enum.PlaybackState.Completed then
					valuesFolder["LeftHooked"].Value = true
					valuesFolder["LeftEjected"].Value = false
					valuesFolder["LeftPosition"].Value = hook.Position
					createWeld(array[4], hook)
					character["Torso"].Ejector.Gas.Enabled = true					
				end
				tweenTable.tweenHookCFrameLeft = nil
				tweenTable.tweenRopeRadiusLeft = nil
			end)
		end
		character["Torso"].leftgrapple.Eject.Enabled = false
	elseif array[1] == "Retract" then
		if array[2] == "Right" then
			if valuesFolder["RightEjected"].Value then
				tweenTable.tweenHookCFrameRight:Cancel()
				local hook = character["Torso"].rightgrapple["PseudoHook"]
				local ropeTween = TweenService:Create(hook["Rope"], tweenInfo, {Thickness = 0})
				ropeTween:Play()
				character["Torso"].rightgrapple.Reel:Play()

				if character["Torso"].Ejector.Gas.Enabled then
					if valuesFolder["LeftHooked"].Value then 
						print("left hooked") 
					else
						character["Torso"].Ejector.Gas.Enabled = false
					end
				end

				ropeTween.Completed:Connect(function()
					valuesFolder["RightEjected"].Value = false
					valuesFolder["RightPosition"].Value = Vector3.new()
					wait(.1)
					hook:Destroy()
					character["Torso"].Ejector.Gas.Enabled = false

				end)
			elseif valuesFolder["RightHooked"].Value then
				local hook = character["Torso"].rightgrapple["PseudoHook"]
				local ropeTween1 = TweenService:Create(hook["Rope"], tweenInfo, {Thickness = 0})
				local ropeTween2 = TweenService:Create(hook["Rope"], tweenInfo, {Radius = 0.2})
				wait(.1)

				spawn(function()
					if hook:FindFirstChild("HookWeld") then
						hook["HookWeld"]:Destroy()
					else
						print("Oop no weld. Moving on.")
					end
				end)
				ropeTween1:Play()
				ropeTween2:Play()
				character["Torso"].rightgrapple.Reel:Play()

				if character["Torso"].Ejector.Gas.Enabled then
					if valuesFolder["LeftHooked"].Value then 
						print("left hooked") 

					else
						character["Torso"].Ejector.Gas.Enabled = false
					end
				end

				ropeTween2.Completed:Connect(function()
					valuesFolder["RightHooked"].Value = false
					valuesFolder["RightPosition"].Value = Vector3.new()
					wait(.1)
					hook:Destroy()
					character["Torso"].Ejector.Gas.Enabled = false

				end)
			end
		elseif array[2] == "Left" then
			if valuesFolder["LeftEjected"].Value then
				tweenTable.tweenHookCFrameLeft:Cancel()
				local hook = character["Torso"].leftgrapple["PseudoHook"]
				local ropeTween = TweenService:Create(hook["Rope"], tweenInfo, {Thickness = 0})
				ropeTween:Play()
				character["Torso"].leftgrapple.Reel:Play()

				if character["Torso"].Ejector.Gas.Enabled then
					if valuesFolder["RightHooked"].Value then 
						print("Right hooked") 
					else
						character["Torso"].Ejector.Gas.Enabled = false
					end
				end


				ropeTween.Completed:Connect(function()
					valuesFolder["LeftEjected"].Value = false
					valuesFolder["LeftPosition"].Value = Vector3.new()
					wait(.1)
					hook:Destroy()
					character["Torso"].Ejector.Gas.Enabled = false

				end)
			elseif valuesFolder["LeftHooked"].Value then
				local hook = character["Torso"].leftgrapple["PseudoHook"]
				local ropeTween1 = TweenService:Create(hook["Rope"], tweenInfo, {Thickness = 0})
				local ropeTween2 = TweenService:Create(hook["Rope"], tweenInfo, {Radius = 0.2})
				wait(.1)
				spawn(function()
					if hook:FindFirstChild("HookWeld") then
						hook["HookWeld"]:Destroy()
					else
						print("Oop no weld. Moving on.")
					end
				end)
				ropeTween1:Play()
				ropeTween2:Play()
				character["Torso"].leftgrapple.Reel:Play()

				if character["Torso"].Ejector.Gas.Enabled then
					if valuesFolder["RightHooked"].Value then 
						print("Right hooked") 
					else
						character["Torso"].Ejector.Gas.Enabled = false
					end
				end

				ropeTween2.Completed:Connect(function()
					valuesFolder["LeftHooked"].Value = false
					valuesFolder["LeftPosition"].Value = Vector3.new()
					wait(.1)
					hook:Destroy()
					character["Torso"].Ejector.Gas.Enabled = false
				end)
			end
		end
	end
end



InputService.InputBegan:connect(function(input, isTyping)
	if isTyping then return end
	if input.KeyCode == Enum.KeyCode.R and canReload and BladesAmount > 0 and not Blades then
		remotesFolder:WaitForChild("Reload"):FireServer()
		canReload = false
		Blades = true

		for i,v in pairs (character.Humanoid:GetPlayingAnimationTracks())do
			v:Stop()
		end

		local track = character:WaitForChild("Humanoid"):LoadAnimation(BladeChange)
		track:Play()

		track.Priority = Enum.AnimationPriority.Action
		wait(1)
		canReload = true
	end
end)



if InputService:IsKeyDown(Enum.KeyCode.Space) and GasAmount > 0 then
	if gasBoost == false then
		gasBoost = true
		Multiplier = 1.3
		print("Gas! Gas! Gas!")
		remotesFolder["Boost"]:FireServer(gasBoost)
		GasAmount = GasAmount - 1

		print(GasAmount)
		UpdateBar()
	elseif gasBoost == true then
		gasBoost = false
		Multiplier = 1
	end
end



InputService.InputBegan:connect(function(input, isTyping)
	if isTyping then return end
	if input.KeyCode == Enum.KeyCode.R and canReload and Blades then
		remotesFolder:WaitForChild("ReturnBlade"):FireServer()
		canReload = false
		Blades = false

		for i,v in pairs (character:WaitForChild("Humanoid"):GetPlayingAnimationTracks())do
			v:Stop()
		end

		local track = character:WaitForChild("Humanoid"):LoadAnimation(BladeChange)
		track:Play()


		track.Priority = Enum.AnimationPriority.Action
		wait(1)
		canReload = true
	end
end)

InputService.InputBegan:connect(function(input, isTyping)
	local chance = math.random(1,2)
	if isTyping then return end
	if input.UserInputType == Enum.UserInputType.MouseButton1 and Blades then
		if chance == 1 then

			for i,v in pairs (character:WaitForChild("Humanoid"):GetPlayingAnimationTracks())do
				v:Stop()
			end

			local track = character:WaitForChild("Humanoid"):LoadAnimation(VerticalSpin)
			track:Play()


			track.Priority = Enum.AnimationPriority.Action
			remotesFolder["Attack"]:FireServer()

		elseif chance == 2 then

			for i,v in pairs (character:WaitForChild("Humanoid"):GetPlayingAnimationTracks())do
				v:Stop()
			end

			local track = character:WaitForChild("Humanoid"):LoadAnimation(HorizontalSpin)
			track:Play()


			track.Priority = Enum.AnimationPriority.Action
			remotesFolder["Attack"]:FireServer()

		end
	end
end)

InputService.InputBegan:connect(function(input, isTyping)
	local DoneOnce = false
	local briefCooldown = 0.3

	if isTyping then return end
	if input.KeyCode == Enum.KeyCode.W then
		if not DoneOnce then
			DoneOnce = true
			wait(briefCooldown)
			DoneOnce = false
			return
		else 
			if DoneOnce == true then
				print("Burst!")
				DoneOnce = false
			end
		end
	end
end)
--//Loops


spawn(function()
	while true do
		RunService.Heartbeat:Wait()
		if  valuesFolder["RightHooked"].Value or valuesFolder["LeftHooked"].Value then
			character.Humanoid.AutoRotate = false
			-- body gyro
			bodyGyro.MaxTorque = lerpUnit(bodyGyro.MaxTorque, Vector3.new(150000 * Multiplier, 150000 * Multiplier, 150000 * Multiplier), 0.3)
			bodyGyro.CFrame = CFrame.new(character.HumanoidRootPart.Position, valuesFolder["RightPosition"].Value + valuesFolder["LeftPosition"].Value)

			-- body velocity
			bodyVelocity.MaxForce = lerpUnit(bodyVelocity.MaxForce, Vector3.new(200000 * Multiplier, 200000 * Multiplier, 200000 * Multiplier), 0.1)

			if InputService:IsKeyDown(Enum.KeyCode.A) then

				bodyVelocity.Velocity = lerpUnit(bodyVelocity.Velocity, (returnHookDirection("Right") + returnHookDirection("Left") + (-character.HumanoidRootPart.CFrame.RightVector)) * (valuesFolder["Magnitude"].Value - 30) * Multiplier, 0.08)
				--bodyGyro.CFrame = CFrame.new(character.HumanoidRootPart.Position, valuesFolder["RightPosition"].Value + valuesFolder["LeftPosition"].Value) * CFrame.Angles(0, 0, math.rad(25))
			elseif InputService:IsKeyDown(Enum.KeyCode.D) then

				bodyVelocity.Velocity = lerpUnit(bodyVelocity.Velocity, (returnHookDirection("Right") + returnHookDirection("Left") + (character.HumanoidRootPart.CFrame.RightVector)) * (valuesFolder["Magnitude"].Value - 30) * Multiplier, 0.08)
				--bodyGyro.CFrame = CFrame.new(character.HumanoidRootPart.Position, valuesFolder["RightPosition"].Value + valuesFolder["LeftPosition"].Value) * CFrame.Angles(0, 0, math.rad(-25))
			else
				bodyVelocity.Velocity = lerpUnit(bodyVelocity.Velocity, (returnHookDirection("Right") + returnHookDirection("Left")) * (valuesFolder["Magnitude"].Value * Multiplier), 0.2)
				--bodyGyro.CFrame = CFrame.new(character.HumanoidRootPart.Position, valuesFolder["RightPosition"].Value + valuesFolder["LeftPosition"].Value) * CFrame.Angles(0, 0, math.rad(0))
			end
			camera.FieldOfView = lerpUnit(camera.FieldOfView, 100*Multiplier, 0.05)

		else
			-- reset values when we arent hooked 
			character.Humanoid.AutoRotate = true
			bodyVelocity.MaxForce = lerpUnit(bodyVelocity.MaxForce, Vector3.new(), 0.2)
			bodyGyro.MaxTorque = lerpUnit(bodyGyro.MaxTorque, Vector3.new(), 0.3)
			camera.FieldOfView = lerpUnit(camera.FieldOfView, 70, 0.05)
		end
	end
end)

local function onInputBegan(key, event)
	if event then return end
	if debounce then return end
	--..scripting the right hook ejection key
	if key.KeyCode == Enum.KeyCode.E then
		if checkHookPlayerDistance() <= valuesFolder["MaxHookDistance"].Value then
			if mouse.Target ~= nil then
				-- we can eject hook and do other cool stuff
				onServerEvent({"Eject", "Right", mouse.Hit.Position, mouse.Target})
				debounce = true
				RightHook = true
				wait(cooldown)
				debounce = false

				GasAmount = GasAmount - 1

				local track = character:WaitForChild("Humanoid"):LoadAnimation(Grapple)
				track:Play()

				for i,v in pairs (character.Humanoid:GetPlayingAnimationTracks())do
					v:Stop()
				end

				track.Priority = Enum.AnimationPriority.Action

				print(GasAmount)
				UpdateBar()
			end
		end

	elseif key.KeyCode == Enum.KeyCode.Q then
		if checkHookPlayerDistance() <= valuesFolder["MaxHookDistance"].Value then
			if mouse.Target ~= nil then
				onServerEvent({"Eject", "Left", mouse.Hit.Position, mouse.Target})
				debounce = true
				LeftHook = true
				wait(cooldown)
				debounce = false

				GasAmount = GasAmount - 1
				local track = character:WaitForChild("Humanoid"):LoadAnimation(Grapple)
				track:Play()

				for i,v in pairs (character.Humanoid:GetPlayingAnimationTracks())do
					v:Stop()
				end

				track.Priority = Enum.AnimationPriority.Action

				print(GasAmount)
				UpdateBar()

			end
		end
	end
end

local function onInputEnded(key, event)
	if event then return end

	if key.KeyCode == Enum.KeyCode.E then
		if valuesFolder["RightHooked"].Value or valuesFolder["RightEjected"].Value then
			onServerEvent({"Retract", "Right"})
			RightHook = false
		end

	elseif key.KeyCode == Enum.KeyCode.Q then
		if valuesFolder["LeftHooked"].Value or valuesFolder["LeftEjected"].Value then
			onServerEvent({"Retract", "Left"})
			LeftHook = false
		end
	end
end


--//Events
InputService.InputBegan:Connect(onInputBegan)

InputService.InputEnded:Connect(onInputEnded)

Thats’ a lot of code to read through and find out exactly which event fires to activate the grappling hook. If you trim the post to the bare minimum it would help everyone rather than digging through reams of unrelated code.

Creating constraints on the client is a bad idea. It causes a desynchronization with the server’s physics mechanism, which breaks Roblox’s physics replicator.

Even if you get this working as intended, your character may freeze up from the perspective of other players because the server doesn’t know how to interpolate the data you’re sending to it, as those constraints don’t exist from its perspective.

If I were you I would try having the constraints pre-made on the server, and have some RemoteEvent where you ask the server to set the necessary properties when firing a hook at some target and position in the world. Simultaneously, you should set those properties yourself on the client at the same time, as a form of client-side prediction. That will get rid of the lag you’re describing.

5 Likes

Decided to not use any rope constraints and went with beams instead for the grapple part.