A lot of these issues are fixed by simply having a grid – which you already do, so what comes next is easier than it would usually be otherwise.
The easiest way I can think of for making a bounding box is to simply set some maximum and minimum position values. You could do this visually, using a part, or could do it somewhere in your code (e.g. you could give each area a ‘settings’ module that stores a Vector3 of the ‘size’ of the bounding box). Region3 works but doesn’t have any (usable) support for rotation – you’d be better off just using CFrames (more on that in your next question).
To actually check where something is within your bounding box, just compare the position values of the model you are placing to the maximum and minimum positions. An easy example of this is:
local WithinX = Position.X >= Minimum.X and Position.X <= Maximum.X
local WithinY = Position.Y >= Minimum.Y and Position.Y <= Maximum.Y
local WithinZ = Position.Z >= Minimum.Z and Position.Z <= Maximum.Z
if WithinX and WithinY and WithinZ then
-- place object
end
Next up is local space. Using local space will make your job much easier for everything else you are doing (notably the above), so this is a good thing to think about. The built-in, proper way to handle local and world spaces is to use CFrames.
CFrame1:ToObjectSpace(CFrame2)
will put CFrame2 into CFrame1’s space (if it was equal to CFrame2, then it would be at the local position ‘0, 0, 0’). CFrame2:ToWorldSpace(CFrame1)
will reverse this and put CFrame1 into the world space (that is, if CFrame2 is the local space it was in, otherwise the returned CFrame won’t be accurate). You can see these documented further in the DevHub: https://www.robloxdev.com/api-reference/datatype/CFrame
This will help you with collisions, your bounding box, and aligning the object to the grid.
Here’s a brief example:
local BoundingPart = workspace.BoundingPart.CFrane
local BoundingPartCFrame = BoundingPart.CFrame
-- Convert into local space
local PlacementModelCFrame = MouseCFrame:ToObjectSpace(BoundingPartCFrame)
-- Round
-- Check bounding box
-- Check collisions
-- Return to world space
PlacementModelCFrame = PlacementModelCFrame:ToWorldSpace(BoundingPartCFrame)
-- Place item
As for collisions, you can handle that in a variety of ways. If you wanted, you could do it all purely in data and just store the positions and sizes (in grid areas, not in studs) of all the objects you have and check all of those when checking collisions. This is the best option compared to the others, IMO.
Alternatively, you could try comparing some sort of root ‘main’ part in each of the models. This is what would work best with collisions, however like you said they may confuse the system and Roblox doesn’t really have any built- in ways for us to handle specifically colliding (not touching) parts.
This is an example of the former option:
local PlacedModels = {
1 = {
1 = {
1 = ModelA; -- Stored at X1, Y1, Z1
2 = ModelB;
}
}
}
local function CheckNoCollisions(X, Y, Z)
-- Beware this will error if the locations you go through don't exist in the table already
-- so make sure to handle cases where X, Y or Z doesn't exist
return PlacedModels[X][Y][Z] == nil
end
This example uses a three-dimensional array, however if you wanted you could turn it into a dictionary instead and do something like PlaceModels[Vector3.new(1, 1, 1)]
.
Finally, I’ve found the best method to align something to a grid is as simple as rounding the position values. If you’re in local space, rounding should be simple and you won’t need to handle things such as weird offsets and rotations.
Here’s an example:
local function RoundNumber(Number, To)
return math.floor((Number / To) + 0.5) * To
end
local function RoundPosition(Position)
local X, Y, Z = Position.X, Position.Y, Position.Z
X = Round(X, 3)
Y = Round(Y, 3)
Z = Round(Z, 3)
return Vector3.new(X, Y, Z)
end
Hopefully this is helpful 