Hello! I’m working on my building system and I need help making an incredment that snaps like btools.
Here’s my building system vs what I want it to do.
My scripts.
HAMMER CLIENT:
-- funny variables
local rs = game.ReplicatedStorage
local run_service = game["Run Service"]
local uis = game.UserInputService
local player = game.Players.LocalPlayer
local tool = script.Parent
local camera = workspace.CurrentCamera
local remotes = tool.remotes
local pc = require(tool.placement_calculator)
local hammer_gui = script.hammer_gui
local build_button = hammer_gui.menu.build
local delete_button = hammer_gui.menu.delete
local block_menu = hammer_gui.block_menu
local building = true
local blocks = rs:WaitForChild("blocks")
local selected_block = blocks.plastic
local preview = nil
local last_selected_instance = nil -- Keep track of the last selected object
local equipped = false
-- make a ghost thing of a block to see it before placing it
function make_preview()
preview = selected_block:Clone()
preview.Parent = workspace
preview.CanCollide = false
preview.CanQuery = false
preview.Transparency = math.min(preview.Transparency + 0.5, 0.7)
for i, part in pairs(preview:GetDescendants()) do
if part:IsA("BasePart") then
-- Increase the transparency of each part by 50%
part.Transparency = math.min(part.Transparency + 0.5, 0.7)
end
end
end
-- toggle for if the preview should be on or off
function preview_toggle()
if equipped and not preview and building then
make_preview()
end
if not equipped and preview then
preview:Destroy()
preview = nil
end
end
-- render block preview
function render_preview()
local mouse_location = uis:GetMouseLocation()
local unit_ray = camera:ViewportPointToRay(mouse_location.X, mouse_location.Y)
local params = RaycastParams.new()
params.FilterDescendantsInstances = game.Players.LocalPlayer.Character:GetChildren()
params.FilterType = Enum.RaycastFilterType.Exclude
local raycast = workspace:Raycast(unit_ray.Origin, unit_ray.Direction * 5000, params)
if raycast and preview and equipped and building then
local normal_cf = CFrame.lookAt(raycast.Position, raycast.Position + raycast.Normal)
local relative_snapped = normal_cf:PointToObjectSpace(raycast.Position)
local x_vector = normal_cf:VectorToWorldSpace(Vector3.xAxis * -math.sign(relative_snapped.X))
local y_vector = normal_cf:VectorToWorldSpace(Vector3.yAxis * -math.sign(relative_snapped.Y))
local cf = CFrame.fromMatrix(raycast.Position, x_vector, y_vector, raycast.Normal)
local adjustment_direction = raycast.Normal.Unit
local normal_adjustment = adjustment_direction * (math.abs(preview.Size:Dot(adjustment_direction))) / 2
local grid = false
if grid then
preview.Position = pc.snap_to_grid(cf.Position + normal_adjustment)
else
preview.Position = cf.Position + normal_adjustment
end
elseif not building and raycast and equipped then
-- Handle delete box
if raycast.Instance ~= last_selected_instance and raycast.Instance:GetAttribute("block") == player.Name then
-- Remove delete box from the previously selected instance
if last_selected_instance and last_selected_instance:FindFirstChild("delete_box") then
last_selected_instance.delete_box:Destroy()
end
-- Add delete box to the new instance
if not raycast.Instance:FindFirstChild("delete_box") then
local outline = Instance.new("SelectionBox")
outline.Name = "delete_box"
outline.Adornee = raycast.Instance
outline.Parent = raycast.Instance
outline.Color3 = Color3.fromRGB(255, 0, 0)
end
-- Update last_selected_instance
last_selected_instance = raycast.Instance
elseif not raycast.Instance:GetAttribute("block", player.Name) then
if last_selected_instance and last_selected_instance:FindFirstChild("delete_box") then
last_selected_instance.delete_box:Destroy()
end
last_selected_instance = nil
end
else
-- Clear delete box if no instance is selected
if last_selected_instance and last_selected_instance:FindFirstChild("delete_box") then
last_selected_instance.delete_box:Destroy()
end
last_selected_instance = nil
end
preview_toggle()
end
-- place block on server
function activate()
if preview and building then
script.place:Play()
remotes.place_block:FireServer(preview.Position, selected_block.Name)
end
if last_selected_instance and not building then
script.delete:Play()
remotes.delete_block:FireServer(last_selected_instance)
end
end
-- switches between build mode and delete mode
function building_toggle(value)
building = value
if not value and preview then
preview:Destroy()
preview = nil
end
end
-- update equipped value to check if player is holding tool
function equip_toggle(value)
equipped = value
if value then
hammer_gui.Parent = player.PlayerGui
else
hammer_gui.Parent = script
end
end
-- sets up the block menu so u can use it
function set_up_block_menu()
for i, block in pairs(blocks:GetChildren()) do
local block_button = Instance.new("TextButton")
block_button.Name = block.Name
block_button.Text = block.Name
block_button.TextScaled = true
block_button.Parent = block_menu
block_button.MouseButton1Click:Connect(function()
change_block(block)
end)
end
end
function change_block(block)
if block ~= selected_block then
preview:Destroy()
preview = nil
selected_block = block
end
end
-- just funny cally cally things
make_preview()
set_up_block_menu()
run_service:BindToRenderStep("preview", Enum.RenderPriority.Camera.Value, render_preview)
tool.Activated:Connect(activate)
tool.Equipped:Connect(function()
equip_toggle(true)
end)
tool.Unequipped:Connect(function()
equip_toggle(false)
end)
build_button.MouseButton1Click:Connect(function()
building_toggle(true)
end)
delete_button.MouseButton1Click:Connect(function()
building_toggle(false)
end)
PLACEMENT_CALCULATOR:
local GRID_SIZE = 1
local module = {}
function module.snap_to_grid(pos)
return Vector3.new(
math.round(pos.X // GRID_SIZE) * GRID_SIZE,
math.round(pos.Y // GRID_SIZE) * GRID_SIZE,
math.round(pos.Z // GRID_SIZE) * GRID_SIZE
) * GRID_SIZE
end
return module