Building system doesn't snap to the right place

I’m trying to make a building system, but I seem to have encountered a pretty confusing issue.

External Media

as you can see, in the video, the block doesn’t snap to the right y axis, making it either floating or underground
Here’s the code snippet (it’s not the most optimized code but it works ig)

local STUDS_UNIT = 4
local ANGLE_INCR = 90

local player = game.Players.LocalPlayer
local UserInputService = game:GetService("UserInputService")
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local PlacementEvent = game.ReplicatedStorage.Event.PlacementEvent

local mouse = player:GetMouse()
local currentCamera = workspace.CurrentCamera

local placementBlocks = game.ReplicatedStorage.PlacementBlocks
local ballBlock = placementBlocks.Block

local currentMode = nil
local currentObject = nil

local currentOrientation = CFrame.new()

local rayInstance = nil
local rayPosition = nil
local rayNormal = nil

local canPlace = false

local connections = {}

local function getRotatedSize(size)
	local instanceSize = currentOrientation * CFrame.new(size) -- times the orientation by default size
	instanceSize = Vector3.new(
		math.abs(instanceSize.X),
		math.abs(instanceSize.Y),
		math.abs(instanceSize.Z) --abs ensure we get a positive number
	)

	return instanceSize
end

local function snapToGrid(size : Vector3)
	
	local instanceSize  = getRotatedSize(size)
	local gridPosition = Vector3.new(
		math.floor(rayPosition.X / STUDS_UNIT + .73) * STUDS_UNIT + rayNormal.X * (instanceSize.X / 2), -- ray position is divided by the studs unit (as grid units)
		math.floor(rayPosition.Y / STUDS_UNIT + .73) * STUDS_UNIT + rayNormal.Y * (instanceSize.Y / 2), -- the + .5 at the end is for rounding with the math.floor
		math.floor(rayPosition.Z / STUDS_UNIT + .73) * STUDS_UNIT + rayNormal.Z * (instanceSize.Z / 2) -- after that multiply by .5 to convert back to real world unit
	)
	-- the half instance size is so it sits at the surface, not inside
	
	return gridPosition
end

local function checkCollisions(object, ignoreList)
	local overParams = OverlapParams.new()
	overParams.FilterType = Enum.RaycastFilterType.Exclude
	
	if currentObject and ignoreList then
		table.insert(ignoreList, currentObject)
	end
	
	for _, player in ipairs(game.Players:GetPlayers()) do
		table.insert(ignoreList, player.Character)
	end
	
	overParams.FilterDescendantsInstances = ignoreList or {}
	
	return workspace:GetPartsInPart(object, overParams)
end

local function mouseRaycast()
	local mousePosition = UserInputService:GetMouseLocation()
	local mouseRay = currentCamera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
	
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = CollectionService:GetTagged("Grid")
	rayParams.FilterType  = Enum.RaycastFilterType.Include
	
	local rayResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 100, rayParams)
	
	return rayResult
end

local function activateBuildMode()		
	currentMode = "Build"
	
	if currentObject then
		currentObject:Destroy()
		currentObject = nil
	end

	-- GHOST OBJECT
	currentObject = ballBlock:Clone()
	currentObject.Name = "GhostObject"
	currentObject.Parent = workspace
	currentObject.Anchored = true
	currentObject.CanCollide = false

	if ballBlock:IsA("Part") then
		currentObject.Transparency = .8
	end
end

local function deactivateBuildMode()	
	if currentObject then
		currentObject:Destroy()
		currentObject = nil
	end
end

local function onToolEquip(mouse)
	activateBuildMode()
	
	connections.Input = UserInputService.InputBegan:Connect(function(input, gp)
		if not gp then
			if input.KeyCode == Enum.KeyCode.E then
				if not currentMode then
					currentMode = "Build"

					if currentObject then
						currentObject:Destroy()
						currentObject = nil
					end

					-- GHOST OBJECT
					currentObject = ballBlock:Clone()
					currentObject.Name = "GhostObject"
					currentObject.Parent = workspace
					currentObject.Anchored = true
					currentObject.CanCollide = false

					if ballBlock:IsA("Part") then
						currentObject.Transparency = .8
					end

				else
					currentMode = nil
				end
			elseif input.KeyCode == Enum.KeyCode.R then
				if currentObject and currentMode then
					currentOrientation = CFrame.Angles(0, math.rad(ANGLE_INCR), 0) * currentOrientation
				end
			elseif input.KeyCode == Enum.KeyCode.T then
				if currentObject and currentMode then
					currentOrientation = CFrame.Angles(0, 0, math.rad(ANGLE_INCR)) * currentOrientation
				end
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				if currentMode then
					if currentObject and canPlace then
						PlacementEvent:FireServer(ballBlock, currentObject.CFrame)
					end
				end
			end
		end
	end)
	
	connections.Heartbeat = RunService.Heartbeat:Connect(function()		
		if currentMode then 

			local raycastResult = mouseRaycast()

			if not raycastResult then return end

			rayInstance = raycastResult.Instance
			rayPosition = raycastResult.Position
			rayNormal = raycastResult.Normal

			if rayNormal and rayPosition and rayInstance and currentObject then
				if currentObject:IsA("Part") then
					currentObject.CFrame = CFrame.new(snapToGrid(currentObject.Size)) * currentOrientation --uses cframes to ensure it takes orientation to account to
				end
			end

			local objectCollision = checkCollisions(currentObject, CollectionService:GetTagged("Grid"))
			
			if currentObject then
				if #objectCollision > 0 then
					currentObject.Color = Color3.fromRGB(255, 0, 0)
					canPlace = false
				else
					currentObject.Color = Color3.fromRGB(0, 255, 0)
					canPlace = true
				end
			end
		else
			if currentObject then
				currentObject:Destroy()
				currentObject = nil
			end
		end
	end)
end

local function onToolUnequip()
	deactivateBuildMode()
		
	for _, v in pairs(connections) do
		if v then
			v:Disconnect()
		end
	end
	connections = {}
	
end

script.Parent.Equipped:Connect(onToolEquip)
script.Parent.Unequipped:Connect(onToolUnequip)

I’d appreciate it if anyone helps
Thanks in advance!

1 Like