How to snap a part to grid

I am making a game where you can build. Now one problem.

I want to make a grid thingy where the part snaps to a grid. I tried using

CFrame.new(position.X - position.X%3, position.Y - position.Y%3, position.Z - position.Z%3).Position
I also tried math.floor(X / 3) * 3 but it gives the same result

If i want to build up, then place a part on the left side or right side then only one side works, if i try placing the block it goes inside of the block, but not on the other side.

Is there any other method? How do i fix this?
some other info:
The part is a 3x3x3 and the snap value is 3, so it will snap every 3 studs
The baseplate is one single block, there’s no multiple blocks simulating a grid.
I want a snapping similar to Build A Hideout And Sword Fight’s

please help; if you need more information please tell me

The math.floor() way should have worked. Can you show the exact code you used? Also try using math.round instead of floor, it should feel more natural.

that is the whole code, math.floor one is the exact same as the other (x - x%3)

math.floor does not give any better results

EDIT: By whole code I mean that’s the only function I used, its literally Part.Position = and the formula

I need all your code to understand the input position you’re using.

local module = {}

module.SelectedBlock = "Brick"

function module.Snap(position, ins, Object)
	if Object:IsA("BasePart") then
		--if ins.Instance.Name == "BasePlot" then
		--	return Vector3.new(position.X - position.X%3, position.Y + Object.Size.Y/2, position.Z-position.Z%3)
		--else
		--	return ins.Instance.CFrame:PointToWorldSpace(ins.Normal * 3)
		--end
		return CFrame.new(position.X - position.X%3, position.Y - position.Y%3 + Object.Size.Y, position.Z - position.Z%3).Position
	else
		--if ins.Instance.Name == "BasePlot" then
		--	return Vector3.new(position.X - position.X%3, position.Y + Object:GetExtentsSize().Y / 2, position.Z-position.Z%3)
		--else
		--	return ins.Instance.CFrame:PointToWorldSpace(ins.Normal * 3)
		--end
		return CFrame.new(position.X - position.X%3 + 1, position.Y - position.Y%3 + Object.PrimaryPart.Size.Y, position.Z - position.Z%3 + 1).Position
	end
end
function module:placeBlock(x : string, position : Vector3, ins : RaycastResult, ori : Vector3)
	local fObject = script.Parent.Parent.Items:FindFirstChild(x)
	if not fObject then return "Failed" end
	script.Parent.Parent.Construct:InvokeServer({
		TypeOfCall = "Place",
		Object = fObject.Name,
		Position = module.Snap(position, ins, fObject),
		Orientation = ori
	})
end
function module:removeBlock(block : BasePart)
	script.Parent.Parent.Construct:InvokeServer({TypeOfCall = "Remove", Object = block})
end

return module

The code in – is the one that works, but its the most non-effecient thing

1 Like

You seem to have two different positions going into Snap, “position” and the raycast result’s Position (ins.Position), did you mean to use ins.Position?

No, the code i put is fully working;

The commented code is the method that works, but I said its bad, since it uses 2 different methods into one

Sorry Im a bit late but If you still want some help with this here is a bit of code I just made for my building game. Even if ya don’t need it Ima still post it for those who might.

function Snap(HitPosition:Vector3,SnapAmount:number,BlockSize:Vector3,HitNormal:Vector3)
	local NewPos = HitPosition+(HitNormal*(BlockSize/2)) -- Gets the new position of a block
	local X = math.floor((NewPos.X+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the x Axis
	local Y = math.floor((NewPos.Y+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the y Axis
	local Z = math.floor((NewPos.Z+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the z Axis
	local NewBlockPos = Vector3.new(math.round(X*1000)/1000,math.round(Y*1000)/1000,math.round(Z*1000)/1000) -- Removes floating point imprecision.
	return NewBlockPos
end

Also Here is a version for purely snapping to a Position without calculating a block’s new position

function Snap(Position:Vector3,SnapAmount:number)
	local X = math.floor((Position.X+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the x Axis
	local Y = math.floor((Position.Y+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the y Axis
	local Z = math.floor((Position.Z+(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the z Axis
	local NewBlockPos = Vector3.new(math.round(X*1000)/1000,math.round(Y*1000)/1000,math.round(Z*1000)/1000) -- Removes floating point imprecision.
	return NewBlockPos
end

Note: The math.round(Number*1000)/1000 bit is to remove any floating point imprecision, it is not 100% needed if you don’t want it. Also 1000 can be made bigger to allow a higher decimal count.

Also the reason your first attempt didn’t work is most likely because math.floor will cause the position to be slightly offset which is what the +(SnapAmount/2) does is to eliminate that offset.

Another Alternative to math.floor((Position.X+(SnapAmount/2))/SnapAmount)*SnapAmount is
math.round(Position.X/SnapAmount)*SnapAmount but I dont recomment it because it breaks a bit in some parts of the world.

Hopefully this actually works and I didn’t make a mistake while editing it.

EDIT:
I found out some time after making this that the previous response that i gave is NOT the best solution for position snapping. At the time I made do with it, but i came across the issues again in a later project that I worked on, so I made a couple of new functions which work a bit better.

I was also asked whether these work with any snap increment, so my answer is yes. The previous versions should work with most snapping increments, but may behave a bit odd with some increments, and the new functions that I am giving work with any snap increment as far as I am aware. (at any number least within roblox’s capabilities).

The issue with the previous functions that I gave is that they sometimes result in the part clipping below the hit position, so I made two new functions, one which positions the part directly on the hit surface and removes the vertical snapping, essentially only snapping the part along a 2D grid rather than a 3d grid, and the other which snaps the part to the nearest snap position which is above the hit surface.

-- Properly snaps to a 3d grid without the part clipping into the ground
function Snap3D(HitPosition:Vector3,SnapAmount:number,BlockSize:Vector3,HitNormal:Vector3)
	local NewSize = Vector3.new(
		math.max(BlockSize.X/2,SnapAmount),
		math.max(BlockSize.Y/2,SnapAmount),
		math.max(BlockSize.Z/2,SnapAmount)
	)
	
	local GridSize = HitNormal*NewSize
	local BlockPosition:Vector3 = HitPosition+GridSize
	local X = math.ceil((BlockPosition.X-(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the x Axis
	local Y = math.ceil((BlockPosition.Y-(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the y Axis
	local Z = math.ceil((BlockPosition.Z-(SnapAmount/2))/SnapAmount)*SnapAmount -- Gets the position snapped to the snap amount on the z Axis
	if not X or X ~= X then
		X = BlockPosition.X
	end
	if not Y or Y ~= Y then
		Y = BlockPosition.Y
	end
	if not Z or Z ~= Z then
		Z = BlockPosition.Z
	end
	local NewBlockPos = Vector3.new(math.round(X*1000)/1000,math.round(Y*1000)/1000,math.round(Z*1000)/1000) -- Removes floating point imprecision.
	return NewBlockPos
end
-- Snaps the part to the surface
-- Downside: only snaps on the x axis relative to the part (side to side) on sloped surfaces
function surfaceSnap(HitPosition:Vector3,SnapAmount:number,BlockSize:Vector3,HitNormal:Vector3)
	local BlockPosition:Vector3 = HitPosition+(HitNormal*(BlockSize/2))
	
	local GridSize_X = math.round((HitNormal.Magnitude-math.abs(math.round(HitNormal.X))))*SnapAmount
	local GridSize_Y = math.round((HitNormal.Magnitude-math.abs(math.round(HitNormal.Y))))*SnapAmount
	local GridSize_Z = math.round((HitNormal.Magnitude-math.abs(math.round(HitNormal.Z))))*SnapAmount
	
	local X = math.round(BlockPosition.X/GridSize_X)*GridSize_X
	local Y = math.round(BlockPosition.Y/GridSize_Y)*GridSize_Y
	local Z = math.round(BlockPosition.Z/GridSize_Z)*GridSize_Z

	if not X or X ~= X then
		X = BlockPosition.X
	end
	if not Y or Y ~= Y then
		Y = BlockPosition.Y
	end
	if not Z or Z ~= Z then
		Z = BlockPosition.Z
	end

	local NewBlockPos = Vector3.new(math.round(X*1000)/1000,math.round(Y*1000)/1000,math.round(Z*1000)/1000) -- Removes floating point imprecision.
	return NewBlockPos
end
13 Likes