Block placing system not working properly with terrain

Hello,

I have followed a tutorial to create a grid based block building system. It works great, however, it does not work properly on terrain. The issue I am having is on terrain, it sometimes follows the grid and sometimes it doesn’t. For example, if I try to place a block, it will be off the grid and will not work. If I move the mouse around, I can find a spot that does snap to the grid properly and from there I can place the block. Here are some examples (sorry for the weird contrast):
Here is when it is off grid:


Notice the highlighted text in console. The value after Preview Position is where the preview block is currently showing up. I cannot place the block here.

In this image, notice that the highlight text is now rounded properly. I can place the block here.

This rounding issue does not occur on regular parts. Only on terrain. Does anyone have any ideas? I will link the entire code used.

Local/Tool script:

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local blockTemplate = ReplicatedStorage:WaitForChild("Block")
local placeBlockEvent = ReplicatedStorage:WaitForChild("PlaceBlock")
local PlacementValidator = require(ReplicatedStorage:WaitForChild("PlacementValidator"))
local camera = workspace.CurrentCamera

local player = game.Players.LocalPlayer
local char = player.Character

local tool = script.Parent.Parent

local castParams = RaycastParams.new()
castParams:AddToFilter(char)

local preview = nil

local function preparePreviewPart(part)
	preview = blockTemplate:Clone()
	preview.Transparency = 0.5
	preview.CanCollide = false
	preview.CanQuery = false
	preview.Parent = workspace
	preview.CastShadow = false
	preview.Anchored = true
end

local function renderPreview()
	local mouseLocation = UserInputService:GetMouseLocation()
	local unitRay = camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
	local cast = workspace:Raycast(unitRay.Origin, unitRay.Direction * 1000, castParams)
	if cast and preview then
		local snappedPosition = PlacementValidator.SnapToGrid(cast.Position)
		local normalCF = CFrame.lookAlong(cast.Position, cast.Normal)
		local relativeSnapped = normalCF:PointToObjectSpace(snappedPosition)
		local xVector = normalCF:VectorToWorldSpace(Vector3.xAxis * -math.sign(relativeSnapped.X))
		local yVector = normalCF:VectorToWorldSpace(Vector3.yAxis * -math.sign(relativeSnapped.Y))
		
		local cf = CFrame.fromMatrix(snappedPosition, xVector, yVector, cast.Normal)
		preview.Position = cf:PointToWorldSpace(blockTemplate.Size/2)

		print('Cast Position ' ..tostring(cast.Position)..' Snapped Position '..tostring(snappedPosition)..' Preview Position '..tostring(preview.Position))
	end
end


tool.Equipped:Connect(function()
	preparePreviewPart(blockTemplate)
	RunService:BindToRenderStep("Preview", Enum.RenderPriority.Camera.Value, renderPreview)
end)

tool.Unequipped:Connect(function()
	RunService:UnbindFromRenderStep("Preview")
	preview:Destroy()
	preview = nil	
end)

tool.Activated:Connect(function()
	placeBlockEvent:FireServer(preview.Position)
end)

Server Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local placeBlockEvent = ReplicatedStorage.PlaceBlock
local blockTemplate = ReplicatedStorage.Block
local PlacementValidator = require(ReplicatedStorage.PlacementValidator)

local function placeBlock(player, position)
	if not PlacementValidator.IsWithinMaxDistance(player, position) or not PlacementValidator.IsWithinGrid(position) then
		return
	end
	local block = blockTemplate:Clone()
	block.Position = position
	block.Parent = workspace
end
placeBlockEvent.OnServerEvent:Connect(placeBlock)

Module Script:

local MAX_DISTANCE = 25
local GRID_SIZE = 4
local PlacementValidator = {}

function PlacementValidator.IsWithinMaxDistance(player, position)
	local playerPos = player.Character and player.Character:GetPivot().Position
	if not playerPos then
		return false
	end
	print("Passed Max Distance Check")
	return (position - playerPos).Magnitude <= MAX_DISTANCE
end

function PlacementValidator.SnapToGrid(pos,)
	return Vector3.new(
		pos.X // GRID_SIZE,
		pos.Y // GRID_SIZE,
		pos.Z // GRID_SIZE
	) * GRID_SIZE
end

function PlacementValidator.IsWithinGrid(pos)
	local blockPos = PlacementValidator.SnapToGrid(pos) + Vector3.one * GRID_SIZE/2
	print("Passed Grid Check")
	return blockPos:FuzzyEq(pos)
end

return PlacementValidator

Here is a video of the issue

1 Like

modified snap to grid function

function PlacementValidator.SnapToGrid(pos)
  local gridSize = GRID_SIZE
  local snappedX = math.floor(pos.X / gridSize) * gridSize
  local snappedY = math.floor(pos.Y / gridSize) * gridSize
  local snappedZ = math.floor(pos.Z / gridSize) * gridSize
  return Vector3.new(snappedX, snappedY, snappedZ)
end

Did you get this from an AI? This code just rewrites what I already had.

Thanks for the reply though.

Sounds like the position also needs to account for terrain height. I’m not sure the mouse will point/return a position under the terrain. Is this working without the terrain?

Yes, it works perfectly without terrain. It takes into account the position of the terrain, I just don’t know at what points or how. It does kind of seem like it takes a position under the terrain, but slightly, since it is sunken underneath the surface by half the height or so. You can sort of see this in the screenshots.

Thanks for your reply.

Updated post with link to a video showing the issue. Watch BlockPlaceTest | Streamable

Update: I think it has to do with the LookAlong, however, I have not had a chance to play around with it. Does anyone have any idea?