Make dragging tool snap parts to surface

Hey everybody!

I’m currently working on a retro style game, but since the old building tools are completely busted now, I have to recreate them. (the only thing I’m actively using is the move tool)

Currently, I have the new move tool pretty much down solid, though there is one thing that’s heavily bothering me, and that’s making the parts that are being dragged snap to the surface of the part they’re being dragged on.

I’ve tried multiple times to implement this and all those attempts have failed, sadly.

Here’s what I have:

Here’s what I want: (snapping to the surface only, nothing else is needed)

Script:

-- Dragger Local Side
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local setPartEvent = ReplicatedStorage:WaitForChild("SetPart")
local setCollisionGroupEvent = ReplicatedStorage:WaitForChild("SetCollisionGroup")

local player = Players.LocalPlayer
local mouse = player:GetMouse()

local tool = script.Parent
local selectionBox = tool:WaitForChild("SelectionBox")

local dragDistance = 30
local isDragging = false
local partBeingDragged: Part

local selectionBoxConnec

-- Variables --

local function CleanUp()
	if selectionBoxConnec then
		selectionBoxConnec:Disconnect()
	end
	
	partBeingDragged = nil
	selectionBox.Adornee = tool
end

local function PartIsOk(part)
	if part and CollectionService:HasTag(part, "Draggable") and player:DistanceFromCharacter(part.Position) < dragDistance then
		return true
	end
	
	return false
end

local function SetSelectionBox()
	selectionBoxConnec = RunService.Heartbeat:Connect(function()
		if PartIsOk(mouse.Target) and not isDragging then
			selectionBox.Adornee = mouse.Target
			selectionBox.Color3 = selectionBox.Adornee.Color
		else
			selectionBox.Adornee = tool
		end
		
		if isDragging then
			selectionBox.Adornee = partBeingDragged
		end
	end)
end

local function MouseDragging()
	local alignPos = Instance.new("AlignPosition")
	local attachment = Instance.new("Attachment")
	local alignOrien = Instance.new("AlignOrientation")
	local attachment2 = Instance.new("Attachment")
	
	alignPos.Mode = Enum.PositionAlignmentMode.OneAttachment
	alignPos.ApplyAtCenterOfMass = true
	alignPos.RigidityEnabled = true
	alignPos.Attachment0 = attachment
	alignPos.Parent = partBeingDragged
	
	attachment.Position = Vector3.new(0, 0, 0)
	attachment.Parent = partBeingDragged
	
	if partBeingDragged.Name == "Dough" then -- Name checking is due to the parts in my game
		alignOrien = Instance.new("AlignOrientation")
		attachment2 = Instance.new("Attachment")
		
		alignOrien.Mode = Enum.OrientationAlignmentMode.OneAttachment
		alignOrien.RigidityEnabled = true
		alignOrien.Attachment0 = attachment2
		alignOrien.PrimaryAxis = Vector3.new(0, 1, 0)
		alignOrien.Parent = partBeingDragged
		
		attachment2.Position = Vector3.new(0, 0, 0)
		attachment2.Parent = partBeingDragged
	elseif string.match(partBeingDragged.Name, "Box") or partBeingDragged.Name == "Dew" then -- Name checking is due to the parts in my game
		alignOrien = Instance.new("AlignOrientation")
		attachment2 = Instance.new("Attachment")
		
		alignOrien.Mode = Enum.OrientationAlignmentMode.OneAttachment
		alignOrien.RigidityEnabled = true
		alignOrien.Attachment0 = attachment2
		alignOrien.PrimaryAxis = Vector3.new(1, 0, 0)
		alignOrien.Parent = partBeingDragged
		
		attachment2.Position = Vector3.new(0, 0, 0)
		attachment2.Parent = partBeingDragged
	end
	
	
	while partBeingDragged do
		local mousePos = mouse.Hit.Position
		
		if player:DistanceFromCharacter(mousePos) < dragDistance then
			alignPos.Position = mousePos
		end
		
		RunService.Heartbeat:Wait()
	end
	
	alignPos.Position = Vector3.new(mouse.Hit.Position.X, mouse.Hit.Position.Y + 0.25, mouse.Hit.Position.Z)
	
	alignPos:Destroy()
	attachment:Destroy()
	alignOrien:Destroy()
	attachment2:Destroy()
end

-- Upon player clicking, check if target is valid, then, move the part being dragged along with
-- the player's mouse, incrementing it as well
local function BeginDrag()
	if not PartIsOk(mouse.Target) then return end
	isDragging = true
	
	partBeingDragged = mouse.Target
	mouse.TargetFilter = partBeingDragged
	
	setPartEvent:FireServer(partBeingDragged) -- Just here to set the player has network owner of the part
	setCollisionGroupEvent:FireServer(partBeingDragged, true)
	MouseDragging()
end

local function StopDrag()
	local pos
	if partBeingDragged then
		setCollisionGroupEvent:FireServer(partBeingDragged, false)
	end
	
	mouse.TargetFilter = nil
	partBeingDragged = nil
	isDragging = false
end

-- When player equips tool, check if target is draggable, if it is, adorn selection box
tool.Equipped:Connect(function()
	SetSelectionBox()
end)

tool.Unequipped:Connect(CleanUp)

tool.Activated:Connect(BeginDrag)
tool.Deactivated:Connect(StopDrag)

Change your alignPos.Position = mousePos to the below should offset it
then you can adjust it abit as needed or make it more advanced

alignPos.Position = mousePos + Vector3.new(0,partBeingDragged.Size.Y/2,0)

if it will be any other orientation on the surface item or the one dragging you will need to change the code for that

1 Like

heres the snap formula

math.round(x/snap+0.5)*snap

i dont remember much change the / and * with each other

Thanks! This works for snapping it on the ground and preventing it from clipping through. Though, I assume for different orientations I would need to divide the x and z by 2 depending on where I’m dragging it?

This is a bit vague, is “snap” supposed to be the increment at which the part moves, or?

yknow minecraft right? u know how blocks are like snapped to a grid thats just what that line of code does

do that for each x y and z

1 Like

Yeah I get that, but what is the value of “snap” supposed to be?

1 Like

the size of the part aka lets say ur parts size x = 2, y = 1 and z = 4
so for each of them u have to put the snap so

local function snap(Number,Snap)
   return math.round(Number/Snap+ 0.5) * Snap
end
local snappedvector = Vector3.new(snap(--[[urnum]],part.Size.X),snap(--[[urnum]],part.Size.Y),snap(--[[urnum]],part.Size.Z))
1 Like

It would work like this

local part = path.to.part

local mouse = game:GetService("Players").LocalPlayer:GetMouse()

game:GetService("UserInputService").InputChanged:Connect(function(input)
if (input.UserInputType == Enum.UserInputType.MouseMovement) then
local ray = workspace:Raycast(part.Position, Vector3.new(0,10,0))

part.Position = Vector3.new(
math.floor(mouse.Position.X),
part.Size.Y*0.5+ray.Position.Y,
math.floor(mouse.Position.Z)
)
end
end)

Oh, I gotcha. Thanks!

So implementing this, it’s actually mostly what I’m looking for, but I want the part to snap to every 1 stud rather than the part’s size.

For example, the part in this video is snapping to every 4 studs since it’s 4x4x4 in size:

Would you have any idea on how to go about this, or would I just need to mess around with it until it works?

What I have currently:

local x = (math.round(mousePos.X / partBeingDragged.Size.X + 0.5) * partBeingDragged.Size.X)
local y = (math.round(mousePos.Y / partBeingDragged.Size.Y + 0.5) * partBeingDragged.Size.Y)
local z = (math.round(mousePos.Z / partBeingDragged.Size.Z + 0.5) * partBeingDragged.Size.Z)
			
alignPos.Position = Vector3.new(x, y, z)

just round the numbers simply

local x = math.round(mousePos.X)
local y = math.round(mousePos.Y)
local z = math.round(mousePos.Z)
			
alignPos.Position = Vector3.new(x, y, z)
1 Like

Yeah I discovered that a bit after experimenting around a bit lol

Currently, this implementation works best for what I’m doing:

mousePos = Vector3.new(math.floor(mousePos.X), mousePos.Y, math.floor(mousePos.Z))
alignPos.Position = mousePos + Vector3.new(0, partBeingDragged.Size.X / 2, 0)

The only issue with it is that it doesn’t allow snapping against walls you drag the part against, only the floors. I’m not 100% sure if there’s a way to detect if you’re dragging a part against a wall with Roblox’s current features

RayCast allows you to get the Normal Vector of whatever part was hit by the ray. I actually wrote up a solution earlier that utilized the Normal Vector to allowing snapping to floors, ceilings, or walls - but I completely forgot to post it.

local UserInputService = game:GetService ("UserInputService")

local Part = Instance.new ("Part")
Part.Size = Vector3.new (math.random (1, 10), math.random (1, 10), math.random (1, 10))
Part.Anchored = true
Part.Parent = workspace

local Player = game:GetService ("Players").LocalPlayer
local Camera = workspace.CurrentCamera

local Params = RaycastParams.new ()
Params.FilterType = Enum.RaycastFilterType.Blacklist
Params.FilterDescendantsInstances = {Player, Part}

UserInputService.InputChanged:Connect (function (input, gameProcessed)
	if gameProcessed or input.UserInputType ~= Enum.UserInputType.MouseMovement then
		return
		
	end
	
	local mouseLocation = UserInputService:GetMouseLocation ()
	
	local unitRay = Camera:ScreenPointToRay (mouseLocation.X, mouseLocation.Y)
	local result = workspace:Raycast (unitRay.Origin, unitRay.Direction * 500, Params)

	if result and result.Instance then
		local roundedPosition = Vector3.new (math.round (result.Position.X), result.Position.Y, math.round (result.Position.Z))

		Part.Position = roundedPosition + result.Normal * (Part.Size / 2)
	end
end)
2 Likes

you can use TargetSurface to do that. or see what itslevande here said

Wow, thank you! This is exactly what I need!
I completely forgot that Raycast normals existed, that would’ve helped a loooonnngggg time ago… lol

Yeah, I had no idea Roblox natively supported Normal Vectors with Raycasting. I only just found out about them recently and they’ve already helped me solve some problems that I could barely wrap my mind around.

1 Like

Yep. I only found out about them on a previous project I was working on that dealt heavily with Raycasting; very helpful they are.

But nevertheless, thanks for your solution!

1 Like

By chance is this possible to also change on the snap placement such as instead of 1 per stud it can be from 2 and higher?

( Sorry for the post lol. )

With a slight modification, yes. Define SnapDistance to be any number you want it to be, then change the roundedPosition calculation to :

local roundedPosition = Vector3.new (math.round (result.Position.X / SnapDistance) * SnapDistance, 
			result.Position.Y,
			math.round (result.Position.Z / SnapDistance) * SnapDistance)
1 Like