Thank you so much for this! Definitely bookmarking this for future use.
Okay, so I’m posting this as an addition because I have had quite a few people ask me how they could extend this to work on multiple parts such that the surface is no longer continuous or a perfect rectangle.
Instead people are asking how could they have their furniture placement system smoothly work on a set of surfaces like so:
Well to answer that I make a few assumptions. For one we are assuming that all the parts that make up this more complex larger canvas are all relatively axis aligned. This means the complex surface as a whole can be rotated, but each part individually can’t have a unique rotation.
Once we have that simple setup applied we can use AABB collision detection to do some very simple and easy overlap calculations. For our purposes we’d like to be able to find the volume overlap of two parts (if any).
local ZERO = Vector3.new(0, 0, 0)
--
local AABB = {}
AABB.__index = AABB
--
local function vec3Compare(a, b, func)
return Vector3.new(
func(a.x, b.x),
func(a.y, b.y),
func(a.z, b.z)
)
end
--
function AABB.new(a, b)
local self = setmetatable({}, AABB)
self.Min = vec3Compare(a, b, math.min)
self.Max = vec3Compare(a, b, math.max)
return self
end
function AABB.fromPositionSize(pos, size)
return AABB.new(pos + size/2, pos - size/2)
end
--
function AABB:Intersects(aabb)
local aMax, aMin = self.Max, self.Min
local bMax, bMin = aabb.Max, aabb.Min
if (bMin.x > aMax.x) then return false end
if (bMin.y > aMax.y) then return false end
if (bMin.z > aMax.z) then return false end
if (bMax.x < aMin.x) then return false end
if (bMax.y < aMin.y) then return false end
if (bMax.z < aMin.z) then return false end
return true
end
function AABB:Union(aabb)
if (not self:Intersects(aabb)) then
return nil
end
local min = vec3Compare(aabb.Min, self.Min, math.max)
local max = vec3Compare(aabb.Max, self.Max, math.min)
return AABB.new(min, max)
end
--
return AABB
From there we just add an extra check to our placement code that ensures the full area of the item we want to be placed is covered by canvases that we can actually place on!
-- loop through and update every placement's placement code
placements[i].CalcPlacementCFrame = function(self, model, position, rotation)
local cf, size = self:CalcCanvas()
local modelSize = worldBoundingBox(CFrame.Angles(0, rotation, 0), model.PrimaryPart.Size)
-- use AABB to make sure the model has no 2D area on other canvases
local sum = 0
for j = 1, #placements do
local canvasCF, canvasSize = placements[j]:CalcCanvas()
local volume = overlap(
CFrame.new(position) * (canvasCF - canvasCF.p), Vector3.new(modelSize.x, modelSize.z, 1),
canvasCF, Vector3.new(canvasSize.x, canvasSize.y, 1)
)
sum = sum + volume
end
-- only clamp we're fully covered (margin of error included)
local area = modelSize.x * modelSize.z
local clamp = (sum < area - 0.1)
local lpos = cf:pointToObjectSpace(position);
local size2 = (size - Vector2.new(modelSize.x, modelSize.z))/2
local x = clamp and math.clamp(lpos.x, -size2.x, size2.x) or lpos.x
local y = clamp and math.clamp(lpos.y, -size2.y, size2.y) or lpos.y
local g = self.GridUnit
if (g > 0) then
x = math.sign(x)*((math.abs(x) - math.abs(x) % g) + (size2.x % g))
y = math.sign(y)*((math.abs(y) - math.abs(y) % g) + (size2.y % g))
end
return cf * CFrame.new(x, y, -modelSize.y/2) * CFrame.Angles(-math.pi/2, rotation, 0)
end
With those key concepts in mind you can now start to code a proper solution!
Here’s the placefile I used in the above video. Just keep in mind if I were to code this from the get go it prob would have been a bit cleaner from an organization standpoint. Right now it’s a bit spaghetti code-ish, but it does the job
placement added.rbxl (24.9 KB)
I’m no scripter but this seems like a very intuitive article, well written and I enjoyed it despite not having any scripting knowledge!
Another question I had:
How would you make it so you could hold the R key and move your mouse left and right to customize the rotation of the model in any degree, instead of being limited to 45/90 degree rotation?
To do this we only really need to adjust and calculate the rotation argument in the :CalcPlacementCFrame()
method.
-
Adjust our rotation key binding such that when holding down the
R
key we have a boolean that tells us if we should be rotating or not. -
If we aren’t rotating we move around the object as per usual. If we are rotating we save the last position our object was placed at and use that as the position of our placement. We’ll call this the pivot point.
-
We calculate the difference between our mouse and that pivot point and then convert it to the object space of our canvas part’s surface. This ensures that the rotation is relative to what we’re placing on.
-
With that object space vector we can find the angle of rotation with
math.atan2
and update the rotation parameter accordingly.
This is what that looks like:
All the changes are in the ClientPlacement
localscript. That should be enough to get you going. Enjoy!
placement rotation.rbxl (25.4 KB)
It shares a great similarity with the option that allows you to move the articles or move them for your home, which means that if it could be useful for future games. Great topic!!
Why does the saving not work?
I downloaded the finished place but I cant get the saving to work.
Edit: I have tried everything but it dosen’t want to save.
You made this 10 times better my dude.
This really is the most thorough explanation of a placing system, thank you so much EgoMoose!
I have got to the stage where I am having difficulty in placing furniture eg a picture frame, on the vertical face ie. a wall. I would really like to be able for it to work seamlessly on walls connecting at different angles. Anyone got any advice?
I greatly appreciate all the help.
All the info you need should be in the thread.
- The placement system.
- How to put on different faces of a single part.
- How to use multiple faces.
Its all in the thread whether it be the original post or one of my replies. Good luck!
Ok, that is good to know!
Thank you EgoMoose
Hi EgoMoose, it appears the script you posted for the placement system to be used on multiple parts fails to uploaded the saved furniture items. It appears you are missing the fire of the function Placement.fromSerialisation() for the items to load in.
Currently having this same problem and I’m super confused right now. Could you tell me how you got the client to check the collisions? I keep overthinking it and confusing myself.
Thank you for this!! I hope that as developers, we can improve more of the system.
Do you think you could go into more detail on how you got the furniture to only be movable on specific parts? Your reply that seems like it was explaining it is basically a giant wall of code, and on top of that it’s using an OOP approach which I personally find unreadable.
It would be helpful if you could show something that’s more reader-friendly and explains what they heck you’re doing. The cherry on top would be if it were more general. I like the grid-placement but I don’t want to use everything you’ve written here.
Not sure if you figured this out already, but here it is for future people looking at this thread.
The way he was able to make the furniture only move on a specific part(s) was due to the fact that he set the players mouse TargetFilter to the “canvasPart”. Next, he just set the models parent to the target filter.
Mouse = Player:GetMouse()
Mouse.TargetFilter = workspace.CanvasPart
The loading for the more advanced version where you can have multiple canvases does not work. What’s the point of saving the data if you won’t be able to load it. Unless I’m wrong and loading function works, but I honestly have no idea how to specify what exact canvas the model had been placed on and how it will load it ignoring the fact it can be placed on multiple canvases at once. I have tried making it possible but I was kind of met with failure. The saving itself is alright but the loading has a few issues.
Nice tutorial, I will 100% reference this when I will need to make something like this
One problem I ran into was that when I would save and rejoin, it would actually clone multiple of those models in the same position and the more times I would save and rejoin the more it would clone these models.
How can this be adapted so that the rotation of the object still faces the floor?
what if i want to place on smooth terrain how would i do that?