How to correctly snap - Building Script

I am currently scripting a building system, but currently when I try to place a floor next to a wall it ends up looking like this:
wall2
Since I guess my mouse is hitting the part it will only get the position of the centre of the part. But since I am hovering on the edge of the part like in the first picture I’d like it to place at the side of the wall, like any normal building game. Here’s an example:
wall1

Thank you for reading this, if you have any efficient suggestions, please let me know!

1 Like

Are you using raycasting to determine the position of the part? If so, RaycastResult has a property called Normal which is the vector pointing directly outward (i.e. “normal”) to the surface at the raycast position. You can add a multiple of this vector to the position to offset it from the wall.

1 Like

Thanks, is there a way to do it with mouse.Hit? (That’s what I’m using)

I don’t believe so. That would only get the position, not the normal vector. You can use mouse.Hit, however, to construct the ray for the raycast.

Here’s an example of what that might look like. I don’t have a code editor on me right now, so there might be some minor errors.

local MAX_RAYCAST_DISTANCE = 1000 -- set this to the maximum raycast distance
local part = nil -- whatever part you want to move

if mouse.Target == nil then
    return
end

local mousePosition = mouse.Hit
local cameraPosition = workspace.CurrentCamera.CFrame.Position
local direction = (mousePosition - cameraPosition).Unit

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = { mouse.Target }

local result = workspace:Raycast(mousePosition, direction * MAX_RAYCAST_DISTANCE, params)

if result ~= nil then
    local partPosition = result.Position + result.Normal * part.Size.Z / 2
    
    part.CFrame = CFrame.lookAt(partPosition, -normal)
end

This would be in a loop or connected to an update event, of course

1 Like

Thanks so much, but for some reason RayCast isn’t a child of workspace? (even though it is in the documentation lol) workspace:RayCast() Error: RayCast is not a valid member of Workspace "Workspace"

My bad, messed up the capitalization. The ‘c’ should be lowercase. I’ll update my post.

1 Like

No errors, but unfortunately it in most cases only pushes it more to the direction and is super laggy for some reason, but it may be an issue on my part. Could you check my script for errors please?

local mousePosition = mouse.Hit.Position
	local cameraPosition = workspace.CurrentCamera.CFrame.Position
	local direction = (mousePosition - cameraPosition).Unit
	
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Include
	params.FilterDescendantsInstances = { mouse.Target }
	
	local result = workspace:Raycast(mousePosition, direction * 1000, params)
	
	local partPosition = nil
	
	if result ~= nil then
		partPosition = result.Position + result.Normal * Part.Size.Z / 2
		
		PosX = math.floor(partPosition.X / Grid + 0.5) * Grid 
		PosY = math.floor(partPosition.Y / Grid + 0.5) * Grid
		PosZ = math.floor(partPosition.Z / Grid + 0.5) * Grid
	end
Snap()
		
		if PosX ~= nil and PosY ~= nil and PosZ ~= nil then
			if buildSelected.Value == 'Floor' then
				Part.Position = (Vector3.new(PosX, PosY + Part.Size.Z/2, PosZ))
			else
				Part.Position = (Vector3.new(PosX, PosY + Part.Size.Y/2, PosZ))
			end
		end

Looks like I messed up the RaycastParams, it should have been Exclude, not Include. I was able to get this to work:

local MAX_RAYCAST_DISTANCE = 1000
local SNAP_WIDTH = 2
	
local part = nil -- whatever part you're moving
	
local mousePosition = mouse.Hit.Position
local cameraPosition = workspace.CurrentCamera.CFrame.Position
local direction = (mousePosition - cameraPosition).Unit

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { part }

local result = workspace:Raycast(cameraPosition, direction * MAX_RAYCAST_DISTANCE, params)
	
if result ~= nil then
	local rawPosition = result.Position + result.Normal * part.Size.Z / 2
	
	local snappedPosition = Vector3.new(
		math.round(rawPosition.X / SNAP_WIDTH) * SNAP_WIDTH,
		math.round(rawPosition.Y / SNAP_WIDTH) * SNAP_WIDTH,
		math.round(rawPosition.Z / SNAP_WIDTH) * SNAP_WIDTH
	)
	
	part.CFrame = CFrame.lookAt(snappedPosition, snappedPosition - result.Normal)
end
1 Like

100% working, but really really clunky for some reason. Do you know an alternative way?

This is most likely the most straightforward way to accomplish this. What do you mean by “clunky”?

I’ll add you as a developer so you can get a feel for it yourself but I need to add you first, can you accept my friend request?