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
BodyPositionandBodyGyroto the selected part or model’s PrimaryPart. - Set
CanCollideto 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
AssemblyLinearVelocityproperty for natural physics behavior.
Example snippet:
local velocity = (currentPosition - previousPosition) / deltaTime
model.PrimaryPart.AssemblyLinearVelocity = velocity
B. Fix rotation glitches
- Currently using
BodyGyrowith high torque values and setting itsCFrameevery update. - Considering switching to
AlignOrientationwithRigidityEnabledto reduce jitter. - Possibly locking rotation axes manually to prevent unwanted spinning.
C. Clean removal of physics movers
- Ensure all
BodyPositionandBodyGyroinstances are properly destroyed when dragging stops. - Restore
CanCollideon 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)