Hello!
I have made these scripts so I can have a grabbing system for my restaurant games. I thought about sharing it for fun.
Binding the input
local UIS = game:GetService("UserInputService")
local CAS = game:GetService("ContextActionService")
local RepStore = game:GetService("ReplicatedStorage")
-- Data
local pickedUpPart = false
local part
local pos, norm
--Player
local player = game:GetService("Players").LocalPlayer
local cam = workspace.CurrentCamera
local mouse = player:GetMouse()
local character = player.Character
local function cleanUp()
print("end drag")
end
local function beginDrag()
print("dragging")
end
local function checkDrag(actionName, inputState, _inputObject)
--Check whether to begin or end grabbing
if inputState == Enum.UserInputState.Begin and not pickedUpPart then
beginDrag()
elseif inputState == Enum.UserInputState.End and pickedUpPart then
cleanUp()
end
end
-- Bind input
CAS:BindAction("dragObject",checkDrag,false,Enum.UserInputType.MouseButton1)
You should see the prints in the output when you click the left mouse button.
Now you can use whatever parts as long its small and unanchored
The stuff begins
We return to our beginDrag()
function. Remove the print part. We first shall check if the part is at our reach and mouse.
local function beginDrag()
--Raycast to find a part from the mouse ray.
local mouseRay = mouse.UnitRay
local rayFind = workspace:Raycast(cam.CFrame.Position, mouseRay.Direction.Unit * 10,RaycastParams.new())
if rayFind then
local tempPart = rayFind.Instance --Storing it in a variable because some problems arise with the instance changing.
print("We got part: "..tempPart.Name)
end
end
Now some problems arise because the characters get in the way, so we exclude them from the raycast.
local function getCharacters()
local characters = {}
for a,b in pairs(game:GetService("Players"):GetPlayers()) do
if b.Character then
table.insert(characters,b.Character)
end
end
return characters
end
...
--In the beginDrag() function
--Raycast to find a part from the mouse ray.
local mouseRay = mouse.UnitRay
local params = RaycastParams.new()
params.FilterDescendantsInstances = getCharacters()
params.FilterType = Enum.RaycastFilterType.Exclude
local rayFind = workspace:Raycast(cam.CFrame.Position, mouseRay.Direction.Unit * 10,params)
The characters should not be in the way now.
Anchored parts could also get in the way. To avoid this, whitelist any child of the Draggables folder.
-- In the rayFind condition
if rayFind then
local tempPart = rayFind.Instance --Storing it in a variable because some problems arise with the instance changing.
if tempPart.Parent == workspace.Draggables then
print("We got part: "..tempPart.Name)
end
end
Now we do not want grabbing conflicts, so we start dealing with network ownership to control whether the server or some client handles the physics for a certain part.
-- In the rayFind condition
local rayFind = workspace:Raycast(cam.CFrame.Position, mouseRay.Direction.Unit * 10,params)
if rayFind then
local tempPart = rayFind.Instance --Storing it in a variable because some problems arise with the instance changing.
if tempPart.Parent == workspace.Draggables then
if RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == player or RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == nil then
-- Pickup part
pickedUpPart = true
part = tempPart
part.CanCollide = false
print("We got part: "..tempPart.Name)
end
end
end
Create a server script to handle these remotes.
local RepStore = game:GetService("ReplicatedStorage")
RepStore.Remotes.SetNetworkOwner.OnServerEvent:Connect(function(plr, draggedPart, bool)
if draggedPart:FindFirstAncestor("Workspace") then
draggedPart:SetNetworkOwner(bool and plr or nil)
end
end)
RepStore.Remotes.GetNetworkOwner.OnServerInvoke = function(plr, part)
local owner
local success, e = pcall(function() -- Wrapped in a pcall just in case it returns a error where the part is either anchored or welded.
owner = part:GetNetworkOwner()
end)
if success then
return owner
else
return "no"
end
end
Time to work on the cleanUp()
function
local function cleanUp()
local part_ = part --Stored for later use on network ownership.
-- Remove physics
if part then
part.CanCollide = true
end
--Cleanup
pickedUpPart = false
mouse.TargetFilter = nil
part = nil
end
Now we can start working on grabbing the part. Let’s have some fun with raycasts
local function updatePosition()
if not part then return end
if not part.Parent then return end
-- Setup ignoreList
local ignoreList = getCharacters()
local mouseRay = mouse.UnitRay
local params = RaycastParams.new()
params.FilterDescendantsInstances = ignoreList
params.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(mouseRay.Origin,mouseRay.Direction.Unit * 10,params)
pos = result and result.Position or mouseRay.Origin + (mouseRay.Direction.Unit * 10)
norm = result and result.Normal or Vector3.zero
end
In case we do not have results from the raycast we will put the position to the cast’s endpoint (mouseRay.Origin + (mouseRay.Direction.Unit * 10)
) and the normal to Vector3.zero
.
Then we use this function to move the part to the update
-- In the rayFind condition
if rayFind then
local tempPart = rayFind.Instance --Storing it in a variable because some problems arise with the instance changing.
if tempPart.Parent == workspace.Draggables then
if RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == player or RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == nil then
-- Pickup part
pickedUpPart = true
part = tempPart
part.CanCollide = false
--Filters
RepStore.Remotes.SetNetworkOwner:FireServer(part,true)
local connection
connection = game:GetService("RunService").RenderStepped:Connect(function()
if not part or not part.Parent then cleanUp() connection:Disconnect() return end -- End in case part is destroyed
if not pickedUpPart then connection:Disconnect() return end -- End when cleaned up
updatePosition()
local endPos = pos + (norm * (part.Size * 0.5))
part.Position = endPos
end)
end
end
end
Some funny stuff happens with the part bouncing way high and I want physics on the part. When I tried looking for implementation for the system, it’s mainly based on the deprecated bodyposition and bodygyro instances. Luckily, I found a way to adapt those functions to alignposition and alignorientation. I also forgot to revoke network ownership for the player once the part’s been left alone for a while.
-- In the variables
-- Physics
local a0
local alignPos
local alignRot
--Player
local player = game:GetService("Players").LocalPlayer
local cam = workspace.CurrentCamera
local mouse = player:GetMouse()
local character = player.Character
...
local function cleanUp()
local part_ = part
-- Remove physics
if part then
a0:Destroy()
alignPos:Destroy()
alignRot:Destroy()
part.CanCollide = true
-- Wait until part stops falling or gets deleted to disconnect network ownership
coroutine.wrap(function()
local start = tick()
local latest = tick()
-- Reset timer if part gets dragged before half a second.
local connection = part_.ChildAdded:Connect(function(child)
if child:IsA("AlignPosition") or child:IsA("AlignOrientation") then
start = tick()
end
end)
while true do
if latest-start >= 0.5 then connection:Disconnect() break end
latest = tick()
game:GetService("RunService").RenderStepped:Wait()
end
if part_ then RepStore.Remotes.SetNetworkOwner:FireServer(part_,false) end
end)()
end
--Cleanup
pickedUpPart = false
mouse.TargetFilter = nil
part = nil
end
... -- In the beginDrag() function
-- In the rayFind condition
--Pickup part
pickedUpPart = true
part = tempPart
part.CanCollide = false
--Physics Body
a0 = Instance.new("Attachment")
a0.Parent = part
a0.WorldPosition = part.Position
alignPos = Instance.new("AlignPosition")
alignPos.MaxVelocity = 75
alignPos.MaxForce = 708^2
alignPos.Responsiveness = 100
alignPos.Mode = Enum.PositionAlignmentMode.OneAttachment
alignPos.Parent = part
alignPos.Attachment0 = a0
alignRot = Instance.new("AlignOrientation")
alignRot.MaxTorque = 500
alignRot.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignRot.Parent = part
alignRot.Attachment0 = a0
--Filters
RepStore.Remotes.SetNetworkOwner:FireServer(part,true)
mouse.TargetFilter = part
local connection
connection = game:GetService("RunService").RenderStepped:Connect(function()
if not part or not part.Parent then cleanUp() connection:Disconnect() return end
if not pickedUpPart then connection:Disconnect() return end
updatePosition()
local endPos = pos + (norm * (part.Size * 0.5))
alignPos.Position = endPos
end)
We can throw objects now. Fun!
And that’s it for the barebones of this grabbing mechanic.
For xbox support, it’s simple. Just add Enum.KeyCode.ButtonL2
in the BindAction call along with Enum.UserInputType.MouseButton1
For mobile support, we’ll have to tinker around with the functions a bit.
First set the createTouchButton property of the BindAction call to true
. Then declare this variable for a debounce on the mobile button.
-- Mobile Support
local touchState = false --False for drag, true for cleanup
Now, time to tinker around with the dragging functions. Mobile has touching mechanics rather than mouse so the ray direction will be from the camera lookvector rather than the unit direction from the mouse ray. This is where userinputservice comes into play
local function cleanUp()
local part_ = part
-- Remove physics
if part then
a0:Destroy()
alignPos:Destroy()
alignRot:Destroy()
part.CanCollide = true
-- Wait until part stops falling or gets deleted to disconnect network ownership
coroutine.wrap(function()
local start = tick()
local latest = tick()
local connection = part_.ChildAdded:Connect(function(child)
if child:IsA("AlignPosition") or child:IsA("AlignOrientation") then
start = tick()
end
end)
while true do
if latest-start >= 0.5 then connection:Disconnect() break end
latest = tick()
game:GetService("RunService").RenderStepped:Wait()
end
if part_ then RepStore.Remotes.SetNetworkOwner:FireServer(part_,false) end
end)()
end
--Cleanup
pickedUpPart = false
part = nil
-- Toggle cool if on mobile
if UIS.TouchEnabled then touchState = not touchState end
end
local function beginDrag()
local mouseRay = mouse.UnitRay
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = getCharacters()
local rayFind = workspace:Raycast(cam.CFrame.Position, (UIS.TouchEnabled and cam.CFrame.LookVector or mouseRay.Direction.Unit) * 10,rayParams)
if rayFind then
local tempPart = rayFind.Instance
if tempPart.Anchored == false then
--Check if its parent is a model and is a primary part
if tempPart.Parent:IsA("Model") and tempPart.Parent ~= workspace and tempPart.Parent.PrimaryPart ~= tempPart then tempPart = tempPart.Parent.PrimaryPart end
if RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == player or RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == nil then
-- Pickup part
pickedUpPart = true
part = tempPart
part.CanCollide = false
--Physics Body
a0 = Instance.new("Attachment")
a0.Parent = part
a0.WorldPosition = part.Position
alignPos = Instance.new("AlignPosition")
alignPos.MaxVelocity = 75
alignPos.MaxForce = 708^2
alignPos.Responsiveness = 100
alignPos.Mode = Enum.PositionAlignmentMode.OneAttachment
alignPos.Parent = part
alignPos.Attachment0 = a0
alignRot = Instance.new("AlignOrientation")
alignRot.MaxTorque = 500
alignRot.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignRot.Parent = part
alignRot.Attachment0 = a0
--Filters
RepStore.Remotes.SetNetworkOwner:FireServer(part,true)
mouse.TargetFilter = part
local connection
-- Toggle cool if on mobile
if UIS.TouchEnabled then touchState = not touchState end
connection = game:GetService("RunService").RenderStepped:Connect(function()
if not part or not part.Parent then cleanUp() connection:Disconnect() return end
if not pickedUpPart then connection:Disconnect() return end
updatePosition()
local endPos = pos + (norm * (part.Size * 0.5))
alignPos.Position = endPos
end)
end
end
end
end
The same goes for the updatePosition() function
local function updatePosition()
if not part then return end
if not part.Parent then return end
-- Setup ignoreList
local ignoreList = getCharacters()
if part.Parent:IsA("Model") and part.Parent.PrimaryPart == part then
for a,b in pairs(part.Parent:GetChildren()) do
if b:IsA("Part") or b:IsA("UnionOperation") or b:IsA("MeshPart") then
table.insert(ignoreList,b)
end
end
else
table.insert(ignoreList,part)
end
local mouseRay = UIS.TouchEnabled and Ray.new(character.Head.Position,cam.CFrame.LookVector) or mouse.UnitRay
local params = RaycastParams.new()
params.FilterDescendantsInstances = ignoreList
params.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(mouseRay.Origin,mouseRay.Direction.Unit * 10,params)
pos = result and result.Position or mouseRay.Origin + (mouseRay.Direction.Unit * 10)
norm = result and result.Normal or Vector3.zero
end
In the contextFunction we now configure so that if the player is on mobile, we call the begin and cleanup functions everytime the context button is released.
local function checkDrag(actionName, inputState, _inputObject)
if UIS.MouseEnabled or UIS.GamepadEnabled then
if inputState == Enum.UserInputState.Begin and not pickedUpPart then
beginDrag()
elseif inputState == Enum.UserInputState.End and pickedUpPart then
cleanUp()
end
elseif UIS.TouchEnabled then
if inputState == Enum.UserInputState.End then
if touchState then
cleanUp()
else
beginDrag()
end
end
end
return Enum.ContextActionResult.Pass
end
CAS:BindAction("dragObject",checkDrag,true,Enum.UserInputType.MouseButton1,Enum.KeyCode.ButtonL2)
At the end of this entire localScript, we add this just in case the humanoid dies.
character.Humanoid.Died:Wait()
CAS:UnbindAction("dragObject")
cleanUp()
With these supports. The entire localScript should look like this. With support for models. The server script remains the same.
local UIS = game:GetService("UserInputService")
local CAS = game:GetService("ContextActionService")
local RepStore = game:GetService("ReplicatedStorage")
-- Data
local pickedUpPart = false
local part
local pos, norm
-- Mobile Support
local touchState = false --False for drag, true for cleanup
-- Physics
local a0
local alignPos
local alignRot
local player = game:GetService("Players").LocalPlayer
local cam = workspace.CurrentCamera
local mouse = player:GetMouse()
local character = player.Character
local function getCharacters()
local characters = {}
for a,b in pairs(game:GetService("Players"):GetPlayers()) do
if b.Character then
table.insert(characters,b.Character)
end
end
return characters
end
local function updatePosition()
if not part then return end
if not part.Parent then return end
-- Setup ignoreList
local ignoreList = getCharacters()
if part.Parent:IsA("Model") and part.Parent.PrimaryPart == part then
for a,b in pairs(part.Parent:GetChildren()) do
if b:IsA("BasePart") or b:IsA("MeshPart") then
table.insert(ignoreList,b)
end
end
else
table.insert(ignoreList,part)
end
local mouseRay = UIS.TouchEnabled and Ray.new(character.Head.Position,cam.CFrame.LookVector) or mouse.UnitRay
local params = RaycastParams.new()
params.FilterDescendantsInstances = ignoreList
params.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(mouseRay.Origin,mouseRay.Direction.Unit * 10,params)
pos = result and result.Position or mouseRay.Origin + (mouseRay.Direction.Unit * 10)
norm = result and result.Normal or Vector3.zero
end
local function cleanUp()
local part_ = part
-- Remove physics
if part then
a0:Destroy()
alignPos:Destroy()
alignRot:Destroy()
part.CanCollide = true
-- Wait until part stops falling or gets deleted to disconnect network ownership
coroutine.wrap(function()
local start = tick()
local latest = tick()
local connection = part_.ChildAdded:Connect(function(child)
if child:IsA("AlignPosition") or child:IsA("AlignOrientation") then
start = tick()
end
end)
while true do
if latest-start >= 0.5 then connection:Disconnect() break end
latest = tick()
game:GetService("RunService").RenderStepped:Wait()
end
if part_ then RepStore.Remotes.SetNetworkOwner:FireServer(part_,false) end
end)()
end
--Cleanup
pickedUpPart = false
mouse.TargetFilter = nil
part = nil
-- Toggle cool if on mobile
if UIS.TouchEnabled then touchState = not touchState end
end
local function beginDrag()
local mouseRay = mouse.UnitRay
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = getCharacters()
local rayFind = workspace:Raycast(cam.CFrame.Position, (UIS.TouchEnabled and cam.CFrame.LookVector or mouseRay.Direction.Unit) * 10,rayParams)
if rayFind then
local tempPart = rayFind.Instance
if tempPart.Anchored == false then
--Check if its parent is a model and is a primary part
if tempPart.Parent:IsA("Model") and tempPart.Parent ~= workspace and tempPart.Parent.PrimaryPart ~= tempPart then tempPart = tempPart.Parent.PrimaryPart end
if RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == player or RepStore.Remotes.GetNetworkOwner:InvokeServer(tempPart) == nil then
-- Pickup part
pickedUpPart = true
part = tempPart
part.CanCollide = false
--Physics Body
a0 = Instance.new("Attachment")
a0.Parent = part
a0.WorldPosition = part.Position
alignPos = Instance.new("AlignPosition")
alignPos.MaxVelocity = 75
alignPos.MaxForce = 708^2
alignPos.Responsiveness = 100
alignPos.Mode = Enum.PositionAlignmentMode.OneAttachment
alignPos.Parent = part
alignPos.Attachment0 = a0
alignRot = Instance.new("AlignOrientation")
alignRot.MaxTorque = 500
alignRot.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignRot.Parent = part
alignRot.Attachment0 = a0
--Filters
RepStore.Remotes.SetNetworkOwner:FireServer(part,true)
mouse.TargetFilter = part
local connection
-- Toggle cool if on mobile
if UIS.TouchEnabled then touchState = not touchState end
connection = game:GetService("RunService").RenderStepped:Connect(function()
if not part or not part.Parent then cleanUp() connection:Disconnect() return end
if not pickedUpPart then connection:Disconnect() return end
updatePosition()
local endPos = pos + (norm * (part.Size * 0.5))
alignPos.Position = endPos
end)
end
end
end
end
local function checkDrag(actionName, inputState, _inputObject)
if UIS.MouseEnabled or UIS.GamepadEnabled then
if inputState == Enum.UserInputState.Begin and not pickedUpPart then
beginDrag()
elseif inputState == Enum.UserInputState.End and pickedUpPart then
cleanUp()
end
elseif UIS.TouchEnabled then
if inputState == Enum.UserInputState.End then
if touchState then
cleanUp()
else
beginDrag()
end
end
end
return Enum.ContextActionResult.Pass
end
CAS:BindAction("dragObject",checkDrag,true,Enum.UserInputType.MouseButton1,Enum.KeyCode.ButtonL2)
character.Humanoid.Died:Wait()
CAS:UnbindAction("dragObject")
cleanUp()
And that is all. This is my first tutorial, so feedback would be appreciated. You can tweak the properties of the physics bodies to make the part more bouncy or have higher velocity when thrown or dragged.