Grid System 0.5 Offset Issue

Hello DevForum users!

I am trying to do a Grid System, with a grid of 3, the problem is, my system dosent work that right, when i try to position the object for some reasson it appears 0.5 studs off?

Issue i wanna solve: the 0.5 studs off.
Additionally i dont want to use specific stuff to if a object haves Even or Uneven position or stuff if its possible.

Some Screenshots:
Right now its looking like this:

I want it to be like:

Heres the code, maybe its not neccesary but just so yall can analize it better.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Modules = ReplicatedStorage:WaitForChild("Modules")
local CommunityModules = Modules:WaitForChild("CommunityModules")

local Warp = require(CommunityModules:WaitForChild("Warp"))

local BuildingObjects = ReplicatedStorage:WaitForChild("BuildingObjects")

local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local Player = game.Players.LocalPlayer
local Mouse = Player:GetMouse()
local Camera = workspace.CurrentCamera
local Add = 0.01

local placementCooldown = false

local rotationAngle = 0
local rotationStep = 90
local placementRotation = CFrame.Angles(0, math.rad(rotationAngle), 0)

shared.currentObject = nil

local Place = Warp.Client("Place")

local gridSize = 3

local function basicSnap(position)
	return Vector3.new(
		math.round((position.X / gridSize) + Add) * gridSize,
		math.round((position.Y / gridSize) + Add) * gridSize,
		math.round((position.Z / gridSize ) + Add) * gridSize
	)
end

local function getWorldSizeAxes(part, sizeModel)
	local cf = part.CFrame
	local size = sizeModel
	local xSize = math.abs(cf.RightVector.X) * size.X + math.abs(cf.UpVector.X) * size.Y + math.abs(cf.LookVector.X) * size.Z
	local ySize = math.abs(cf.RightVector.Y) * size.X + math.abs(cf.UpVector.Y) * size.Y + math.abs(cf.LookVector.Y) * size.Z
	local zSize = math.abs(cf.RightVector.Z) * size.X + math.abs(cf.UpVector.Z) * size.Y + math.abs(cf.LookVector.Z) * size.Z
	return xSize, ySize, zSize
end

local function isEven(n)
	return n % 2 == 0
end

local function snapToGrid(raycastPos: Vector3, raycastNormal: Vector3, template: BasePart, rotation: CFrame)
	local TemplateParent: Model = template.Parent
	local size = TemplateParent:GetExtentsSize()
	local xSize, ySize, zSize = getWorldSizeAxes(template, size)

	local xStuds = math.round((xSize / gridSize) + Add)
	local yStuds = math.round((ySize / gridSize) + Add)
	local zStuds = math.round((zSize / gridSize) + Add)

	local snapped = basicSnap(raycastPos)
	local offset = Vector3.zero

	local absNormal = Vector3.new(math.abs(raycastNormal.X), math.abs(raycastNormal.Y), math.abs(raycastNormal.Z))
	local sizeInNormalDir = Vector3.new(
		absNormal.X * xSize,
		absNormal.Y * ySize,
		absNormal.Z * zSize
	)
	offset += raycastNormal * (sizeInNormalDir.Magnitude / 2)
	local studsInNormalDir = math.round((sizeInNormalDir.Magnitude)+ Add) 
	if not isEven(studsInNormalDir) then --or currentObject.Parent:GetAttribute("IncludeOffset")
		if (math.abs(raycastNormal.X) > 0.5)  and (not shared.currentObject.Parent:GetAttribute("IncludeOffset") or not isEven(studsInNormalDir)) then
			if raycastNormal.X < 0 then
				--	warn("Negative X normal")
				offset = Vector3.new(offset.X, offset.Y, offset.Z)
			else
				--	warn("Positive X normal")
				offset = Vector3.new(offset.X, offset.Y, offset.Z)
			end
		end

		if math.abs(raycastNormal.Y) > 0.5 then
			if raycastNormal.Y < 0 then
				--				warn("Negative Y normal")
				--			warn("Snapped y negative")
				offset = Vector3.new(offset.X, offset.Y - size.Y / 2, offset.Z)
			else
				--warn("Positive Y normal")
				--	warn("Snapped y positive")
				offset = Vector3.new(offset.X, offset.Y - size.Y / 2, offset.Z)
			end
		end

		if (math.abs(raycastNormal.Z) > 0.5)  and (not shared.currentObject.Parent:GetAttribute("IncludeOffset") or not isEven(studsInNormalDir)) then
			if raycastNormal.Z < 0 then
				--	warn("Negative Z normal")
				offset = Vector3.new(offset.X, offset.Y, offset.Z)
			else
				--warn("Positive Z normal")
				offset = Vector3.new(offset.X, offset.Y, offset.Z)
			end
		end
	end
	local corrected = Vector3.new(
		snapped.X + offset.X,
		snapped.Y + offset.Y,
		snapped.Z + offset.Z
	)
	warn(rotation)
	return corrected, rotation
end

local function validatePlacement()
	if shared.currentObject then
		local plot
		if Player:GetAttribute("Plot") then
			plot = workspace.Map.Plots:FindFirstChild(Player:GetAttribute("Plot"))
		else
			return false
		end

		local overlapParams = OverlapParams.new()
		overlapParams.FilterDescendantsInstances = {shared.currentObject.Parent}
		overlapParams.FilterType = Enum.RaycastFilterType.Exclude
		local parts = workspace:GetPartBoundsInBox(shared.currentObject.CFrame, shared.currentObject.Size - Vector3.new(0.1,0.1,0.1), overlapParams)
		local parts2 = workspace:GetPartBoundsInBox(shared.currentObject.CFrame, shared.currentObject.Size, overlapParams)
		local inPlot = false

		for _2, object in pairs(parts2) do
			--	warn("for loop", object.Name)
			if object == plot.Plot.PlotArea then
				--	warn("not inside area")
				inPlot = true
			end
		end

		if not inPlot then
			--warn("not in plot")
			return false
		end

		for _, part in ipairs(parts) do
			if part.Name ~= "PlotArea" then
				return false
			end
		end

		return true, plot
	end
end

local function Rotate(object: Model)
	if object and object.PrimaryPart then
		rotationAngle = (rotationAngle + rotationStep) % 360
		placementRotation = CFrame.Angles(0, math.rad(rotationAngle), 0)
	end
end

local function Update()
	if not shared.currentObject then return end
	local NewRaycast = shared.Utilities.Raycast({shared.currentObject})

	if not NewRaycast then return end
	local Position, rotation = snapToGrid(NewRaycast.Position, NewRaycast.Normal, shared.currentObject.PrimaryPart, placementRotation)
	local FinalPos = Position + Vector3.new(0,shared.currentObject:GetExtentsSize().Y / 2,0)
	shared.currentObject.PrimaryPart.CFrame = CFrame.new(FinalPos) * rotation
end

local function RemovePlaceholder()
	if not shared.currentObject then return end
	shared.currentObject = nil
end

local function AddPlaceholder(Name: string)
	local Object = BuildingObjects:WaitForChild(Name)
	if not Object then return end
	RemovePlaceholder()
	local NewObject = Object:Clone()
	NewObject.Parent = workspace
	shared.currentObject = NewObject
	local Highlight = Instance.new("Highlight", shared.currentObject)
	Highlight.FillTransparency = 0.75
	Highlight.FillColor = Color3.new(0.333333, 1, 0)
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.R then
		Rotate(shared.currentObject)
	end
end)

UserInputService.InputBegan:Connect(function(input, gameProccesed)
	if gameProccesed then return end
	if not shared.currentObject then return end
	
	
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		Place:Fire(true, shared.currentObject.Name, shared.currentObject.PrimaryPart.CFrame)
	end
end)

RunService.Heartbeat:Connect(Update)

AddPlaceholder("Wall")
2 Likes

The math tries to align the part’s edge to the grid instead of its center.

Your snapToGrid function is very complex, which is likely causing the bug. The best way to fix this is to replace that whole function with a much shorter one that does the math correctly.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Modules = ReplicatedStorage:WaitForChild("Modules")
local CommunityModules = Modules:WaitForChild("CommunityModules")
local Warp = require(CommunityModules:WaitForChild("Warp"))
local BuildingObjects = ReplicatedStorage:WaitForChild("BuildingObjects")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local Player = game.Players.LocalPlayer
local Mouse = Player:GetMouse()
local Camera = workspace.CurrentCamera

local placementCooldown = false
local rotationAngle = 0
local rotationStep = 90
local placementRotation = CFrame.Angles(0, math.rad(rotationAngle), 0)
shared.currentObject = nil
local Place = Warp.Client("Place")
local gridSize = 3

local function snapToGrid(raycastPos: Vector3, raycastNormal: Vector3, template: BasePart)
	local objectModel = template.Parent
	local size = objectModel:GetExtentsSize()
	local rotation = CFrame.Angles(0, math.rad(rotationAngle), 0)
	
	local rotatedSize = rotation:VectorToWorldSpace(size)
	rotatedSize = Vector3.new(math.abs(rotatedSize.X), math.abs(rotatedSize.Y), math.abs(rotatedSize.Z))

	local snappedPos = Vector3.new(
		math.floor(raycastPos.X / gridSize + 0.5) * gridSize,
		math.floor(raycastPos.Y / gridSize + 0.5) * gridSize,
		math.floor(raycastPos.Z / gridSize + 0.5) * gridSize
	)
	
	local offset = raycastNormal * (rotatedSize / 2)
	
	return CFrame.new(snappedPos + offset) * rotation
end

local function validatePlacement()
	if shared.currentObject then
		local plot
		if Player:GetAttribute("Plot") then
			plot = workspace.Map.Plots:FindFirstChild(Player:GetAttribute("Plot"))
		else
			return false
		end

		local overlapParams = OverlapParams.new()
		overlapParams.FilterDescendantsInstances = {shared.currentObject.Parent}
		overlapParams.FilterType = Enum.RaycastFilterType.Exclude
		local parts = workspace:GetPartBoundsInBox(shared.currentObject.CFrame, shared.currentObject.Size - Vector3.new(0.1,0.1,0.1), overlapParams)
		local parts2 = workspace:GetPartBoundsInBox(shared.currentObject.CFrame, shared.currentObject.Size, overlapParams)
		local inPlot = false

		for _2, object in pairs(parts2) do
			if object == plot.Plot.PlotArea then
				inPlot = true
			end
		end

		if not inPlot then
			return false
		end

		for _, part in ipairs(parts) do
			if part.Name ~= "PlotArea" then
				return false
			end
		end

		return true, plot
	end
end

local function Rotate(object: Model)
	if object and object.PrimaryPart then
		rotationAngle = (rotationAngle + rotationStep) % 360
		placementRotation = CFrame.Angles(0, math.rad(rotationAngle), 0)
	end
end

local function Update()
	if not shared.currentObject then return end
	
	if not Mouse.Target then return end
	local NewRaycast = {Position = Mouse.Hit.p, Normal = Mouse.TargetSurface.Normal}

	local finalCFrame = snapToGrid(NewRaycast.Position, NewRaycast.Normal, shared.currentObject.PrimaryPart)
	shared.currentObject:SetPrimaryPartCFrame(finalCFrame)
end

local function RemovePlaceholder()
	if not shared.currentObject then return end
    shared.currentObject:Destroy()
	shared.currentObject = nil
end

local function AddPlaceholder(Name: string)
	local Object = BuildingObjects:WaitForChild(Name)
	if not Object then return end
	RemovePlaceholder()
	local NewObject = Object:Clone()
	NewObject.Parent = workspace
	shared.currentObject = NewObject
	local Highlight = Instance.new("Highlight", shared.currentObject)
	Highlight.FillTransparency = 0.75
	Highlight.FillColor = Color3.new(0.333333, 1, 0)
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.R then
		Rotate(shared.currentObject)
	end
end)

UserInputService.InputBegan:Connect(function(input, gameProccesed)
	if gameProccesed then return end
	if not shared.currentObject then return end
	
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		Place:Fire(true, shared.currentObject.Name, shared.currentObject.PrimaryPart.CFrame)
	end
end)

RunService.Heartbeat:Connect(Update)
AddPlaceholder("Wall")

Let me know if you encounter any issues.

2 Likes

You create an offset by adding on the parts size divided by 2 into the placement of said object, especially as it was stated to be half a stud off. In this case you’d only use the X and Z
coordinate, as the Y value is not relevent here, in which you’d have a result of something like this:

-- X/Z Position refers to the original value with the addition of a value
(XPosition + ObjectSize.X/2)
--(Ignore Y as its not relevent here)
(ZPosition + ObjectSize.Z/2)
-- Depending on the type of offset, you may need to subtract the value

So, the Normal is not being obtained, it errors that its trying to get the normal of a enum type. I am not really sure how to fix this without using any raycast.