How can I use math.Clamp to prevent players from building off there plot

So my goal was and still is to make a plot building system. I have finished the movement feature of this system but my problem is the player is supposed to be only able to place things on their plot but they are still able to place objects off there plot slightly just because I used a mouse.Target which is very inaccurate. So someone (a lot of people on the forum told me to use math.Clamp but I can’t figure how to use it and how to find the plot’s CFrame to do this. So here’s my script so you could maybe help find out how to incorporate a mouse.Clamp. I already know it needs to be in the SnapToGrid Function but I still don’t know what the forum members wanted me to try so I hope you can help:

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

local BuildPro = false
local OnPlot = false

local BuildUi = script.Parent.FHOS.Frame.BuildFrame

local MarketPlaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")
game.Players.PlayerAdded:Connect(function()
	local hasPass = false
	
	local success, message = pcall(function()
		hasPass = MarketPlaceService:UserOwnsGamePassAsync(Player.UserId, 11321181)
	end)
	if not success then
		warn("Error while checking if player has pass: " .. tostring(message))
		return
	end
	if hasPass == true then
		BuildPro = true
	end
end)

local Grid = 2

local PosX
local PosY
local PosZ
local Plot
local plotName
local Object

local GivenPlot = game.ReplicatedStorage.BuildEvents.GivenPlot
local PlaceObject = game.ReplicatedStorage.BuildEvents.PlaceObject
GivenPlot.OnClientEvent:Connect(function(plt)
	Plot = plt
	plotName = plt.Name
end)

local Rot = Plot.Orientation.Y

Plot.Touched:Connect(function(hit)
	local plr = game:GetService("Players"):GetPlayerFromCharacter(hit.Parent)
	if plr then
		OnPlot = true
	end
end)
Plot.TouchEnded:Connect(function(hit)
	local plr = game:GetService("Players"):GetPlayerFromCharacter(hit.Parent)
end)
local CanStart = true
local Placing = nil
local CanPlace = nil
local MoveObject = false
local CanPlace
local BuildMode = Instance.new("BoolValue")
BuildMode.Parent = Player
BuildMode = false
BuildSelection = BuildUi.ScrollingFrame
Soil = BuildSelection.SoilSelect.ImageButton
StartBarn = BuildSelection.StartBarnSelect.ImageButton
Tent = BuildSelection.TentSelect.ImageButton
HayBale = BuildSelection.HayBaleSelect.ImageButton
Log = BuildSelection.LogSelect.ImageButton
GreenScreen = BuildSelection.GreenScreenSelect.ImageButton
Flag = BuildSelection.FlagSelect.ImageButton
SmallDeck = BuildSelection.SmallDeck.ImageButton
DeckStairs = BuildSelection.DeckStairs.ImageButton
LargeDeck = BuildSelection.LargeDeck.ImageButton
Chair = BuildSelection.Chair.ImageButton
Bed = BuildSelection.Bed.ImageButton
TallBillboard = BuildSelection.TallBillboard.ImageButton
ShortBillboard = BuildSelection.ShortBillboard.ImageButton
Table = BuildSelection.Table.ImageButton
CampFire = BuildSelection.CampFire.ImageButton

script.Parent.FHOS.Frame.Frame.BuildModeApp.TextButton.MouseButton1Click:Connect(function()
	script.Parent.FHOS.Frame.Frame.Visible = false
	script.Parent.FHOS.Frame.FhBrand.Visible = false
	script.Parent.FHOS.Frame.Power.Visible = false
	script.Parent.FHOS.Frame.Time.Visible = false
	script.Parent.FHOS.Frame.Network.Visible = false
	script.Parent.FHOS.Frame.Battery.Visible = false
	script.Parent.FHOS:TweenPosition(UDim2.new(0.105, 0,0.485, 0), "Out", "Linear", 1, false, nil)
	while script.Parent.FHOS.Rotation > -90 do
		script.Parent.FHOS.Rotation = script.Parent.FHOS.Rotation - 1
		wait()
	end
	BuildMode = true
	if BuildMode == true and OnPlot == true or BuildPro == true then
		Camera.CameraSubject = Plot
	end
end)

local function Round(n, to)
	local to = to or 1
	if to == 0 then return n end
	return math.floor(n/to + 0.5) * to
end

local function SnapToGrid(vector3)
	return Vector3.new(
		Round(vector3.X, Grid),
		Round(vector3.Y, 0),
		Round(vector3.Z, Grid)
	)
end

local function CloneService(Obj)
	if CanStart == true and not CanPlace and not Placing then
		Object = game.ReplicatedStorage.BuildObjects:FindFirstChild(Obj):Clone()
		Object.Parent = game.Workspace
		Mouse.TargetFilter = Object
		
		Object.PrimaryPart.Orientation = Vector3.new(0, Object.PrimaryPart.Orientation.Y, 0)
		
		CanStart = false
		CanPlace = true
		Placing = true
	end
end

Soil.MouseButton1Click:Connect(function()
	CloneService("Soil")
	MoveObject = true
end)
StartBarn.MouseButton1Click:Connect(function()
	CloneService("StarterBarn")
	MoveObject = true
end)
Tent.MouseButton1Click:Connect(function()
	CloneService("Tent")
	MoveObject = true
end)
HayBale.MouseButton1Click:Connect(function()
	CloneService("Haybale")
	MoveObject = true
end)
Log.MouseButton1Click:Connect(function()
	CloneService("Log")
	MoveObject = true
end)
GreenScreen.MouseButton1Click:Connect(function()
	CloneService("GreenScreen")
	MoveObject = true
end)
Flag.MouseButton1Click:Connect(function()
	CloneService("Flag")
	MoveObject = true
end)
SmallDeck.MouseButton1Click:Connect(function()
	CloneService("SmallDeck")
	MoveObject = true
end)
DeckStairs.MouseButton1Click:Connect(function()
	CloneService("DeckStairs")
	MoveObject = true
end)
LargeDeck.MouseButton1Click:Connect(function()
	CloneService("LargeDeck")
	MoveObject = true
end)
Chair.MouseButton1Click:Connect(function()
	CloneService("Chair")
	MoveObject = true
end)
Bed.MouseButton1Click:Connect(function()
	CloneService("Bed")
	MoveObject = true
end)
TallBillboard.MouseButton1Click:Connect(function()
	CloneService("TallBillboard")
	MoveObject = true
end)
ShortBillboard.MouseButton1Click:Connect(function()
	CloneService("ShortBillboard")
	MoveObject = true
end)
Table.MouseButton1Click:Connect(function()
	CloneService("Table")
	MoveObject = true
end)
CampFire.MouseButton1Click:Connect(function()
	CloneService("Campfire")
	MoveObject = true
end)

local function Place()
	if Placing and CanPlace then
		PosX = Object.PrimaryPart.Position.X
		PosY = Object.PrimaryPart.Position.Y
		PosZ = Object.PrimaryPart.Position.Z
		PlaceObject:FireServer(Object.Name, PosX, PosY, PosZ, Rot, Plot)
		Placing = false
		CanPlace = false
		CanStart = true
		
		Object:Destroy()
	end
end

local function GetMousePlacementCFrame(Plot, Object)
	local objectOffset = Vector3.new(0, Object.PrimaryPart.Size.Y/2, 0)
	
	local mouseHit = Mouse.Hit
	
	local mouseHitRelative = Plot.CFrame:ToObjectSpace(mouseHit) + objectOffset
	
	local mouseHitRelativeSnapped = CFrame.new( SnapToGrid(mouseHitRelative.p))
	
	local mouseHitWorldSnapped = Plot.CFrame * mouseHitRelativeSnapped
	
	return mouseHitWorldSnapped
end

local function updateModelPlacement()
	if Plot and (not Plot:IsDescendantOf(Object)) and Mouse.Target.Name == plotName then
		Object:SetPrimaryPartCFrame(GetMousePlacementCFrame(Plot, Object)*CFrame.Angles(0,math.rad(Rot),0))
		Object.PrimaryPart.Orientation = Vector3.new(0, Rot, 0)
	end
end
--TAKE THIS PART OUT FOR RELEASE
BuildMode = true



game:GetService("UserInputService").InputBegan:Connect(function(input, GPE)
	if input.KeyCode == Enum.KeyCode.R and Placing == true and CanPlace == true then
		Rot = Rot + 45
		Object:SetPrimaryPartCFrame(Object.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(Rot), 0))
	end
end)

RunService.RenderStepped:Connect(function()
	Mouse.TargetFilter = Object
	if Placing == true and CanPlace == true and not CanStart then
		Plot.Texture.Transparency = 0
		updateModelPlacement()
	end
end)

Mouse.Button1Down:Connect(Place)

Have you tried the previous answer, please don’t make multiple threads of the same issue, you can just reply to your original post and it will bump the thread automatically, plus it will show that you actually tried to solve the problem by yourself and also make it easier for us to see the whole picture like the video you posted in this thread which you didn’t post in this one yet which would be really helpfull to those seeing your problem.

I believe you should have replied to @RoBoPoJu to ask for the explanation for his solution instead of making a new thread.

Anyways I’ll explain the logic in an crudely drawn MSPaint diagram:

TL;DR move your building on the grid, by moving the mouse position if its outside the grid.

In this case, you will need the mouse position to be centered on that X point in the center of the where the next closest point on the grid where the model should be so that it will remain inside the grid.

1 Like

I tried your other solution and it didn’t work. Now for the finding the mouse.hit.x it would be inaccurate because all the models have different areas they cover so there mouse.hit.x would be different. I hope you can help find a solution… Also I don’t know CFrame but I see that a CFrame has a 50 x and z and I wasn’t sure if that could be incorporated…

Hi!
It should be possible to do what the call a “clipping” operation.

How this works, is you have a point where you want to be, and then you “clip” it to where it can actually legally be.

Lets say the grid goes from 0,0 to 100,100 in world studs.

The mouse hit lands at -20,50, and the building is 10x10

local cursorPosition = Vector3.new(-20,0,50)  --replace with your code
local buildingSize = Vector3.new(10, 0 ,10)
--Make some bounds, which is the edge of the play area, moved inwards by half the size of the building  (center pivot point)
local leftHandEdge = 0 + (buildingSize.x / 2)
local topHandEdge = 0 + (buildingSize.z / 2)
local rightHandEdge = 100 - (buildingSize.x / 2)
local bottomHandEdge = 100 - (buildingSize.z / 2)

local placementX = cursorPosition.x
local placementZ = cursorPosition.z

--Move the placement position back inside the bounds 
if (placementX< leftHandEdge) then  placementX = leftHandEdge  end
if (placementZ< topEdge) then  placementZ = topEdge  end
if (placementX > rightHandEdge) then  placementX = rightHandEdge  end
if (placementZ > bottomEdge) then  placementZ = bottomEdge end
1 Like

Using the code posted by @MrChickenRocket with your own values might work. If you want to use the code I posted in the other thread, here’s a list of what you should give to the function.

  • the part or model
  • the CFrame you are going to set as the CFrame of the part or the model’s PrimaryPart. If the CFrame is not valid, the function calculates the closest valid CFrame. It then returns the result.
  • the center CFrame of the plot’s bottom. Because you seem to have a part’s top surface as the plot, I believe this should be plot.CFrame*CFrame.new(0, plot.Size.Y/2, 0)
  • grid size on the grids’s x axis. This should probably be plot.Size.X
  • grid size on the grids’s z axis. This should probably be plot.Size.Z
  • The grid snap value. This seems to be stored in the Grid variable in your current script.

You could edit your getMousePlacementCFrame function to look like this

local function getMousePlacementCFrame(Plot, Object)
    local objectOffset = Vector3.new(0, object.PrimaryPart.Size.Y/2, 0)
    
    local mouseHit = mouse.Hit
    
    local cfToCheck = mouseHit+objectOffset
    local plotBottomCenter = plot.CFrame*CFrame.new(0, plot.Size.Y/2, 0)
    
    return makeSureObjIsInGrid(object, cfToCheck, plotBottomCenter, plot.Size.X, plot.Size.Z, Grid)
end

If you want a simple explanation of what the function does, it checks each corner of the parts relative to the CFrame given to the function to see if any of them would be outside. It then calculates the new Position based on how far away from the plot edge the corner that is furthest away is. It uses CFrame calculations which means it’ll work with rotated parts too. However, it’s not as efficient as @MrChickenRocket’s code.

Your script is already quite long, so it might be a good idea to put my code in a module script and require that module script. If you use a module script, it needs to return the main function, which is called makeSureObjIsInGrid. To use it in your main script, require the module script from there and store the return value in a variable.

local makeSureObjIsInGrid = require(--[[reference to module script]])

Here’s the code to put in the module.

local function getCornerCfs(cf, size)
	local cornerCfs = table.create(8)
	local hxs, hys, hzs = size.X/2, size.Y/2, size.Z/2
	local i = 0
	-- repeat for each corner of the part using the loop and multipliers in it
	for ix = 1, 2 do
		local xm =(-1)^ix
		for iy = 1, 2 do
			local ym =(-1)^iy
			for iz = 1, 2 do
				local zm =(-1)^iz
				i += 1
				cornerCfs[i] = cf*CFrame.new(xm*hxs, ym*hys, zm*hzs)
			end
		end
	end
	return cornerCfs
end

local function getExtra(cf, size, gridCenterInverse, xLimit, zLimit) -- this is a separate function to avoid code repetition
	local hxs, hys, hzs = size.X/2, size.Y/2, size.Z/2
	local largestExtraX, largestExtraZ = 0, 0
	-- repeat for each corner of the part using the loop and multipliers in it
	local cornerCfs = getCornerCfs(cf, size)
	for i, cornerCf in ipairs(cornerCfs) do
		local cornerX, cornerZ = cornerCf.X, cornerCf.Z
		
		-- getting what the offset of this spesific corner of the part would be to the GRID_CENTER
		-- If the object given to makeSureobjIsInGrid is a model, this is
		-- the CFrame the corner would have if the PrimaryPart of the model would be set to the CFrame given to
		-- the makeSureObjIsInGrid
		-- if this is a single part not in a model, then the corner's Cframe this checks is the cframe where the corner would be if
		-- the CFrame given to makeSureObjIsInGrid would be the CFrame of the part
		local relCf = gridCenterInverse*cornerCf
		
		local relX, relZ = relCf.X, relCf.Z
		local negX, negZ = relX < 0, relZ < 0
		
		-- this is how much the corner is too far away from the GRID_CENTER on the local x and z axes of the GRID_CENTER
		local extraX, extraZ = relX-(negX and -xLimit or xLimit), relZ-(negZ and -zLimit or zLimit)
		
		-- here the script checks if it is further away than any corner checked before
		-- and if it is, the largest value will be updated
		if math.abs(relX) > xLimit and ((negX and extraX < largestExtraX) or (not negX and extraX > largestExtraX)) then
			largestExtraX = extraX
		end
		if math.abs(relZ) > zLimit and ((negZ and extraZ < largestExtraZ) or (not negZ and extraZ > largestExtraZ)) then
			largestExtraZ = extraZ
		end
	end
	return largestExtraX, largestExtraZ
end

-- When using this for models, the cf should be the CFrame that the model's PrimaryPart would be set to if that CFrame was already valid
-- the gridcenter must be a CFrame, the sizes are the sizes of the grid on the local x- and z- axes of the gridCenter
local function makeSureObjIsInGrid(obj, cf, gridCenter, gridXSize, gridZSize, gridUnit)
	local gridCenterInverse, xLimit, zLimit = gridCenter:Inverse(), gridXSize/2, gridZSize/2
	if obj:IsA("BasePart") then
		local extraX, extraZ = getExtra(cf, obj.Size, gridCenterInverse, xLimit, zLimit)
		
		-- relative CFrame of the part on the local axis of GRID_CENTER
		-- (gridCenter is kind of like treated as the center of the world)
		local relCf = gridCenterInverse*cf
		--local newPartCf = gridCenter*(relCf-Vector3.new(extraX, 0, extraZ))
		
		local xOffset, zOffset = relCf.X-extraX, relCf.Z-extraZ
		if gridUnit then
			local absXOffset, absZOffset = math.abs(xOffset), math.abs(zOffset)
			xOffset = math.sign(xOffset)*(absXOffset-absXOffset%gridUnit)
			zOffset = math.sign(zOffset)*(absZOffset-absZOffset%gridUnit)
		end
		local newPartCf = gridCenter*(relCf+Vector3.new(-relCf.X+xOffset, 0, -relCf.Z+zOffset))
		
		return newPartCf
	elseif obj:IsA("Model") then -- you'll probably use models when you make furniture
		
		-- make sure that if you use models the PrimaryPart is set. Otherwise this will error.
		local realPrimaryPartCfInverse = obj:GetPrimaryPartCFrame():Inverse()
		
		local largestExtraX, largestExtraZ = 0, 0 -- variable names may be missleading, the extra values can also be negative
		for i, v in ipairs(obj:GetDescendants()) do
			if v:IsA("BasePart") then
				local realPartCf = v.CFrame
				
				-- giving the getExtra function a CFrame that is relative to the cf the same way as
				-- the current CFrame of the part is relative to the current PrimaryPartCFrame of the model
				local extraX, extraZ = getExtra(cf*(realPrimaryPartCfInverse*realPartCf), v.Size, gridCenterInverse, xLimit, zLimit)
				
				local negX, negZ = extraX < 0, extraZ < 0
				
				-- changing the largestValues if distance from edge is larger than any distance found until this
				-- and updating maxValueif it is
				if math.abs(extraX) > math.abs(largestExtraX) then
					largestExtraX = extraX
				end
				if math.abs(extraZ) > math.abs(largestExtraZ) then
					largestExtraZ = extraZ
				end
			end
		end
		
		-- offset between the two cframes on the local axis of the center CFrame of the grid
		local relCf = gridCenterInverse*cf
		
		local xOffset, zOffset = relCf.X-largestExtraX, relCf.Z-largestExtraZ
		if gridUnit then
			local absXOffset, absZOffset = math.abs(xOffset), math.abs(zOffset)
			xOffset = math.sign(xOffset)*(absXOffset-absXOffset%gridUnit)
			zOffset = math.sign(zOffset)*(absZOffset-absZOffset%gridUnit)
		end
		
		-- This will be set as the CFrame of the PrimaryPart. It's calculated by using the relative offset (relCf)
		-- and substracting the extras from that and then kind of moving the GRID_CENTER on it's own axis with the resulting CFrame.
		local newPrimaryCf = gridCenter*(relCf+Vector3.new(-relCf.X+xOffset, 0, -relCf.Z+zOffset))
		return newPrimaryCf
	end
end

return makeSureObjIsInGrid
1 Like

THANKS SO MUCH!!! I tbh have been trying to fix that for a month. It is a tiny bit glitchy but I will edit some stuff but overall that worked really well. I can finish my game soon!!!

Again, Thanks so much! - @TyPlays_YT