Dragging system overview

Hello! I’ve been working on a dragging system, similar to that of a Lumber Tycoon 2 one. I just finished the rotating part, and I am kind of satisfied with how it works. However, I am an amateur at scripting, I am not aware of any issues that might appear with methods I have used, and there is probably lots of other small improvements I must make to make this code almost flawless. But as of now, here is how it looks:

LocalScript in StarterPlayerScripts:

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")

local grabEvent = ReplicatedStorage.RemoteEvents.Grabbing -- event responsible for initial grab
local ungrabEvent = ReplicatedStorage.RemoteEvents.Ungrabbing -- event responsible for ungrabbing
local dragEvent = ReplicatedStorage.RemoteEvents.Dragging -- event responsible for updating the position and orientation, which part follows

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera

local highlight = ReplicatedStorage.MinorAssets.PlayersHighlight -- just an object to make it easier to understand wether or not part is grabbable

local draggable = false

local draggedObject = nil

local rotatingForward  -- future connections
local rotatingBackward -- 
local rotatingRight -- 
local rotatingLeft --

local xAngle = 0
local yAngle = 0

local xMax = 180
local xMin = -180
local yMax = 180
local yMin = -180

local function Rotate(actionName, inputState, inputObject) -- This function is responsible for rotating the part. While the key is held down, the angle changes. W and S responsible for xAngle, A and D for yAngle
	if inputState == Enum.UserInputState.Begin and inputObject.KeyCode == Enum.KeyCode.W then
		if rotatingBackward ~= nil then
			rotatingBackward:Disconnect()
		end
		rotatingForward = RunService.RenderStepped:Connect(function()
			if xAngle - 1 < -180 then
				xAngle = 180
			else
				xAngle = xAngle - 1
			end
			print(xAngle)
		end)
	elseif inputState == Enum.UserInputState.End and inputObject.KeyCode == Enum.KeyCode.W then
		rotatingForward:Disconnect()
	elseif inputState == Enum.UserInputState.Begin and inputObject.KeyCode == Enum.KeyCode.S then
		if rotatingForward ~= nil then
			rotatingForward:Disconnect()
		end
		rotatingBackward = RunService.RenderStepped:Connect(function()
			if xAngle + 1 > 180 then
				xAngle = -180
			else
				xAngle = xAngle + 1
			end
			print(xAngle)
		end)
	elseif inputState == Enum.UserInputState.End and inputObject.KeyCode == Enum.KeyCode.S then
		rotatingBackward:Disconnect()
	elseif inputState == Enum.UserInputState.Begin and inputObject.KeyCode == Enum.KeyCode.D then
		if rotatingLeft ~= nil then
			rotatingLeft:Disconnect()
		end
		rotatingRight = RunService.RenderStepped:Connect(function()
			if yAngle - 1 < -180 then
				yAngle = 180
			else
				yAngle = yAngle - 1
			end
			print(yAngle)
		end)
	elseif inputState == Enum.UserInputState.End and inputObject.KeyCode == Enum.KeyCode.D then
		rotatingRight:Disconnect()
	elseif inputState == Enum.UserInputState.Begin and inputObject.KeyCode == Enum.KeyCode.A then
		if rotatingRight ~= nil then
			rotatingRight:Disconnect()
		end
		rotatingLeft = RunService.RenderStepped:Connect(function()
			if yAngle + 1 > 180 then
				yAngle = -180
			else
				yAngle = yAngle + 1
				print(yAngle)
			end
		end)
	elseif inputState == Enum.UserInputState.End and inputObject.KeyCode == Enum.KeyCode.A then
		rotatingLeft:Disconnect()
	end
end

local function GetParts(chosenPart) -- Recursion function, basically. Gets "directly" connected parts AKA welds and "indirectly" connected parts AKA hinges. Thinking about ropes, whether or not to add them in the function
	local toCheck = {chosenPart}
	local checked = {}

	while #toCheck > 0 do
		for i, part in pairs(toCheck) do
			if table.find(checked, part) then
				continue
			else
				local directlyConnectedParts = part:GetConnectedParts(true)
				local indirectlyConnectedParts = {}

				local joints = part:GetJoints()

				for i, joint in pairs(joints) do
					if joint:IsA("HingeConstraint") or joint:IsA("BallSocketConstraint") then
						local hinge = joint
						if hinge.Attachment0.Parent == part then
							if not table.find(toCheck, hinge.Attachment1.Parent) then
								table.insert(indirectlyConnectedParts, hinge.Attachment1.Parent)
							end
						elseif hinge.Attachment1.Parent == part then
							if not table.find(toCheck, hinge.Attachment0.Parent) then
								table.insert(indirectlyConnectedParts, hinge.Attachment0.Parent)
							end
						end
					end
				end

				for i, con in pairs(indirectlyConnectedParts) do
					table.insert(toCheck, con)
				end

				for i, v in pairs(directlyConnectedParts) do
					if not table.find(toCheck, v) then
						table.insert(toCheck, v)
					end
				end
				table.insert(checked, part)
			end
		end
		if #toCheck == #checked then
			break
		end
	end
	return checked
end

local MapCheck = false -- The checks. In future responsible for activating the highlight and changing "grabbable" to true or false.
local RangeCheck = false --
local AnchorCheck = false --
local ButtonCheck = false --
local ConnectionsCheck = false --
local SeatsCheck = false -- 

RunService.RenderStepped:Connect(function() -- Check functions. Goes through checks: If part is parented to map, if its in acceptable range, if its anchored or not, if it has a click detector, if it is "directly" connected to a seat that is being occupied or another anchored part. Since I have not tested this with other people, I dont know if I need to add a check for players body parts.
	local target = mouse.Target
	highlight.Adornee = target
	
	if target ~= nil then
		
		if target.Parent ~= workspace.Map then
			MapCheck = true
		else
			MapCheck = false
		end
		
		if (player.Character:FindFirstChild("Head").Position - target.Position).Magnitude <= 20 then
			RangeCheck = true
		else
			RangeCheck = false
		end
		
		if target.Anchored == false then
			AnchorCheck = true
		else
			AnchorCheck = false
		end
		
		if target:FindFirstChild("ClickDetector") == nil then
			ButtonCheck = true
		else
			ButtonCheck = false
		end
		
		local allConnectedParts = GetParts(target)
		local connectedParts = target:GetConnectedParts(true)
		local anchoredConnections = {}
		local occupiedSeats = {}
		
		for i, part in pairs(connectedParts) do
			if part.Anchored == true then
				table.insert(anchoredConnections, part)
			end
		end
		
		if #anchoredConnections == 0 then
			ConnectionsCheck = true
		else
			ConnectionsCheck = false
		end
		
		for i, part in pairs(allConnectedParts) do
			if part:IsA("Seat") or part:IsA("VehicleSeat") then
				if part.Occupant ~= nil then
					table.insert(occupiedSeats, part)
				end
			end
		end
		
		if #occupiedSeats == 0 then
			SeatsCheck = true
		else
			SeatsCheck = false
		end
		
		if MapCheck == true and RangeCheck == true and ButtonCheck == true and AnchorCheck == true and SeatsCheck == true and ConnectionsCheck == true then
			highlight.Enabled = true
			draggable = true
		else
			highlight.Enabled = false
			draggable = false
		end
	else
		highlight.Enabled = false
		draggable = false
	end
end)

local connection

UserInputService.InputBegan:Connect(function(input) -- Bind to rotating function
	if input.KeyCode == Enum.KeyCode.LeftShift then
		if draggedObject ~= nil then
			ContextActionService:BindAction("Rotating", Rotate, false, Enum.KeyCode.W, Enum.KeyCode.S, Enum.KeyCode.D, Enum.KeyCode.A)
		end
	end
end)

UserInputService.InputEnded:Connect(function(input) -- Unbind tp rotating function
	if input.KeyCode == Enum.KeyCode.LeftShift then
		if rotatingForward ~= nil then
			rotatingForward:Disconnect()
		end
		if rotatingBackward ~= nil then
			rotatingBackward:Disconnect()
		end
		if rotatingLeft ~= nil then
			rotatingLeft:Disconnect()
		end
		if rotatingRight ~= nil then
			rotatingRight:Disconnect()
		end
		ContextActionService:UnbindAction("Rotating")
	end
end)

UserInputService.InputBegan:Connect(function(input) -- Function for initial grab and drag
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		if draggable == true then
			
			local initialPosition = mouse.Hit.Position
			local initialCFrame = camera.CFrame - camera.CFrame.Position + initialPosition
			local target = mouse.Target
			local distance = (player.Character:FindFirstChild("Head").Position - initialPosition).Magnitude
			
			draggedObject = target
			
			grabEvent:FireServer(initialCFrame, target)
			
			connection = RunService.RenderStepped:Connect(function()
				
				local direction = mouse.UnitRay.Direction
				local difference = (camera.CFrame.Position - player.Character:WaitForChild("Head").Position).Magnitude
				local position = Vector3.new(camera.CFrame.Position.X + direction.X * (distance + difference), camera.CFrame.Position.Y + direction.Y * (distance + difference), camera.CFrame.Position.Z + direction.Z * (distance + difference))
				local cframe = camera.CFrame * CFrame.fromAxisAngle(Vector3.new(1, 0, 0), math.rad(xAngle)) * CFrame.fromAxisAngle(Vector3.new(0, 1, 0), math.rad(yAngle)) - camera.CFrame.Position + position
				dragEvent:FireServer(cframe)
			end)
		end
	end
end)

UserInputService.InputEnded:Connect(function(input) -- Function for dropping the object and resetting some values
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		ungrabEvent:FireServer()
		connection:Disconnect()
		draggedObject = nil
		xAngle = 0
		yAngle = 0
		if rotatingForward ~= nil then
			rotatingForward:Disconnect()
		end
		if rotatingBackward ~= nil then
			rotatingBackward:Disconnect()
		end
		if rotatingLeft ~= nil then
			rotatingLeft:Disconnect()
		end
		if rotatingRight ~= nil then
			rotatingRight:Disconnect()
		end
		ContextActionService:UnbindAction("Rotating")
	end
end)

ServerScript in ServerScriptService:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local holdPoint -- point where the players mouse holds the part
local alignPosition -- future constraint
local alignOrientation -- future constraint

local grabEvent = ReplicatedStorage.RemoteEvents.Grabbing
local ungrabEvent = ReplicatedStorage.RemoteEvents.Ungrabbing
local dragEvent = ReplicatedStorage.RemoteEvents.Dragging
local undragEvent = ReplicatedStorage.RemoteEvents.Undragging

grabEvent.OnServerEvent:Connect(function(player, initialCFrame, target) -- triggered by initial grab
	holdPoint = Instance.new("Attachment")
	holdPoint.Parent = target
	holdPoint.WorldCFrame = initialCFrame
	
	alignPosition = Instance.new("AlignPosition")
	alignPosition.Mode = Enum.PositionAlignmentMode.OneAttachment
	alignPosition.Parent = target
	alignPosition.MaxForce = 100000
	alignPosition.Responsiveness = 200
	alignPosition.Attachment0 = holdPoint
	
	alignOrientation = Instance.new("AlignOrientation")
	alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
	alignOrientation.Parent = target
	alignOrientation.MaxTorque = 100000
	alignOrientation.Responsiveness = 50
	alignOrientation.Attachment0 = holdPoint
end)

dragEvent.OnServerEvent:Connect(function(player, cframe) -- drag, goes on until object is dropped
	alignPosition.Position = cframe.Position
	alignOrientation.CFrame = cframe
end)

ungrabEvent.OnServerEvent:Connect(function(player) -- drop
	holdPoint:Destroy()
	alignOrientation:Destroy()
	alignPosition:Destroy()
end)

I have tried to add “anti flying” function, that would basically use Touched event on a grabbed part, and if the touched part was a child of player character, then it would trigger the ungrab event too. However, it for some reason didn’t work.

I also was wondering if it is possible to make code a little bit shorter, because I am pretty sure that what I did is not quite efficient, as I said already, I am an amateur.

Here are the codes in file form:
HighlightAndDrag.lua (9.6 KB)
Grab.lua (1.5 KB)

If you would like to test out the system yourself, you either can ask me for a copy of the place in personal messages, or try to replicate the environment yourself through the code.

No tips? No recommendations? :disappointed_face:

Still nothing? I guess I just have to hope this thing will perform well…