How do I Align Parts Correctly in a Building System? [Not Solved]

So, I’m trying to make a building system in Roblox Studio, so that when you hold a tool you can place a block. One thing I didn’t realize was so important (and what created the issue) is aligning parts depending on the surface. For example, if you’re building on the ground or the top of a part, you’d expect the part to be built centered above the cursor, so that the part is placed on the ground, not in it. However, when you’re placing a part on the side of another part, you’d expect that the part being created is placed with it centered on the cursor, right up against the other part. So I made code to do this (seen as the block of if-statements in the code below), using the mouse.Hit.TargetSurface. However, the issue comes when adding part rotation. When the part is rotated, suddenly all the checks in the block of if-statements become incorrect, leading to the part not being aligned properly with other parts.

Here is my code, a local script parented to the tool (note: the player.Inventory.Wood is an IntValue storing how much wood the player has, since building requires wood.):

local player = game.Players.LocalPlayer

local uis = game:GetService("UserInputService")

while (not player.Character) or (not player:FindFirstChild("Inventory")) do
	wait()
end

local hover = game.ReplicatedStorage.WoodCube:Clone()
hover.Parent = game.Workspace
hover.BrickColor = BrickColor.new("Electric blue")
hover.Transparency = 0.5
hover.CanCollide = false
hover.Transparency = 1

local mouse = player:GetMouse()

script.Parent.Equipped:Connect(function()
	
	hover.Transparency = 0.5
	mouse.TargetFilter = hover

	while script.Parent.Parent:FindFirstChild("Humanoid") do
		
		if (not player.Inventory:FindFirstChild("Wood")) or not ((hover.Position - script.Parent.Parent.HumanoidRootPart.Position).magnitude <= 30) then

			hover.BrickColor = BrickColor.new("Bright red")

		else
			
			hover.BrickColor = BrickColor.new("Electric blue")
			
		end
		
		if mouse.TargetSurface.Name == "Top" then
			hover.Position = mouse.Hit.Position + Vector3.new(0, (hover.Size.Y / 2), 0)
		elseif mouse.TargetSurface.Name == "Bottom" then
			hover.Position = mouse.Hit.Position + Vector3.new(0, -(hover.Size.Y / 2), 0)
		elseif mouse.TargetSurface.Name == "Left" then
			hover.Position = mouse.Hit.Position + Vector3.new(-(hover.Size.X / 2), 0, 0)
		elseif mouse.TargetSurface.Name == "Right" then
			hover.Position = mouse.Hit.Position + Vector3.new((hover.Size.X / 2), 0, 0)
		elseif mouse.TargetSurface.Name == "Back" then
			hover.Position = mouse.Hit.Position + Vector3.new(0, 0, (hover.Size.Z / 2))
		elseif mouse.TargetSurface.Name == "Front" then
			hover.Position = mouse.Hit.Position + Vector3.new(0, 0, -(hover.Size.Z / 2))
		else
			hover.Position = mouse.Hit.Position + Vector3.new(0, (hover.Size.Y / 2), 0)
		end

		wait()

	end

	hover.Transparency = 1
	
end)

mouse.Button1Down:Connect(function()
	
	if (script.Parent.Parent:FindFirstChild("Humanoid")) and (player.Inventory:FindFirstChild("Wood")) and (not (mouse.Target == nil)) then
		
		if (hover.Position - script.Parent.Parent.HumanoidRootPart.Position).magnitude <= 30 then
			
			game.ReplicatedStorage.PlaceObject:FireServer(hover.CFrame)
			
		end
		
	end
	
end)

uis.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.R then
		hover.Rotation += Vector3.new(0, 90, 0)
	end
end)

I need to find a way to fix my issue, but I can’t think of anything. Any help is appreciated.

p.s. I know my code is far from optimal, feel free to offer suggestions on fixing anything that may be game-breaking.

1 Like

Calculating the distance of the center of the object you are placing from the target point and then summing it to the position should be the solution to your problem. The way you can calculate the distance of a part’s center from another part’s surface is using raycasting.

You can get the distance using the Distance property of a RayCastResult.

To create a ray you need to know the direction it has to follow. In physics a direction is the straight line on which the origin point and the end point lie, if you know the position of the two points you can calculate the direction by subtracting the origin position to the end position.

A few things you could improve in your code:

Don’t use while loops to wait for instances, instead use

local character = player.Character or player.CharacterAdded:Wait() --assign the player character to the variable if it has loaded by the time the script is executed otherwise (or) wait for the character to load and then assign it to the variable

This is not a good way of doing what you want to do. This while loop won’t ever stop so it will keep running and using memory, this is bad because it will be ran and will keep running every time the tool is equipped, you can fix this by adding another condition to the loop:

local equipped = false

-- on tool equipped event
equipped = true

-- on tool unequipped event
equipped = false

-- the while loop should have this condition other than the wood one

while (woodCondition and equipped) do
    -- code here
end
2 Likes

I’ve never used raycasting, could you explain how I’d go about doing that in this case?
Also, thanks for the tips on the code, I’ll take a look at improving on those.

There is a guide for raycasting in the documentation, you can read it here: Raycasting | Roblox Creator Documentation

I don’t think I’m understanding, when would I create the raycast and why couldn’t I just use magnitude?

Also, would this work if you were trying to place one part on a differently sized part?

You would use the raycast when the user places the item and the difference with magnitude is that the ray will give you the distance from the surface and not the center of the part as the ray hits the surface of it first. And yes it would work if you try to place parts on differently sized ones.

Oh, I’m not sure you’re understanding. I want to correctly align the “ghost” of the part (the transparent version you see when hovering the mouse in an area you can build) so that, if the mouse is on a surface, the part becomes centered differently than how it would be normally for an easier user experience. Play any building game and build a wall, then look at the difference in alignment with the mouse when hovering your mouse over the ground and hovering it over the wall.

What exactly is your issue? Images/videos would help understanding the issue better.

1 Like

It’s a bit off topic, but just a heads up: you can reduce that to this:

local surfaceOffsetAxis = mouse.Target and Vector3.FromNormalId(mouse.TargetSurface) or Vector3.yAxis
local surfaceOffsetAmount = (hover.Size * surfaceOffsetAxis).Magnitude / 2 -- The two orthogonal components of surfaceOffsetAxis will always be 0, so only the parallel component contributes. 
local surfaceOffset = surfaceOffsetAxis * surfaceOffsetAmount

hover.Position = mouse.Hit.Position + surfaceOffset

… I’m pretty sure anyway. BTW TargetSurface will never be nil. You have to check if Target is nil if you want special behavior when the mouse isn’t pointing at anything.

1 Like

So these are the images of it working, before rotating the part I’m placing (White dot = mouse cursor, red dot = center of part to be placed, pink line = offset applied):

Then these are the same positions (except the image of it being placed on the ground, which is the same), but now with the part being placed rotated 90 degrees on the y-axis:

The issue is that no matter the rotation of the part being placed, the offset from the part (or terrain) is always the same as if it weren’t. I need a solution to figure out the distance needed to offset the part being placed from the existing one enough that they are touching, but not overlapping, as seen in the first three images.

Looking back at this after a while, I think this actually may be close to the solution, I just need to know how to make this take into account the rotation of the hover when adding the offset. Do you know?
If not, a step-by-step explanation of what the code is doing would help me to figure it out myself.

Aligning parts correctly can be a tricky task, especially when you are dealing with rotations. Here are some suggestions to help you align parts correctly in your building system:

  1. Use CFrame instead of Position

Instead of using the position of the part to align it, you can use its CFrame property. The CFrame property is a combination of the position and rotation of the part, which makes it easier to align the part correctly. You can use the mouse hit position to set the position of the part and the rotation of the part to match the surface normal. For example, if you are building on the top surface of a part, you can set the rotation of the part to match the normal of that surface.

  1. Use SurfaceNormals instead of SurfaceNames

Instead of using the names of the surfaces to align the part, you can use the surface normals. The surface normal is a vector that is perpendicular to the surface of the part. You can use the surface normal to set the rotation of the part to match the surface it is being placed on. This way, you don’t have to worry about the orientation of the part, and it will always be aligned correctly.

  1. Update the Hover Part when the Part is Rotated

When you rotate the part, the orientation of the surfaces changes, which means that the hover part needs to be updated as well. You can update the hover part by setting its rotation to match the rotation of the part you are building. This way, the hover part will be aligned with the part you are building, and you can use it to position the part correctly.

Here’s an example of how you can use CFrame and SurfaceNormals to align parts correctly:

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()

local hover = game.ReplicatedStorage.WoodCube:Clone()
hover.Parent = game.Workspace
hover.BrickColor = BrickColor.new("Electric blue")
hover.Transparency = 0.5
hover.CanCollide = false
hover.Transparency = 1

local function alignPart(part, hitPosition, surfaceNormal)
local surfaceRotation = CFrame.new(Vector3.new(), surfaceNormal)
local partRotation = surfaceRotation:ToObjectSpace(CFrame.new(Vector3.new(), part.CFrame.lookVector)).y
part.CFrame = CFrame.new(hitPosition) * surfaceRotation * CFrame.Angles(0, partRotation, 0)
end

script.Parent.Equipped:Connect(function()
hover.Transparency = 0.5
mouse.TargetFilter = hover

while script.Parent.Parent:FindFirstChild("Humanoid") do
    if (not player.Inventory:FindFirstChild("Wood")) or not ((hover.Position - script.Parent.Parent.HumanoidRootPart.Position).magnitude <= 30) then
        hover.BrickColor = BrickColor.new("Bright red")
    else
        hover.BrickColor = BrickColor.new("Electric blue")
    end

    local hit = mouse.Hit
    if hit then
        local hitPosition = hit.Position
        local surfaceNormal = hit.Normal
        if hit.Instance and hit.Instance:IsA("BasePart") then
            alignPart(hover, hitPosition, surfaceNormal)
        else
            hover.CFrame = CFrame.new(hitPosition)
        end
    end

    wait()
end

hover.Transparency = 1
end)

mouse.Button1Down:Connect(function()
if script.Parent.Parent:FindFirstChild("Humanoid") and player.Inventory:FindFirstChild("Wood") and not (mouse.Target == nil) then
if (hover.Position - script.Parent.Parent.HumanoidRootPart.Position).magnitude <= 30 then
game.ReplicatedStorage.PlaceObject:Fire

I’m getting an error on “hit.Normal” saying that Normal is not a valid member of CFrame (the hit value).

Ah, I see what’s going on. The hit variable is a CFrame, which doesn’t have a Normal property. To get the normal vector of the surface that was hit, you need to use the hit.LookVector property instead.

Replace this line:
local surfaceNormal = hit.Normal

With this line:

local surfaceNormal = -hit.LookVector

This will give you a vector pointing outward from the surface that was hit, which you can use to align the new part. Note that we use the negative of the LookVector because we want a vector pointing away from the surface, not toward it.

I’m now getting an error on “if hit.Instance and hit.Instance:IsA(“BasePart”) then” now, saying “Instance is not a valid member of CFrame”. Also, I’m not sure if this works anyways because my map is using the Terrain system, which isn’t a basepart.

I apologize for the confusion. The hit variable should be a RaycastResult object, not a CFrame . The RaycastResult object has an Instance property that can be used to check if the surface that was hit is a BasePart . So replace this line:

if hit.Instance and hit.Instance:IsA("BasePart") then

With this line:

if hit.Instance and hit.Instance:IsA("BasePart") and not hit.Instance:IsA("Terrain") then

This will check if the surface that was hit is a BasePart and not the Terrain.

Since your map is using a terrain, you may want to handle terrain placement differently than regular BasePart placement. One way to do this is to use the hit.Position property to get the position of the hit, and then use the Terrain:SetMaterialColor() function to modify the terrain at that position.

This still gives the error, Instance isn’t a valid member of hit.

I apologize for the confusion, it seems like I gave you incorrect advice. The hit variable is actually a table that contains information about the result of the WorldToScreenPoint method, and it does not have an Instance property.

Here’s an updated version of the code that should work with the Terrain system:

local mouse = game.Players.LocalPlayer:GetMouse()
local camera = game.Workspace.CurrentCamera
local terrain = game.Workspace.Terrain

mouse.Button1Down:Connect(function()
    local hit = mouse.Hit
    local ray = Ray.new(hit.p, -camera.CFrame.UpVector * 100)
    local _, point = terrain:FindPartOnRay(ray, game.Workspace)
    if point then
        local part = Instance.new("Part")
        part.Anchored = true
        part.Size = Vector3.new(5, 1, 5)
        part.CFrame = CFrame.new(point + Vector3.new(0, 2.5, 0))
        part.Parent = game.Workspace
    end
end)

This code creates a Ray object pointing downwards from the point where the mouse clicked, and uses the FindPartOnRay method of the Terrain object to find the surface that the ray intersects with. If the ray hits the terrain, it creates a new anchored Part at the hit point with a size of 5x1x5 and a height of 2.5 units above the terrain surface.

I apologize, but… are you using ChatGPT for these responses? Because it sounds like it, and very little of this information seems to be correct.

4 Likes

Ah! No! I am just trying my best to help you. x