Client to Server CFrame is delayed

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
  • I want to achieve smoothness between Client and Server with a UnreliableRemoteEvent, but the Server-sided model is not replicating the CFrame as fast as the Client, causing jitteriness.
  1. What is the issue? Include screenshots / videos if possible!
  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
  • SetNetworkOwner, BindToRenderStep and many more, none worked and no solutions on the Hub to help me out.

A fraction of the module’s code:

local AdvancedVRModule = {}

-- Services
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local ReplicateCFrames = game.ReplicatedStorage.NoVR.Events.ReplicateCFrames
local ReplicateInstances = game.ReplicatedStorage.NoVR.Events.ReplicateInstances
local ReplicateInstancesAsProps = game.ReplicatedStorage.NoVR.Events.ReplicateInstancesAsProps

local hands = {
	Left = {model = nil, offset = CFrame.new(-config.handSpacing, 0, 0), scrollDistance = 0, heldObject = nil, gripState = "Open", savedRotation = Vector3.new(0, 0, 0)},
	Right = {model = nil, offset = CFrame.new(config.handSpacing, 0, 0), scrollDistance = 0, heldObject = nil, gripState = "Open", savedRotation = Vector3.new(0, 0, 0)}
}

local currentHand = hands.Left
local isRightMouseHeld = false
local cameraOffset = config.initialOffset

-- UI setup for modal button control
local screenGui = Instance.new("ScreenGui")
screenGui.Parent = player:WaitForChild("PlayerGui")
local modalButton = Instance.new("TextButton")
modalButton.Size = UDim2.new(0, 0, 0, 0)
modalButton.BackgroundTransparency = 1
modalButton.Modal = true
modalButton.Parent = screenGui

-- Hand creation and positioning functions
function AdvancedVRModule.createHands()
	local leftHand = script.Assets.LeftHand:Clone()
	local rightHand = script.Assets.RightHand:Clone()
	leftHand.Parent = workspace.CurrentCamera
	rightHand.Parent = workspace.CurrentCamera

	ReplicateInstances:FireServer("LeftHand", script.Assets.LeftHand, {Name = "LeftHand" .. "."})
	ReplicateInstances:FireServer("RightHand", script.Assets.RightHand, {Name = "RightHand" .. "."})

	hands.Left.model = leftHand
	hands.Right.model = rightHand

	-- Apply saved rotation and scroll distance
	leftHand:PivotTo(leftHand.PrimaryPart.CFrame * CFrame.Angles(math.rad(hands.Left.savedRotation.X), math.rad(hands.Left.savedRotation.Y), math.rad(hands.Left.savedRotation.Z)))
	rightHand:PivotTo(rightHand.PrimaryPart.CFrame * CFrame.Angles(math.rad(hands.Right.savedRotation.X), math.rad(hands.Right.savedRotation.Y), math.rad(hands.Right.savedRotation.Z)))

	hands.Left.model:PivotTo(hands.Left.model.PrimaryPart.CFrame * CFrame.new(0, 0, -hands.Left.scrollDistance))
	hands.Right.model:PivotTo(hands.Right.model.PrimaryPart.CFrame * CFrame.new(0, 0, -hands.Right.scrollDistance))
end

-- Update and control functions
local function updateHandPointingDirection()
	local mouse = player:GetMouse()
	if mouse and mouse.Hit then
		local targetPosition = mouse.Hit.Position
		local handPosition = currentHand.model.PrimaryPart.Position
		local targetCFrame = CFrame.lookAt(handPosition, targetPosition)

		local smoothedCFrame = currentHand.model:GetPivot():Lerp(targetCFrame, config.smoothness)
		ReplicateCFrames:FireServer(script[currentHand.model.Name].Value, smoothedCFrame)
		currentHand.model:PivotTo(smoothedCFrame)
	end
end

local function positionHandsInFrontOfCamera()
	local cameraCFrame = workspace.CurrentCamera.CFrame * cameraOffset
	local function set(hand)
		local handOffset = hand.offset
		if hand == currentHand then
			handOffset = hand.offset * CFrame.new(0, 0, -hand.scrollDistance)
		end

		local targetCFrame = cameraCFrame * handOffset
		local smoothedCFrame = hand.model:GetPivot():Lerp(targetCFrame, config.smoothness)
		ReplicateCFrames:FireServer(script[hand.model.Name].Value, smoothedCFrame)
		hand.model:PivotTo(smoothedCFrame)
	end
	set(hands.Left)
	set(hands.Right)
end

local function updateScrollMovement(scrollDelta)
	local scrollAmount = scrollDelta * config.scrollSpeed
	currentHand.scrollDistance = math.clamp(currentHand.scrollDistance + scrollAmount, 0, config.maxScrollDistance)
end

local function gripOrReleaseObject(state)
	if state == "release" then
		ReplicateInstancesAsProps:FireServer(nil, "release", {
			rope = currentHand.rope,
			heldObject = currentHand.heldObject,
			name = currentHand.model.Name
		})
		currentHand.heldObject = nil
		currentHand.gripState = "Open"
	else
		local handPosition = currentHand.model.PrimaryPart.Position
		for _, object in pairs(workspace:GetDescendants()) do
			if object:IsA("BasePart") and not object:HasTag("Ignore") and not object.Anchored and not object.Parent:FindFirstChildOfClass("Humanoid") and (object.Position - handPosition).Magnitude <= config.gripRange then
				if object.Name ~= "Grip" and object:FindFirstChild("Grip") then
					if object.Parent == workspace and currentHand.heldObject ~= object then
						currentHand.heldObject = object
						ReplicateInstancesAsProps:FireServer(object, "6D", {
							rope = currentHand.rope,
							heldObject = currentHand.heldObject,
							name = currentHand.model.Name
						})
						currentHand.gripState = "Closed"
						break
					end
				elseif object.Name ~= "Grip" and not object:FindFirstChild("Grip") then
					if object.Parent == workspace and currentHand.heldObject ~= object then
						currentHand.heldObject = object
						ReplicateInstancesAsProps:FireServer(object, "Rope", {
							rope = currentHand.rope,
							heldObject = currentHand.heldObject,
							name = currentHand.model.Name
						})
						currentHand.gripState = "Closed"
						break
					end
				end
			end
		end
	end
end

local function updateHandPositionWithMouse()
	local delta = UserInputService:GetMouseDelta()
	local move = Vector3.new(delta.X, -delta.Y, 0) * config.mouseMoveSpeed
	local newOffset = currentHand.offset * CFrame.new(move)
	local cameraCFrame = workspace.CurrentCamera.CFrame * cameraOffset
	local handPosition = (cameraCFrame * newOffset).Position
	if (handPosition - cameraCFrame.Position).Magnitude <= config.maxMoveDistance then
		currentHand.offset = newOffset
	end
end

-- Input and Render Connections
UserInputService.InputChanged:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseMovement then
		if not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
			updateHandPointingDirection()
		else
			updateHandPositionWithMouse()
		end
	elseif input.UserInputType == Enum.UserInputType.MouseWheel then
		updateScrollMovement(input.Position.Z)
	end
end)

RunService:BindToRenderStep("Camera", Enum.RenderPriority.Camera.Value + 1, function()
	positionHandsInFrontOfCamera()
	if not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
		updateHandPointingDirection()
	end
end)

UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.Q then
		AdvancedVRModule.toggleHand()
	elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
		isRightMouseHeld = true
		modalButton.Modal = false
	elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
		gripOrReleaseObject("grip")
	elseif input.KeyCode == Enum.KeyCode.G then
		gripOrReleaseObject("release")
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton2 then
		isRightMouseHeld = false
		modalButton.Modal = true
	end
end)

return AdvancedVRModule

The server code for replicating Client - Server

local ReplicateCFrames = game.ReplicatedStorage.NoVR.Events.ReplicateCFrames
local ReplicateInstances = game.ReplicatedStorage.NoVR.Events.ReplicateInstances
local ReplicateInstancesAsProps = game.ReplicatedStorage.NoVR.Events.ReplicateInstancesAsProps
local Replicate6D = game.ReplicatedStorage.NoVR.Events.Replicate6D

ReplicateInstances.OnServerEvent:Connect(function(Client, Hands, Model, Properties)
	local NewModel = Model:Clone()

	for index, values in pairs(Properties) do
		NewModel[index] = values
	end

	NewModel.Parent = Client.Character
	game.ReplicatedStorage.NoVR[Hands].Value = NewModel

	for index, values in pairs(NewModel:GetDescendants()) do
		if values:IsA("BasePart") then
			values:SetNetworkOwner(Client)
		end
	end
end)

ReplicateInstancesAsProps.OnServerEvent:Connect(function(Client, object, Type, t)
	local newHand = Client.Character:WaitForChild(t.name .. ".", 1)
	if Type == "release" then
		for index, values in pairs(t.heldObject:GetDescendants()) do
			if values:IsA("BasePart") then
				values:SetNetworkOwner(nil)
			end
		end
		if t.heldObject:FindFirstChild("Owner") then
			t.heldObject:FindFirstChild("Owner"):Destroy()
		end

		if t.heldObject then
			if t.rope then
				t.rope:Destroy()
				t.rope = nil
			elseif t.heldObject:FindFirstChild("Motor6D") then
				t.heldObject:FindFirstChild("Motor6D"):Destroy()
			end
		end
	elseif Type == "Rope" then
		for index, values in pairs(object:GetDescendants()) do
			if values:IsA("BasePart") then
				values:SetNetworkOwner(Client.Character)
			end
		end
		local Owner = Instance.new("StringValue") Owner.Parent = object Owner.Name = "Owner" Owner.Value = t.name

		-- Create a RopeConstraint between the hand and the object
		local rope = Instance.new("RopeConstraint")
		rope.Parent = object
		rope.Attachment0 = newHand.PrimaryPart:FindFirstChild("Attachment") or Instance.new("Attachment", newHand.PrimaryPart)
		rope.Attachment1 = object:FindFirstChild("Attachment") or Instance.new("Attachment", object)

		-- Set rope properties (adjust as needed)
		rope.Length = (object.Position - newHand.PrimaryPart.Position).Magnitude
		rope.Restitution = 0.2 -- adjust for bounce effect

		currentHand.rope = rope
	elseif Type == "6D" then
		for index, values in pairs(object:GetDescendants()) do
			if values:IsA("BasePart") then
				values:SetNetworkOwner(Client.Character)
			end
		end

		local Owner = Instance.new("StringValue") Owner.Parent = object Owner.Name = "Owner" Owner.Value = t.name

		local offset = object:GetAttribute("Offset") or Vector3.new(0, 0, 0)
		local rotation = object:GetAttribute("Rotation") or Vector3.new(0, 0, 0)

		-- Create a Motor6D instead of a Weld
		local motor6D = Instance.new("Motor6D")
		motor6D.Part0 = newHand.PrimaryPart
		motor6D.Part1 = object
		motor6D.C1 = motor6D.C1 * CFrame.new(
			object:FindFirstChild("Grip").CFrame.Position.X, 
			object:FindFirstChild("Grip").CFrame.Position.Y,
			object:FindFirstChild("Grip").CFrame.Position.Z) * CFrame.Angles(-math.rad(rotation.X), -math.rad(rotation.Y), -math.rad(rotation.Z))
		motor6D.Parent = object
	end
end)

ReplicateCFrames.OnServerEvent:Connect(function(Client, Model, Pivot)
	Model:PivotTo(Pivot)
end)

Replicate6D.OnServerEvent:Connect(function(Client, currentHandPrimaryPart, currentModel, object, rotation)
	object.Parent = currentModel

	local motor6D = Instance.new("Motor6D")
	motor6D.Part0 = currentHandPrimaryPart
	motor6D.Part1 = object
	motor6D.C1 = motor6D.C1 * CFrame.new(
		object:FindFirstChild("Grip").CFrame.Position.X, 
		object:FindFirstChild("Grip").CFrame.Position.Y,
		object:FindFirstChild("Grip").CFrame.Position.Z) * CFrame.Angles(-math.rad(rotation.X), -math.rad(rotation.Y), -math.rad(rotation.Z))
	motor6D.Parent = object
end)

There’s no way to fully sync the server with the client unless it’s all being managed by the client.

If you want to achieve “smoothness,” you can go about lerping the CFrame to make it look instant, basically interpolating it.

1 Like

Alright then! I figured out another way to do it, but I really appreciate your help!

1 Like

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