How do I fix my grid block placing system?

I am re-making a block placement system that uses a set grid (right now 1.25 studs) that won’t be changed once initiated in the script. The grid on the X and Z axis work but the Y axis is causing trouble due to a floor system I have where the grid is meant to be offset by the floor (otherwise it looks buggy - video 1). I took this grid calculation method from a video when making this the first time and didn’t completely understand it but somehow got it working with a solution similar to what I attempted to do here.


Without the floor displacement ^


With the floor displacement ^

So I’m wondering how I should fix this. Below is the script where I’ve marked the y offset portion, any assistance on how to improve the calculation system would be appreciated. I’m fine with completely changing the calculation system if that would be ideal. (Also to note that the blocks have the configuration of ‘Floor’)

local x = math.floor(mouse.Hit.X / grid + .7) * grid

local added = 0
if hit:FindFirstChild("BuildConfig") and hit:FindFirstChild("BuildConfig"):GetAttribute("BuildPartType") == "Floor" then
	added = .375 -- added to account for floor
end
-- space = .5 btw
local y = math.floor(mouse.Hit.Y / grid + space) * grid + added
local z = math.floor(mouse.Hit.Z / grid + space) * grid
local normalRay = ray.Normal

if normalRay.X == -1 then
	x = math.floor(mouse.Hit.X / grid - space) * grid
end
if normalRay.Z == -1 then
	z = math.floor(mouse.Hit.Z / grid - space) * grid	
end
if normalRay.Y == -1 then
	y = math.floor(mouse.Hit.Y / grid - space) * grid 
end

The issue is probably coming from where you apply the Y offset

Rn you’re snapping the Y position to the grid first, then adding .375 after

local y = math.floor(mouse.Hit.Y / grid + space) * grid + added

That doesnt really make the grid itself offset. It just moves the final result after the snap has already happened, so the Y grid can end up feeling inconsistent compared to X/Z

For an offset grid, you usually want to subtract the offset before snapping, then add it back after snapping

Something like this

local function snap(value, grid, offset)
	offset = offset or 0
	return math.floor((value - offset) / grid + 0.5) * grid + offset
end

Then you can do

local yOffset = 0

local config = hit:FindFirstChild("BuildConfig")
if config and config:GetAttribute("BuildPartType") == "Floor" then
	yOffset = 0.375
end

local x = snap(mouse.Hit.X, grid, 0)
local y = snap(mouse.Hit.Y, grid, yOffset)
local z = snap(mouse.Hit.Z, grid, 0)

But I’d also suggest not handling every negative normal manually like this

if normalRay.X == -1 then
	x = ...
end

A cleaner way is to move the hit position slightly in the direction of the surface normal before snapping. If your placed block is the same size as the grid, you can do

local normal = ray.Normal
local hitPosition = mouse.Hit.Position

local placePosition = hitPosition + normal * (grid / 2)

local yOffset = 0

local config = hit:FindFirstChild("BuildConfig")
if config and config:GetAttribute("BuildPartType") == "Floor" then
	yOffset = 0.375
end

local x = snap(placePosition.X, grid, 0)
local y = snap(placePosition.Y, grid, yOffset)
local z = snap(placePosition.Z, grid, 0)

something like this

local function snap(value, grid, offset)
	offset = offset or 0
	return math.floor((value - offset) / grid + 0.5) * grid + offset
end

local normal = ray.Normal
local hitPosition = mouse.Hit.Position

-- Move from the surface hit point to the center of the block being placed
local placePosition = hitPosition + normal * (grid / 2)

local yOffset = 0

local config = hit:FindFirstChild("BuildConfig")
if config and config:GetAttribute("BuildPartType") == "Floor" then
	yOffset = 0.375
end

local x = snap(placePosition.X, grid, 0)
local y = snap(placePosition.Y, grid, yOffset)
local z = snap(placePosition.Z, grid, 0)

local finalPosition = Vector3.new(x, y, z)

important part is this

(value - offset) / grid

instead of

(value / grid) + offset

cuz the first one offsets the actual grid, while the second one just offsets the final snapped result

Also if .375 is meant to represent the floor height/thickness, it may be better to calculate it from the floor part’s size/position instead of hardcoding it. But if all your floors are the same size, keeping it as a constant is fine

1 Like

So for the most part what you said worked, only change I had to make was so that the offset would appear on both block and floor surfaces. The config portion ended up looking like this:

local config = hit:FindFirstChild("BuildConfig")
if config and config:GetAttribute("BuildPartType") == "Floor" or config:GetAttribute("BuildPartType") == "Block" then
	yOffset = .375
end

Only thing that I am left with is how to make it so that the grid system can account for different floor heights. I got the .375 from looking in studio what the offset was and have honestly no idea why its that number. The floor height right now is 1 so I might just keep it like that, but it would be useful to have different offsets for different floors that stays consistent when stacking blocks. (also lifting the floor made the offset thing not work :sob:)

Video of it working

Nice glad you got most of it working.

For the height issue, I’d avoid using .375 as a fixed offset. That kind of number usually works only for one specific part size / floor height, so once you lift the floor or use a different height, it starts breaking

Instead of thinking of it as an offset, think of it as

place the bottom of the new block on top of the part that was hit

So you can calculate the Y position from the part you are placing on

Also, I’d slightly change your config check. Right now this

if config and config:GetAttribute("BuildPartType") == "Floor" or config:GetAttribute("BuildPartType") == "Block" then

can cause issues because of how and / or are evaluated. If config is nil, the second config:GetAttribute(...) can still run

I would do it like this instead

local config = hit:FindFirstChild("BuildConfig")
local buildType = config and config:GetAttribute("BuildPartType")

if buildType == "Floor" or buildType == "Block" then
    -- placement logic
end

Then for the Y placement, you can calculate the top of the hit part

local function getPlacementY(hitPart, placingPart)
    local hitTopY = hitPart.Position.Y + (hitPart.Size.Y / 2)
    local placingHalfY = placingPart.Size.Y / 2

    return hitTopY + placingHalfY
end

So your placement code would be something like

local gridSize = 1.25

local function snapToGrid(value)
    return math.round(value / gridSize) * gridSize
end

local function getPlacementY(hitPart, placingPart)
    local hitTopY = hitPart.Position.Y + (hitPart.Size.Y / 2)
    local placingHalfY = placingPart.Size.Y / 2

    return hitTopY + placingHalfY
end

local config = hit:FindFirstChild("BuildConfig")
local buildType = config and config:GetAttribute("BuildPartType")

if buildType == "Floor" or buildType == "Block" then
    local x = snapToGrid(mousePosition.X)
    local z = snapToGrid(mousePosition.Z)
    local y = getPlacementY(hit, previewPart)

    previewPart.Position = Vector3.new(x, y, z)
end

That should make it work with different floor heights cuz the y position is no longer based on a guessed number. Its based on the actual top surface of whatever part youre placing on

For example, if the floor is height 1, its top is

floor.Position.Y + floor.Size.Y / 2

If the floor is height 2, the same formula still works. If you move the floor upward, it also still works because floor.Position.Y changes with it

So stacking blocks should stay consistent too, since placing on a block would use the block’s top surface instead of the floor’s top surface

One extra thing: if your preview is a Model instead of a single Part, then previewPart.Size.Y wont be enough. In that case you would want to use Model:GetBoundingBox() / Model:GetExtentsSize() so youre placing based on the full model size instead of just one part

1 Like

But what if the player is placing a part below or to the side of an already placed part? The getPlacementY() function only calculates to get the position of the y value on top of the selected part. I assume this requires determining what side of the part the player is selecting but is that even possible?

Video of what it looks like (the new grid system is only implemented for the preview part)

The getPlacementY() function only works for placing on top because it only thinks about the Y axis. For placing below or on the sides, you need to know which face of the part the player is pointing at

That is possible. If you’re using a raycast RaycastResult.Normal tells you the direction of the surface that was hit

For example

-- top face
Vector3.new(0, 1, 0)

-- bottom face
Vector3.new(0, -1, 0)

-- right / left faces
Vector3.new(1, 0, 0)
Vector3.new(-1, 0, 0)

-- front / back faces
Vector3.new(0, 0, 1)
Vector3.new(0, 0, -1)

So instead of only calculating the Y offset, you can offset the preview in the direction of the surface normal

Something like this for axis-aligned blocks

local gridSize = 1.25

local function snapToGrid(value)
	return math.round(value / gridSize) * gridSize
end

local function getMainAxis(normal)
	local absX = math.abs(normal.X)
	local absY = math.abs(normal.Y)
	local absZ = math.abs(normal.Z)

	if absX > absY and absX > absZ then
		return Vector3.new(math.sign(normal.X), 0, 0)
	elseif absY > absX and absY > absZ then
		return Vector3.new(0, math.sign(normal.Y), 0)
	else
		return Vector3.new(0, 0, math.sign(normal.Z))
	end
end

local function getHalfSizeInDirection(part, normal)
	return (
		math.abs(normal.X) * part.Size.X / 2 +
		math.abs(normal.Y) * part.Size.Y / 2 +
		math.abs(normal.Z) * part.Size.Z / 2
	)
end

local function getPlacementPosition(raycastResult, placingPart)
	local normal = getMainAxis(raycastResult.Normal)
	local hitPosition = raycastResult.Position

	local placingHalfSize = getHalfSizeInDirection(placingPart, normal)

	local x = snapToGrid(hitPosition.X)
	local y = snapToGrid(hitPosition.Y)
	local z = snapToGrid(hitPosition.Z)

	if normal.X ~= 0 then
		x = hitPosition.X + normal.X * placingHalfSize
	elseif normal.Y ~= 0 then
		y = hitPosition.Y + normal.Y * placingHalfSize
	elseif normal.Z ~= 0 then
		z = hitPosition.Z + normal.Z * placingHalfSize
	end

	return Vector3.new(x, y, z)
end

Then after raycasting

local result = workspace:Raycast(origin, direction, raycastParams)

if result and result.Instance then
	local hit = result.Instance

	local config = hit:FindFirstChild("BuildConfig")
	local buildType = config and config:GetAttribute("BuildPartType")

	if buildType == "Floor" or buildType == "Block" then
		previewPart.Position = getPlacementPosition(result, previewPart)
	end
end

The important part is this

local normal = result.Normal

That normal is what tells you what side of the part was selected

So if the player points at the top of a block, the normal is roughly

Vector3.new(0, 1, 0)

If they point at the bottom, it is

Vector3.new(0, -1, 0)

If they point at the side, it will be one of the X or Z directions

This also removes the need for a fixed .375 offset because the preview is moved by half of its own size in the direction of the face that was clicked

For example, when placing on the right side of a block, it does

x = hitPosition.X + placingPart.Size.X / 2

When placing on top, it does

y = hitPosition.Y + placingPart.Size.Y / 2

When placing below, it does

y = hitPosition.Y - placingPart.Size.Y / 2

So the same logic works for top, bottom and sides

One thing to keep in mind though. This version assumes your blocks arent rotated. If your blocks can be rotated, then you would need to do the same idea in the part’s object space using CFrame:VectorToObjectSpace() / CFrame:PointToObjectSpace(). But for a normal grid building system with straight blocks RaycastResult.Normal should be enough.

I managed to get it working, Thank you for your help!

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