I’m currently trying to work on a grid for building, it works mostly fine, the issue comes whenever you introduce different sized models (2x4, 2x8, etc).
It’s fine if the block is say 2x6 because it would look fine if centered, but for blocks that can’t be centered should have its moving point to one side of it (think of a 2x2 block on the left side of a 2x8 block).
Another issue is snapping to other blocks, a 2x2 block works fine, however any other size would go inside it, what I want to happen is to have that block snap to it.
Here’s what it looks like a 2x2 block and a 2x4 block.
I have looked around a bit but couldn’t really find anything related to my issue, what I have tried is offsetting it if the half value was odd but that didn’t work out (or i did it wrong).
local uis = game:GetService("UserInputService")
local player = game.Players.LocalPlayer
local part = nil
local gridSize = 2
function roundToGrid(number)
return math.floor((number + gridSize / 2) / gridSize) * gridSize
end
function snapToGrid(position)
return Vector3.new(
roundToGrid(position.X),
roundToGrid(position.Y),
roundToGrid(position.Z)
)
end
function GetPlacementPosition(partSize)
local mouseLocation = uis:GetMouseLocation()
local viewportRay = Camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {part, player.Character}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(viewportRay.Origin, viewportRay.Direction * 1000, raycastParams)
if not result then return nil end
local halfSize = partSize / 2
local offset = Vector3.new(
math.abs(result.Normal.X) * halfSize.X,
math.abs(result.Normal.Y) * halfSize.Y,
math.abs(result.Normal.Z) * halfSize.Z
)
local rawPosition = result.Position + (result.Normal * offset)
local snappedPosition = snapToGrid(rawPosition)
return snappedPosition
end
First of all define which side of the block is the anchor for snapping (offset from center).
And snap the anchor to the grid, then calculate center position.
Also when snapping to other blocks, calculate the combined half sizes to position blocks edge-to-edge.
Use consistent offsets to avoid overlapping.
local gridSize = 2
function roundToGrid(num)
return math.floor((num + gridSize/2) / gridSize) * gridSize
end
function snapToGrid(vec)
return Vector3.new(roundToGrid(vec.X), roundToGrid(vec.Y), roundToGrid(vec.Z))
end
-- Calculate placement considering anchor offset (anchorOffset = Vector3)
function GetPlacementPosition(hitPosition, anchorOffset)
-- Snap anchor position to grid
local snappedAnchor = snapToGrid(hitPosition)
-- Calculate center position from snapped anchor
local centerPos = snappedAnchor + anchorOffset
return centerPos
end
local blockSize = Vector3.new(2, 4, 8) -- your block size
-- Define anchor offset (for example, left edge on X-axis)
local anchorOffset = Vector3.new(blockSize.X / 2, 0, 0) * Vector3.new(-1, 0, 0)
-- Suppose you got your raycast hit position 'hitPos'
local hitPos = Vector3.new(10, 0, 20)
local placementPos = GetPlacementPosition(hitPos, anchorOffset)
-- Set your block position to placementPos
oh yeah i forgot, here it is with the rotation axis
local blockSize = Vector3.new(2, 4, 8) -- Block size
-- Define anchor offset for alignment (e.g., left edge)
local anchorOffset = Vector3.new(blockSize.X / 2, 0, 0) * Vector3.new(-1, 0, 0)
-- Function to get placement position based on hit position, block rotation, and offset
local function GetPlacementPosition(hitPos, blockCFrame, anchorOffset)
-- Rotate the offset to align with the block's orientation
local rotatedOffset = blockCFrame:VectorToWorldSpace(anchorOffset)
-- Calculate the final placement position
local placementPos = hitPos + rotatedOffset
return placementPos
end
-- Suppose you got your raycast hit position and the desired block orientation
local hitPos = Vector3.new(10, 0, 20) -- Example raycast hit position
local blockOrientation = CFrame.Angles(0, math.rad(45), 0) -- Example block rotation (45 degrees on Y-axis)
-- Compute the block placement position
local placementPos = GetPlacementPosition(hitPos, blockOrientation, anchorOffset)
-- Debug output
print("Placement Position:", placementPos)
No way, i have a very simmilar issue
Heres the code that i have
local CosmeticFolder: Folder=workspace:FindFirstChild("CosmeticWorkspaceFolder") or Instance.new("Folder",workspace)
CosmeticFolder.Name="CosmeticWorkspaceFolder"
local ReplicatedStorage:ReplicatedStorage=game:GetService("ReplicatedStorage")
local BlockStorageFolder:Folder=ReplicatedStorage:FindFirstChild("BlockStorage") or Instance.new("Folder",ReplicatedStorage)
BlockStorageFolder.Name="BlockStorage"
local BlockStore:Folder=BlockStorageFolder:FindFirstChild("Blocks") or Instance.new("Folder",BlockStorageFolder)
BlockStore.Name="Blocks"
local HeartBeat=game:GetService("RunService").Heartbeat
local Tool=script.Parent
local ToolEquipped=false
local Players:Players=game:GetService("Players")
local Player:Player=Players.LocalPlayer
local Character:Model=Player.Character or Player.CharacterAdded:Wait()
local Handle:BasePart=Tool:WaitForChild("Handle")
local Mouse:Mouse=Player:GetMouse()
local cam=workspace.CurrentCamera or workspace:WaitForChild("Camera")
local ReplicatedStorage=game:GetService("ReplicatedStorage")
local BlockDefinitions=require(ReplicatedStorage:WaitForChild("BlockDefinitions"))
local Event:RemoteEvent=Tool:WaitForChild("PlaceBlockEvent")
local PreviewBlock:BasePart=BlockDefinitions.Preview
local CurrentSelectedBlock:BasePart=BlockDefinitions.Wood
local GridSize=script:WaitForChild("GridSize").Value
local MaxBuildDist=script:WaitForChild("MaxDistance").Value
local function OnGridSizeChanged()
GridSize=script.GridSize.Value
CurrentSelectedBlock.Size=Vector3.new(GridSize,GridSize,GridSize)
PreviewBlock.Size=Vector3.new(GridSize+.1,GridSize+.1,GridSize+.1)
end
local function OnMaxDistChanged()
MaxBuildDist=script.MaxDistance.Value
end
script.GridSize:GetPropertyChangedSignal("Value"):Connect(OnGridSizeChanged)
script.MaxDistance:GetPropertyChangedSignal("Value"):Connect(OnMaxDistChanged)
CurrentSelectedBlock.Size=Vector3.new(GridSize,GridSize,GridSize)
PreviewBlock.Size=Vector3.new(GridSize+.1,GridSize+.1,GridSize+.1)
local function Snap(Position:Vector3,SnapAmount:number,Offset:Vector3)
if Offset~=nil then
Position+=Offset
end
local X=math.floor((Position.X+(SnapAmount/2))/SnapAmount)*SnapAmount
local Y=math.floor((Position.Y+(SnapAmount/2))/SnapAmount)*SnapAmount
local Z=math.floor((Position.Z+(SnapAmount/2))/SnapAmount)*SnapAmount
--local NewBlockPos = Vector3.new(math.round(X*1000)/1000,math.round(Y*1000)/1000,math.round(Z*1000)/1000) -- Removes floating point imprecision.
local NewBlockPos=Vector3.new(X,Y,Z)
return NewBlockPos
end
local MouseMovedConnection=nil
--local LastMousePos=Vector3.new(99999,99999,99999)
local RayParams=RaycastParams.new()
RayParams.FilterDescendantsInstances={Character,PreviewBlock}
RayParams.FilterType=Enum.RaycastFilterType.Exclude
RayParams.RespectCanCollide=false
RayParams.IgnoreWater=true
local function MousePosFunction() --heres the good juicy math you want
local MousePos=Mouse.Hit.Position
local Orientation=nil
local Unit=(Mouse.Hit.Position-cam.CFrame.Position).Unit
--local NewRaycast=workspace:Raycast(cam.CFrame.Position,Unit*MaxBuildDist,RayParams)
local NewRaycast=workspace:Spherecast(cam.CFrame.Position,GridSize/2,Unit*MaxBuildDist,RayParams)
if NewRaycast~=nil then
if NewRaycast.Position~=nil then
MousePos=NewRaycast.Position
if NewRaycast.Normal~=nil then
if Mouse.Target~=nil then
Orientation=Mouse.Target.CFrame.LookVector
MousePos=Snap((NewRaycast.Position+Vector3.new(0,-GridSize/2))+NewRaycast.Normal*GridSize/2,GridSize,Mouse.Target.Position.Unit)
else
MousePos=Snap((NewRaycast.Position+Vector3.new(0,-GridSize/2))+NewRaycast.Normal*GridSize/2,GridSize)
end
end
else
MousePos=cam.CFrame.Position+(Unit*MaxBuildDist)
end
else
MousePos=cam.CFrame.Position+(Unit*MaxBuildDist)
end
if Orientation~=nil then
PreviewBlock.CFrame=CFrame.new(MousePos,MousePos+Orientation*5)
else
PreviewBlock.CFrame=CFrame.new(MousePos)
end
--PreviewBlock.Position=Snap(MousePos,GridSize)
end
local function OnMouseMoved()
MousePosFunction()
HeartBeat:Wait() HeartBeat:Wait() HeartBeat:Wait() HeartBeat:Wait() --X4 --Only 15? operations per second to not make your phone overheat af. (Please dont ban me.)
MouseMovedConnection=Mouse.Move:Once(OnMouseMoved)
end
local function OnToolEquipped()
PreviewBlock.Parent=CosmeticFolder
MouseMovedConnection=Mouse.Move:Once(OnMouseMoved)
end
local function OnToolUnequipped()
PreviewBlock.Parent=BlockStore
if MouseMovedConnection~=nil then
MouseMovedConnection:Disconnect()
MouseMovedConnection=nil
end
end
local MouseDown:boolean=false
local Id:number=0
local function OnToolActivated()
MouseDown=true
Id+=1
local OldId=Id
while OldId==Id and MouseDown==true do
MousePosFunction()
Event:FireServer(Player.Name,PreviewBlock.CFrame,workspace.CurrentCamera.CFrame or Handle.CFrame,CurrentSelectedBlock.Name,GridSize)--just tells the server to place a block just ignore this
MousePosFunction()
task.wait(Tool.ContinuousPlacingInterval.Value or .35)
end
end
local function OnToolDeactivated()
MouseDown=false
end
--strategic_oof
Tool.Equipped:Connect(OnToolEquipped)
Tool.Unequipped:Connect(OnToolUnequipped)
Tool.Activated:Connect(OnToolActivated)
Tool.Deactivated:Connect(OnToolDeactivated)
Tool.Destroying:Once(OnToolDeactivated)
Tool.Destroying:Once(OnToolUnequipped)
--strategic_oof
I just dropped this because people in my game would of already deobfiscated this script with exploits eventually
Update, I have made some more progress towards it, however there are a couple issues still, it goes through other blocks when rotated, and when moving the mouse over the negative sides of the block it goes inside of it.
local uis = game:GetService("UserInputService")
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera
local part = nil
local gridsize = 2
local function roundToGrid(number)
return math.floor((number + gridsize / 2) / gridsize) * gridsize
end
local function snapToGrid(position)
return Vector3.new(
roundToGrid(position.X),
roundToGrid(position.Y),
roundToGrid(position.Z)
)
end
local function getPlacementPosition(partSize, partCFrame)
local mouseLocation = uis:GetMouseLocation()
local viewportRay = camera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {part, player.Character}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(viewportRay.Origin, viewportRay.Direction * 1000, raycastParams)
if not result then return nil end
local halfSize = partSize / 2
local localNormal = partCFrame:VectorToObjectSpace(result.Normal)
local anchorDirection = Vector3.new(
math.round(localNormal.X),
math.round(localNormal.Y),
math.round(localNormal.Z)
)
local worldAnchorDirection = partCFrame:VectorToWorldSpace(anchorDirection)
local shouldOffset = Vector3.new(1, 1, 1) - Vector3.new(
halfSize.X % 2,
halfSize.Y % 2,
halfSize.Z % 2
)
local anchorOffset = Vector3.new(
-halfSize.X + 1,
-halfSize.Y + 1,
-halfSize.Z + 1
) * shouldOffset
local anchorWorldPosition = partCFrame:PointToWorldSpace(anchorOffset)
local snappedAnchor = snapToGrid(result.Position)
local finalPosition = snappedAnchor - partCFrame:VectorToWorldSpace(anchorOffset)
return finalPosition
end