Creating A Furniture Placement System

Awesome work! I see that the code only supports one specific part called “ExampleCanvas”, how would I be able to make the code support multiple parts? In case I want to have each player have their own canvas which they can place their furniture on.

3 Likes

It’s entirely possible to do and quite easily extended onto the system provided. That being said, it’s something I leave for readers to figure out. Good luck! :grin:

14 Likes

What part of the scripts would I need to edit to allow players to place items on different height surfaces? Currently can’t have placement on say a two storey building, etc. or different heights/levels of floors.

7 Likes

If I understood the script right then I think that’s what you are looking for.

3 Likes

Thank you so much for this! Definitely bookmarking this for future use. :smiley:

3 Likes

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 :grin:

placement added.rbxl (24.9 KB)

69 Likes

I’m no scripter but this seems like a very intuitive article, well written and I enjoyed it despite not having any scripting knowledge!

13 Likes

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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)

43 Likes

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!!

2 Likes

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.

5 Likes

You made this 10 times better my dude.

8 Likes

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. :slightly_smiling_face:

3 Likes

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!

9 Likes

Ok, that is good to know!

Thank you EgoMoose :slightly_smiling_face:

2 Likes

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.

4 Likes

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.

2 Likes

Thank you for this!! I hope that as developers, we can improve more of the system.

3 Likes

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.

9 Likes

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
5 Likes

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.

6 Likes