Grabbing objects

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
image

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.

image

-- 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.

22 Likes

This is exactly what I’ve been looking for!!! :laughing: :grin:

Thanks a MILLION for this!!! :face_holding_back_tears:

3 Likes

Is there multiple local scripts?

Could you supply a screenshot on where everything goes?

2 Likes

This is perfect!! With messing around with lookvector I was also able to implement a scroll in/out, works perfect !!!

1 Like