Part doesn't snap to the stud the cursor is pointing at

I’m trying to recreate Work at a Pizza Place’s part dragging system but I am having troubles with snapping the actual part to the stud.

I tried using multiple methods like UserInputService:GetMouseLocation() and Player:GetMouse(), I tried using :ViewportPointToRay() instead of :ScreenPointToRay(), yet I couldn’t get the part to snap onto the stud and stay there properly.

My implemention also has issues because snapping becomes exteremely inconsistent when part has a different size set.

I researched multiple DevForum posts but I couldn’t find a good solution, I’d realy appreciate it if you could help me, since I’m terrible at Vectors and CFrame.

Here’s my code:

-- services
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

-- player & character
local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()

-- camera
local Camera = workspace.CurrentCamera

-- mouse
local Mouse = Player:GetMouse()

-- how much studs to snap to
local StudsSnap = 1

-- drag distance
local DragDistance = 100

-- folder containing interactable objects
local ObjectsFolder = workspace.Objects

-- parameters
local Parameters = RaycastParams.new()

-- ignore our character and interactable objects
Parameters.FilterType = Enum.RaycastFilterType.Exclude
Parameters.FilterDescendantsInstances = {ObjectsFolder, Character}

-- update function
RunService.RenderStepped:Connect(function()
	
	local TargetObject = ObjectsFolder.Part
	local MousePosition = UserInputService:GetMouseLocation()

	-- was told that ViewportPointToRay gives the most accurate mouse position minus the inset
	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y, 1)
	
	-- raycast happens here
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * DragDistance, Parameters)

	if Raycast then
		local PositionRound = Vector3.new(math.round (Raycast.Position.X / StudsSnap) * StudsSnap + 0.5, math.round ( Raycast.Position.Y / StudsSnap) * StudsSnap - 0.5 , math.round (Raycast.Position.Z / StudsSnap) * StudsSnap + 0.5 )

		TargetObject.Position = PositionRound + Raycast.Normal * (TargetObject.Size)
	end
end)

I can provide the .rbxl as well to make this easier to solve.
Baseplate.rbxl (54.1 KB)

2 Likes

I did some experimenting and found that this achieves some promising results:

-- services
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

-- player & character
local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()

-- camera
local Camera = workspace.CurrentCamera

-- mouse
local Mouse = Player:GetMouse()

-- how much studs to snap to
local StudsSnap = 1

-- drag distance
local DragDistance = 100

-- folder containing interactable objects
local ObjectsFolder = workspace.Objects

-- parameters
local Parameters = RaycastParams.new()

-- ignore our character and interactable objects
Parameters.FilterType = Enum.RaycastFilterType.Exclude
Parameters.FilterDescendantsInstances = {ObjectsFolder, Character}

-- update function
RunService.RenderStepped:Connect(function()
	
	local TargetObject = ObjectsFolder.Part
	local MousePosition = UserInputService:GetMouseLocation()

	-- was told that ViewportPointToRay gives the most accurate mouse position minus the inset
	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y, 1)
	
	-- raycast happens here
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * DragDistance, Parameters)

	if Raycast then
		local PositionRound = Vector3.new(
			math.floor (Raycast.Position.X / StudsSnap) + 0.5,
			math.floor ( Raycast.Position.Y / StudsSnap) - 0.5,
			math.floor (Raycast.Position.Z / StudsSnap) + 0.5 
		) * StudsSnap -- Multiplies the entire Vector3
		
		TargetObject.Position = PositionRound + Raycast.Normal * (TargetObject.Size)
	end
end)

I made two changes:

  • Changed math.round to math.floor
  • Instead of multiplying each axis value by StudsSnap, I just multiplied the Vector3. (This is just a less redundant way of doing it. Same functionality as before)

I have noticed that when the mouse is close to the bottom of part, it goes underneath and collides with the floor. I’ll see if I can find a way to fix this.

image

1 Like

After about an hour of tinkering, I found that this works the best:

-- services
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

-- player & character
local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()

-- camera
local Camera = workspace.CurrentCamera

-- mouse
local Mouse = Player:GetMouse()

-- how much studs to snap to
local StudsSnap = 1

-- drag distance
local DragDistance = 100

-- folder containing interactable objects
local ObjectsFolder = workspace.Objects

-- parameters
local Parameters = RaycastParams.new()

-- ignore our character and interactable objects
Parameters.FilterType = Enum.RaycastFilterType.Exclude
Parameters.FilterDescendantsInstances = {ObjectsFolder, Character}

-- last known safe position for targetedObject (meaning the last known position that doesn't have collisions)
local LastSafePosition = Vector3.zero -- Creates empty Vector3 [Vector3.new(0,0,0)]

local GetPartsParams = OverlapParams.new()

GetPartsParams.FilterType = Enum.RaycastFilterType.Exclude
GetPartsParams.FilterDescendantsInstances = {ObjectsFolder, Character}

-- function that checks targetedObject's collisions
local function checkCollision(TargetObject, TargetPosition)
	if #workspace:GetPartsInPart(TargetObject, GetPartsParams) == 0 then
		-- If not colliding, update last known safe position
		LastSafePosition = TargetPosition
	elseif #workspace:GetPartsInPart(TargetObject, GetPartsParams) > 0 then
		-- If colliding, revert to last known safe position
		TargetObject.Position = LastSafePosition
	end
end

-- update function
RunService.RenderStepped:Connect(function()
	local TargetObject = ObjectsFolder.Part
	local MousePosition = UserInputService:GetMouseLocation()

	-- Get the mouse position in 3D space using ViewportPointToRay
	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y, 1)

	-- Perform the raycast
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * DragDistance, Parameters)

	if Raycast then
		local PositionRound

		-- Normalize the X, Y, Z values before rounding
		local snapX = Raycast.Position.X / StudsSnap
		local snapY = Raycast.Position.Y / StudsSnap
		local snapZ = Raycast.Position.Z / StudsSnap
		
		-- Calculate the proper offset X, Y, Z values
		local offsetValueX = TargetObject.Size.X/2
		local offsetValueY = TargetObject.Size.Y/2
		local offsetValueZ = TargetObject.Size.Z/2
		
		-- Adjust rounding based on Raycast.Normal direction
		if Raycast.Normal == Vector3.new(1, 0, 0) or Raycast.Normal == Vector3.new(0, 0, 1) or Raycast.Normal == Vector3.new(0, -1, 0) then
			-- For (1,0,0), (0,0,1), (0,-1,0), adjust as per offset
			PositionRound = Vector3.new(
				math.ceil(snapX) * StudsSnap - offsetValueX,
				math.floor(snapY) * StudsSnap + offsetValueY,
				math.ceil(snapZ) * StudsSnap - offsetValueZ
			)
		else
			-- For other normals, apply different rounding strategy
			PositionRound = Vector3.new(
				math.floor(snapX) * StudsSnap + offsetValueX,
				math.ceil(snapY) * StudsSnap - offsetValueY,
				math.floor(snapZ) * StudsSnap + offsetValueZ
			)
		end

		-- Adjust the position with Raycast.Normal and the target object size
		local TargetPosition = PositionRound + Raycast.Normal * (TargetObject.Size)

		-- Move the object to the calculated position
		TargetObject.Position = TargetPosition

		-- Check for collisions with the new position
		checkCollision(TargetObject, TargetPosition)
	end
end)

I added a collision check that checks to see if the object is colliding with any other parts, and if it is, it’s position is reverted to the last known non-colliding position. I also changed how the position is calculated using offsets, different rounding strategies, and the Raycast.Normal (which was the main issue).

I don’t notice any other issues, and I hope that this helps! Here’s a demo of it working:

1 Like

holy crap, you really saved me, man. thank you so much :heart:

1 Like

hey, if you don’t mind me asking, how would I replicate this to other clients/server? I tried multiple ways but I kept having issues.

Alright, I made it replicable on the server. Basically, I switched it to calculate the position on the server, and the client just fires a RemoteEvent to update it. To reduce firing the RemoteEvent non-stop, I added a check to see if the mouse is in the same spot or if it moved.

Note: it does not well if two players are using the same object, so I would recommend to create a system in your game to allow only one player use the object.

Also, you can control whether the update function is running using the :BindToRenderStep() (enables the function) and :UnbindFromRenderStep() (disables the function).

Regardless, hope this helps!

Baseplate.rbxl (57.1 KB)

thank you, although i spotted a couple of issues with your implementation.

first off, there’s noticable lag (obviously because it sets the position on server), and im pretty worried about the network usage being too high if multiple players start dragging parts around.

second, is that the part in your implementation for some weird reason does not snap to this particular wall:

meanwhile the client only version doesnt have this issue:

i checked what you modified yet nothing is different from the code before, this is really weird.

i tried to do a different approach (send a remote every position change, let server receive it then replicate it to other clients), yet its extremely buggy:

ive been banging my head the entire day trying to replicate it properly and smoothly yet it just refuese to work
The Greatest Bistro.rbxl (58.2 KB)

Your method works. The only issue arises when the part is unanchored.

Also, you should change Remotes.ReplicateVector:FireAllClients(Part, Vector) to Remotes.ReplicateVector:FireClient(Part, Vector) because you are sending the replication to the player that is controlling the part.

1 Like

Update: I found a fix for the unanchored bug. Simply set the orientation as Vector3.zero.
image

1 Like

oh you’re right lol, whoops, my bad

works perfectly now, aside from some minor issues i can fix myself, thank you so much for your help !!!

The Greatest Bistro.rbxl (58.2 KB)

Make sure to also add it the orientation in the server script and the replication event.

Here’s a demo of it working btw:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.