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.