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!