Sometimes builds go inside of each other and sometimes they don't with my building system

Hi, I’m currently trying to make a building system but I ran into a problem recently. Sometimes when I place I start to place down a build the build will go inside of something instead of resting on the side or the top of it. Here’s what I mean:


As you can see it works until a go to a certain angle and that it starts to phase through the other blocks. I don’t know why this is happening but if somebody could help me that would be great.

Code:

local mouseRay = mouse.UnitRay
			
local rayCastParams = RaycastParams.new()
rayCastParams.FilterType = Enum.RaycastFilterType.Blacklist
rayCastParams.FilterDescendantsInstances = {GhostItem, player.Character}
			
local castRay = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 500, rayCastParams)
local ignoreList = {GhostItem, player.Character}
			
if castRay then
	local PosX = math.floor(castRay.Position.X / GridSize) * GridSize + GhostItem.PrimaryPart.Size.X/2
	local PosY = math.floor(castRay.Position.Y / GridSize) * GridSize + GhostItem.PrimaryPart.Size.Y/2
	local PosZ = math.floor(castRay.Position.Z / GridSize) * GridSize + GhostItem.PrimaryPart.Size.Z/2
					
	GhostItem:SetPrimaryPartCFrame(CFrame.new(PosX,PosY,PosZ))
end

Since you’re using math.floor, the blocks will tend to go in in the negative axis direction.

math.floor(castRay.Position.X / GridSize + 0.5) may alleviate the issue somewhat, but I doubt it.

A more proper solution would use the RaycastResult.Normal value as well, to place the block in the correct adjacent grid space.

Ideally, you would figure out which grid space the block you’re pointing at is on, and then figure out which direction the face you clicked is facing, and get the adjacent block like that.

A hackier solution that may not work for all edge cases would be to just add the normal vector to your hit position (and also maybe round instead of floor? Dunno):

	local offsetPos = castRay.Position + castRay.Normal
	local PosX = math.floor(offsetPos.X / GridSize + 0.5) * GridSize + GhostItem.PrimaryPart.Size.X/2
	local PosY = math.floor(offsetPos.Y / GridSize + 0.5) * GridSize + GhostItem.PrimaryPart.Size.Y/2
	local PosZ = math.floor(offsetPos.Z / GridSize + 0.5) * GridSize + GhostItem.PrimaryPart.Size.Z/2

I was searching around and the thought of putting castRay.Normal into the code popped up and that might help or solve the issue. Though I don’t know how I would include castRay.Normal into the code to actually make it work. The code you provided just produced the same issue.

Try removing the + 0.5 part.

Kind of worked? Half of it now sticks out instead of the whole block being inside of the part. I’ll try something and see if it works.

Edit: my solution didn’t work :confused:

Sounds like you’ll need a real solution then :slight_smile:

  1. Calculate the position on a grid “edge” that you’re closest to
  2. Figure out which axis direction the normal is in
  3. Offset your position based on that.

Something like this (untested but the comments should push you in the right direction):

-- returns 0 if x is 0, 1 if x > 0, -1 if x < 0
local function sign(x)
    if (x == 0) then return 0; end
    if (x > 0) then return 1; end
    return - 1;
end

-- get the axis-aligned unit vector that vec is closest to
function SnapToAxis(vec)
    local lx = math.abs(vec.X)
    local ly = math.abs(vec.Y)
    local lz = math.abs(vec.Z)
    
    if (lx > ly and lx > lz) then
        return Vector3.new(sign(vec.X), 0, 0);
    elseif (ly > lx and ly > lz) then
        return Vector3.new(0, sign(vec.Y), 0);
    else
        return Vector3.new(0, 0, sign(vec.Z));
    end
end

-- Given a grid size, part size, mouse position, and hit normal, return the
-- center position of the part in the adjacent cell.
-- Note: This will align parts by their centers. That may or may not be what you
-- want.
local function GetPlacementPosition(gridSize, partSize, position, normal)

	-- 1. find the grid corner that the mouse is closest to
    local gridCorner = Vector3.new(
        math.floor(position.X / gridSize + 0.5) * gridSize
        math.floor(position.Y / gridSize + 0.5) * gridSize
        math.floor(position.Z / gridSize + 0.5) * gridSize
    )
    
    -- 2. get the axis that the surface we're touching is closest to
    local snappedNormal = SnapToAxis(normal)

    -- 3. from the corner, move the part outwards in the direction of the axis
    -- by half its size on that axis
    return gridCorner + partSize * snappedNormal / 2
end

edit: Instead of using the Normal to snap, it might make more sense to use (mouse position - part hit position). They’d be the same for simple parts, but it might be useful if you start placing whole models or more complex parts.

5 Likes

Even with the notes, I’m extremely confused on where I would call these functions in my script. I barely even know what half of that means, I’m only comfortable with basic math functions. Could you dumb it down a little bit for me to understand it? (I’m really sorry :sweat_smile:)

No problem, it’s a bunch of lines of code to dump on you.

You would just call the main function here:

if castRay then
	local pos = GetPlacementPosition(GridSize, GhostItem.PrimaryPart.Size, castRay.Position, castRay.Normal)
					
	GhostItem:SetPrimaryPartCFrame(CFrame.new(pos))
end

sign(x) is pretty straightforward, just “return the sign (+1, -1, or 0) of this number”.

SnapToAxis just returns a vector 3, aligned to one of the six axis directions, that the given vector is the closest to pointing to. Basically like “if the x component of this vector is the longest one, we’re closest to the x-axis, so use the sign of the X component to figure out if its +X or -X, and return (1, 0, 0) or (-1, 0, 0).”

GetPlacementPosition is basically the same code you have – round to the nearest grid point – and then it just nudges the position in the direction of the normal.

If you have any specific questions please ask :slight_smile:

If that line you told me to put in is the only thing I have to put in, it causes a lot more issues than before.

Sorry, had Position where it should’ve been Size. I edited it.

Sorry for the late response, works perfectly now, thanks a lot. :smile:

1 Like

Although this is solved I have one more issue, the part doesn’t rotate correctly. This happens because in the new code you use CFrame.new and for it to be able to rotate it can not use CFrame.new. Here’s the old code for it to keep the rotation whenever the CFrame changes:

if castRay and GhostItem then
			
	local PosX = math.floor(castRay.Position.X / GridSize) * GridSize + GhostItem.PrimaryPart.Size.X/2
	local PosY = math.floor(castRay.Position.Y / GridSize) * GridSize + GhostItem.PrimaryPart.Size.Y/1.5
	local PosZ = math.floor(castRay.Position.Z / GridSize) * GridSize + GhostItem.PrimaryPart.Size.Z/2
			
	local primaryCf = GhostItem:GetPrimaryPartCFrame()
	GhostItem:SetPrimaryPartCFrame(primaryCf-primaryCf.Position+Vector3.new(PosX, PosY, PosZ))
end

And here’s the new code:

if castRay then
	local pos = GetPlacementPosition(GridSize, GhostItem.PrimaryPart.Size, castRay.Position, castRay.Normal)
			
	GhostItem:SetPrimaryPartCFrame(CFrame.new(pos))
	local primaryCf = GhostItem:GetPrimaryPartCFrame()
	GhostItem:SetPrimaryPartCFrame(GhostItem.PrimaryPart.CFrame + Vector3.new(primaryCf-primaryCf.Position))
end

This code will produce the issue below:


Because the whole part moving code is wrapped inside of a Mouse.Move function whenever the mouse moves it will set the parts orientation back to normal orientation because in the new code you used CFrame.new. How would I fix this issue?

Edit: After a lot more time than it should’ve been, I fixed it.

2 Likes