Model Dragging System – Velocity Not Applying on Release and Physics Issues

Model Dragging System – Velocity Not Applying on Release and Physics Issues

Hello developers,

I’m working on a model-dragging system where players can pick up and move models or parts in real time. The system uses remote events and physics components (BodyPosition and BodyGyro) attached to the model’s PrimaryPart.

While the basic dragging works, I’m encountering some significant issues, especially with velocity not transferring on release and models glitching with random rotation.


What I’m Doing

On the server, the workflow is:

  • Attach BodyPosition and BodyGyro to the selected part or model’s PrimaryPart.
  • Set CanCollide to false while dragging.
  • Let the client update the desired position and orientation via remote events.
  • Remove the physics movers and restore physics on release.

All parts in the model are welded together with WeldConstraints, and the PrimaryPart is the reference for movement.


Problems Encountered

1. Velocity does not transfer on release
When releasing the model, it simply drops without carrying any velocity, regardless of how fast it was moved.
I have tried to calculate velocity manually by tracking position deltas over time and then applying AssemblyLinearVelocity, but it doesn’t feel smooth or reliable, especially under lag or inconsistent frame updates.

2. Models randomly rotate or spin during dragging
Even with BodyGyro or AlignOrientation used to control rotation, the model sometimes jitters or tilts unpredictably. I suspect timing or syncing issues between physics updates and WeldConstraints cause this behavior.


What I’m Trying to Fix / Improve

A. Proper velocity calculation and application

  • Track the last CFrame and timestamp per dragged model on the server.
  • When ownership is returned, calculate the velocity from recent position changes.
  • Apply the velocity to the PrimaryPart’s AssemblyLinearVelocity property for natural physics behavior.

Example snippet:

local velocity = (currentPosition - previousPosition) / deltaTime
model.PrimaryPart.AssemblyLinearVelocity = velocity

B. Fix rotation glitches

  • Currently using BodyGyro with high torque values and setting its CFrame every update.
  • Considering switching to AlignOrientation with RigidityEnabled to reduce jitter.
  • Possibly locking rotation axes manually to prevent unwanted spinning.

C. Clean removal of physics movers

  • Ensure all BodyPosition and BodyGyro instances are properly destroyed when dragging stops.
  • Restore CanCollide on all parts.
  • Maintain a tracking table of dragged parts/models to avoid orphaned movers.

Video Demonstration


Code Samples

Server-Side Script

local repStorage = game:GetService("ReplicatedStorage")

local parts = {}

local function getRoot(part)
	if part:IsA("Model") then
		return part.PrimaryPart
	else
		return part
	end
end

local function setupBodyMovers(part)
	local root = getRoot(part)
	if not root then return end

	if not root:FindFirstChild("BodyPosition") then
		local bp = Instance.new("BodyPosition")
		bp.Name = "BodyPosition"
		bp.MaxForce = Vector3.new(1e6, 1e6, 1e6)
		bp.P = 3000
		bp.D = 500
		bp.Position = root.Position
		bp.Parent = root
	end

	if not root:FindFirstChild("BodyGyro") then
		local bg = Instance.new("BodyGyro")
		bg.Name = "BodyGyro"
		bg.MaxTorque = Vector3.new(1e6, 1e6, 1e6)
		bg.P = 3000
		bg.D = 500
		bg.CFrame = root.CFrame
		bg.Parent = root
	end
end

local function removeBodyMovers(part)
	local root = getRoot(part)
	if not root then return end

	local bp = root:FindFirstChild("BodyPosition")
	local bg = root:FindFirstChild("BodyGyro")

	if bp then bp:Destroy() end
	if bg then bg:Destroy() end
end

repStorage.Events.Dragging.RequestOwnership.OnServerEvent:Connect(function(plr, part)
	if part.Parent == workspace.Parts or part.Parent == workspace.Droppers then
		for _, v in ipairs(parts) do
			if v == part then
				repStorage.Events.Dragging.RequestOwnership:FireClient(plr, false, part)
				return
			end
		end

		if part:IsA("Model") and not part.PrimaryPart then
			for _, child in ipairs(part:GetDescendants()) do
				if child:IsA("BasePart") then
					part.PrimaryPart = child
					break
				end
			end
		end

		table.insert(parts, part)

		if part:IsA("Model") then
			for _, v in ipairs(part:GetDescendants()) do
				if v:IsA("BasePart") then
					v.CanCollide = false
				end
			end
		else
			part.CanCollide = false
		end

		setupBodyMovers(part)
		repStorage.Events.Dragging.RequestOwnership:FireClient(plr, true, part)
	end
end)

repStorage.Events.Dragging.ReturnOwnership.OnServerEvent:Connect(function(plr, part)
	for i, v in ipairs(parts) do
		if v == part then
			table.remove(parts, i)
			break
		end
	end

	removeBodyMovers(part)

	if part:IsA("Model") then
		for _, v in ipairs(part:GetDescendants()) do
			if v:IsA("BasePart") then
				v.CanCollide = true
			end
		end
	else
		part.CanCollide = true
	end
end)

repStorage.Events.Dragging.UpdatePos.OnServerEvent:Connect(function(plr, part, pos: CFrame)
	local root = getRoot(part)
	if not root then return end

	local bp = root:FindFirstChild("BodyPosition")
	local bg = root:FindFirstChild("BodyGyro")
	if bp and bg then
		bp.Position = pos.Position
		bg.CFrame = pos

		if part:IsA("Model") then
			part:PivotTo(pos)
		else
			root.CFrame = pos
		end
	end
end)

Client-Side Script

local players = game:GetService("Players")
local plr = players.LocalPlayer

local runService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local repStorage = game:GetService("ReplicatedStorage")

local mouse = plr:GetMouse()

local distance = 10
local selected = nil
local highlight = nil

local partsFolder = workspace:WaitForChild("Parts")

local events = repStorage.Events.Dragging

repStorage.Events.Dragging.RequestOwnership.OnClientEvent:Connect(function(bool, hovered)
	if bool then
		repStorage.Keybinds.dragging.Enabled = true
		selected = hovered
		highlight = Instance.new("Highlight")
		highlight.FillTransparency = 1
		highlight.Adornee = hovered
		highlight.Parent = hovered
	end
end)

function isHovering()
	if mouse.Target and mouse.Target:IsA("Part") and (mouse.Target.Parent == partsFolder or mouse.Target.Parent == workspace.Droppers) then
		return mouse.Target
	elseif mouse.Target and mouse.Target.Parent:IsA("Model") and (mouse.Target.Parent.Parent == partsFolder or mouse.Target.Parent.Parent == workspace.Droppers) then
		return mouse.Target.Parent
	end
	return nil
end

mouse.WheelForward:Connect(function()
	if distance < 20 then
		distance += 0.5
	end
end)

mouse.WheelBackward:Connect(function()
	if distance > 5 then
		distance -= 0.5
	end
end)

mouse.Button1Down:Connect(function()
	if selected then
		if highlight then
			highlight:Destroy()
			highlight = nil
		end
		events.ReturnOwnership:FireServer(selected)
		repStorage.Keybinds.dragging.Enabled = false
		selected = nil
	else
		local hovered = isHovering()
		if hovered then
			repStorage.Events.Dragging.RequestOwnership:FireServer(hovered)
		end
	end
end)

runService.Heartbeat:Connect(function()
	if selected then
		local origin = workspace.CurrentCamera.CFrame.Position
		local direction = (mouse.Hit.Position - origin).Unit * distance

		local rayParams = RaycastParams.new()
		rayParams.FilterDescendantsInstances = {plr.Character, selected}
		rayParams.FilterType = Enum.RaycastFilterType.Exclude

		local rayResult = workspace:Raycast(origin, direction, rayParams)
		local targetPosition = rayResult and (rayResult.Position - direction.Unit) or (origin + direction)

		local tweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
		local tween
		if selected:IsA("Model") then
			local model = selected
			local startCFrame = model:GetPivot()
			local rotationCFrame = startCFrame - startCFrame.Position
			local endCFrame = CFrame.new(targetPosition.X, targetPosition.Y, targetPosition.Z) * rotationCFrame

			local cfValue = Instance.new("CFrameValue")
			cfValue.Value = startCFrame

			local tweenInfo = TweenInfo.new(
				0.3,
				Enum.EasingStyle.Sine,
				Enum.EasingDirection.Out
			)

			local tween = TweenService:Create(cfValue, tweenInfo, { Value = endCFrame })

			cfValue:GetPropertyChangedSignal("Value"):Connect(function()
				model:PivotTo(cfValue.Value)
			end)

			tween:Play()

			tween.Completed:Connect(function()
				cfValue:Destroy()
			end)
		else
			tween = TweenService:Create(selected, tweenInfo, {
				Position = targetPosition
			})
			tween:Play()
		end
		
		local rotCFrame
		
		if selected:IsA("Model") then
			local rotation = selected:GetPivot()
			rotCFrame = CFrame.Angles(
				math.rad(rotation.X),
				math.rad(rotation.Y),
				math.rad(rotation.Z)
			)
		else
			local rotation = selected.Rotation
			rotCFrame = CFrame.Angles(
				math.rad(rotation.X),
				math.rad(rotation.Y),
				math.rad(rotation.Z)
			)
		end

		events.UpdatePos:FireServer(selected, CFrame.new(targetPosition) * rotCFrame)
	end
end)


If anyone has worked on similar systems or can share advice, I’m very interested! I can also share parts of my code or test setups on request.

Thanks,
Andreas (@andreasthuis)

2 Likes

Have you ever tried transfering the NetworkOwnership to the player then using BasePart:ApplyImpulse() instead?

Given that you have the distance value you have I’m pretty sure you just need to do this

-- When selected send a signal to the server
ServerEvent:FireServer(target, player)

-- This should be put in a physics loop (Heartbeat)
-- Main dragging part
local camera = workspace.CurrentCamera
local camCFrame = camera.CFrame

local direction = camCFrame.LookVector * distance
local wishPos = camCFrame.Position + direction
local velocity = target.Position - wishPos -- If it goes the other way just switch the two around lol

-- The easeFactor variable depends on how stiff you want the change in position to be
-- Low values = bouncy || High values = stiff
-- No need to add deltatime due to consistent framerate
target:ApplyImpulse(velocity * easeFactor)

-- When you de-select the object send a signal to the server again
ServerEvent:FireServer(target, nil)
1 Like

I attempted to use network ownership, but it disrupted the physics, and I was unsure how to resolve the issue. I will try your solution, although I am uncertain if it will work and I am concerned about potential desynchronization.

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