Land expansion for my Placement System

So basically this is my current math for snapping my objects to my first ever placement system grid and it works great clamping everything to the base plot, however I’m not sure how I could go about adding land expansions using this method unless I just expand the plot itself?

I know some variables aren’t defined here and it’s not pretty but that wasn’t really the goal with this post, it was more so to ask how I should approach this or if I should just do it differently entirely.

function PlacementAPI:Snap(Position)
	-- Check if the base plot part exists
	if not BasePlotPart then
		return Position
	end

	local ObjectSize = Vector3.new(
		math.abs(CurrentObjName.PrimaryPart.Size.X),
		math.abs(ObjectHeight),
		math.abs(CurrentObjName.PrimaryPart.Size.Z)
	)

	-- Calculate the bounding box of the rotated object
	local rotatedCFrame = CFrame.new(Position) * CFrame.Angles(0, math.rad(Rotation), 0)
	local objectBoundingBox = CurrentObjName:GetExtentsSize(rotatedCFrame)

	-- Round the position to the nearest grid cell within the bounding box
	local snappedPosition = Vector3.new(
		math.floor((rotatedCFrame.X - BasePlotTopLeft.X) / PlacementAPI.ClientChecks.GridCellSize + 0.5) * PlacementAPI.ClientChecks.GridCellSize + BasePlotTopLeft.X,
		BasePlotHeight,
		math.floor((rotatedCFrame.Z - BasePlotTopLeft.Z) / PlacementAPI.ClientChecks.GridCellSize + 0.5) * PlacementAPI.ClientChecks.GridCellSize + BasePlotTopLeft.Z
	)

	-- Clamp the snapped position to the boundaries of the bounding box
	if Rotation == 0 then
		snappedPosition = Vector3.new(
			math.clamp(snappedPosition.X, BasePlotTopLeft.X + objectBoundingBox.X/2, BasePlotBottomRight.X - objectBoundingBox.X/2),
			0,
			math.clamp(snappedPosition.Z, BasePlotTopLeft.Z + objectBoundingBox.Z/2, BasePlotBottomRight.Z - objectBoundingBox.Z/2)
		)
	elseif Rotation == 90 then
		snappedPosition = Vector3.new(
			math.clamp(snappedPosition.X, BasePlotTopLeft.X + objectBoundingBox.Z/2, BasePlotBottomRight.X - objectBoundingBox.Z/2),
			0,
			math.clamp(snappedPosition.Z, BasePlotTopLeft.Z + objectBoundingBox.X/2, BasePlotBottomRight.Z - objectBoundingBox.X/2)
		)
	end

	-- Convert snappedPosition to a CFrame before undoing the rotation
	local snappedCFrame = CFrame.new(snappedPosition)

	-- Undo object rotation from the snapped position
	local unrotatedCFrame = snappedCFrame * CFrame.Angles(0, math.rad(Rotation), 0)
	
		-- Convert the position of the object's bottom surface back to its center position
		finalPosition = Vector3.new(unrotatedCFrame.X, BasePlotHeight + ObjectHeight/2, unrotatedCFrame.Z)
		
	return finalPosition
end
1 Like

I see you already have a vector3 or vector2 describing the top left and top right of the plot. I set up a demonstration in Roblox Studio to visualize my explanation for you:
image

You could simply edit those values when you expand the land to fit the newly claimed land:


This works great! With one exception, concavity:


With our current idea, we can only claim squares because we only use two corners. If we were to attempt to add the new plot we’d actually end up claiming this much land:

image

Error plots refer to plots we don’t own.


With this in mind, we need to find a solution. The solution I came up with was region subtraction. We’ll store the top left and bottom right points like normal, but we’ll also store exclusion zones!:

image

We’ll have a table containing all of the concave plots and we’ll have a function that checks if the mouse is attempting to move the object over an exclusion zone based on the top left and bottom right of the exclusion zone.


If you have a lot of plots it might be more performant to only store the topleft and bottom right of a group of error plots:

This will reduce the amount of checks you will have to do in the table.


Instead of using math.clamp(), you’ll most likely need to use if statements to check whether the mouse is in a valid location, and if it is then move the object to it.

I don’t have the math on hand currently but you should be able to accomplish this if you keep in mind that you’re just subtracting 2D regions from a larger 2D superregion.

Hope this helps! :slight_smile:

Whoops I fell asleep around the time you sent this, My brain is still turning on as I just woke up but this looks interesting so hopefully I’m smart enough :sweat_smile:

It’s my first attempt at a placement system and I’m proud of it but sometimes the math is detrimental to my brain. I have a test place where I’ve been working on it if you’d like to see what it looks like at the moment, Just ignore the wonky build camera as I just used a random FreeCam script so I could delay making a camera. Test Place

1 Like

I checked your game out and it functions amazingly so far!

I love how you can swap between objects using Q and E.

I have some recommendations that could improve performance and functionality:

Overlap Handling:

The placement works as intended, but you’re able to overlap objects causing walls to be inside other walls. With build systems like this, I have simply set up a check to see if the position is occupied before placing the object.

For the code, it would look something like this:

local plotObjects = {} --// This is a table of all the objects you have ALREADY placed.

local function IsBlueprintIntersecting(blueprintObject)
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = {plotObjects}
	params.FilterType = Enum.RaycastFilterType.Include
	
	local intersecting = workspace:GetPartsInPart(blueprintObject, params)
	if intersecting then
		return intersecting
	end
end

--// blueprintObject is your to-be placed object highlighted as green.
local function AttemptToPlace(blueprintObject)
	if not IsBlueprintIntersecting(blueprintObject) then
		--// Place it!
	else
		--// Make the outline red temporarily or something.
	end
end

Meshes vs. Parts:

It would be beneficial to swap out your primitive parts for MeshParts. MeshParts are more performant than their primitive counterparts and I find that they provide better shadows.

Also for your flooring, you should use planes with only one face rendered. Instead of rendering 4 faces for each floor tile you’d be rendering 1. The ceiling tiles should stay the same though, you want that extra thickness since the sides of the ceiling tiles matter.


Here is a performant mesh kit that I created in Blender:
https://create.roblox.com/marketplace/asset/13146249255/Performant-Meshes-v2

UserInputService Game Processed:

Currently, whenever you type keys like G, X, B, etc. in chat you end up toggling different build modes when you don’t want to. To alleviate this issue we can check if the game has already processed the inputs internally:

local UIS = game:GetService("UserInputService")

--// Use gp to prevent InputBegan from firing when player is typing in chat or in a textbox:
UIS.InputBegan:Connect(function(input, gp)
	if gp then return end --// gp stands for "game processed"; if the game has already processed this input we'll cancel the function.
	if input.KeyCode == Enum.KeyCode.A then
		--// yadayada
	end
end)

Stripes:

Instead of using separate parts for the stripes, you could use SurfaceGUIs on each face that have frames of the desired color. Using frames instead of parts will reduce the number of objects rendered. For the wall on the far right, you would only need to render 1 part, instead of 5.

1 Like

Thanks for all the feedback earlier, Now that I’ve had some free time I went ahead and converted all of those old models into meshes and it essentially cut the part count in half for almost every model!

I started working on the collision but I was having issues where it would count connecting walls on the corners or placing storage shelves against walls as a collision so I’ll have to fix that.

I also added the UIS check so it would stop firing the keybinds when you’re trying to type in chat.

1 Like