The reason why I’m making a new post instead of using the old one is because it’s cluttered with replies and I don’t want to add a bounty onto all of that.
I’ve been having this issue for a long time, and I stopped working on the game about a year ago because of it and then finally decided to try again. I have the anchor point system so if the half size of the part is even, it’ll still work with the grid, the issue however is seen in the video below, it goes through blocks, and there is another issue where if placing on the negative sides of the blocks it goes through it.
Thanks.
function GetPlacementPosition()
local MouseLocation = UIS:GetMouseLocation()
local ViewportRay = Camera:ViewportPointToRay(MouseLocation.X, MouseLocation.Y)
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {Part, Player.Character}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
local Result = workspace:Raycast(ViewportRay.Origin, ViewportRay.Direction * 1000, raycastParams)
if not Result then return nil end
local PartSize = Part.PrimaryPart.Size
local PartCFrame = Part.PrimaryPart.CFrame
local HalfSize = PartSize / 2
local LocalNormal = PartCFrame:VectorToObjectSpace(Result.Normal)
local AnchorDirection = Vector3.new(
math.round(LocalNormal.X),
math.round(LocalNormal.Y),
math.round(LocalNormal.Z)
)
local WorldAnchorDirection = PartCFrame:VectorToWorldSpace(AnchorDirection)
local ShouldOffset = Vector3.new(1, 1, 1) - Vector3.new(HalfSize.X % 2, HalfSize.Y % 2, HalfSize.Z % 2)
local AnchorOffset = Vector3.new(-HalfSize.X + 1, -HalfSize.Y + 1, -HalfSize.Z + 1) * ShouldOffset
local AnchorWorldPosition = PartCFrame:PointToWorldSpace(AnchorOffset)
local SnappedAnchor = SnapToGrid(Result.Position)
local FinalPosition = SnappedAnchor - PartCFrame:VectorToWorldSpace(AnchorOffset)
return FinalPosition
end
This is not enough info to understand exactly how you want it to look, so I can’t look at the math and figure out what steps exist for what reasons. Could you provide a picture showing the anchor point, when the mouse lands on the target, and relevant distances you want to be held?
The right image shows the anchor point (a 2x2x2 block basically), which is fine, however it should look like the one on the left, in the code I have the anchor point is either on one side of the block or the center of it depending on if its half size is even or odd respectively.
It looks like you account for the size of the part being placed on. You now need to do the same thing for the part being placed, except you will use the opposite face (since for two faces to touch they need to be opposite, right?), and you need to measure from the anchor point.
Heres all the code that should get it working, Part is set later on but it’s just a model with a primary part.
local UIS = game:GetService("UserInputService")
local Player = game.Players.LocalPlayer
local Camera = game.Workspace.CurrentCamera
local Part = nil
local GridSize = 2
function RoundToGrid(Number)
return math.floor((Number + GridSize / 2) / GridSize) * GridSize
end
function SnapToGrid(Position)
return Vector3.new(
RoundToGrid(Position.X),
RoundToGrid(Position.Y),
RoundToGrid(Position.Z)
)
end
function GetPlacementPosition()
local MouseLocation = UIS:GetMouseLocation()
local ViewportRay = Camera:ViewportPointToRay(MouseLocation.X, MouseLocation.Y)
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {Part, Player.Character}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
local Result = workspace:Raycast(ViewportRay.Origin, ViewportRay.Direction * 1000, raycastParams)
if not Result then return nil end
local PartSize = Part.PrimaryPart.Size
local PartCFrame = Part.PrimaryPart.CFrame
local HalfSize = PartSize / 2
local LocalNormal = PartCFrame:VectorToObjectSpace(Result.Normal)
local AnchorDirection = Vector3.new(
math.round(LocalNormal.X),
math.round(LocalNormal.Y),
math.round(LocalNormal.Z)
)
local WorldAnchorDirection = PartCFrame:VectorToWorldSpace(AnchorDirection)
local ShouldOffset = Vector3.new(1, 1, 1) - Vector3.new(HalfSize.X % 2, HalfSize.Y % 2, HalfSize.Z % 2)
local AnchorOffset = Vector3.new(-HalfSize.X + 1, -HalfSize.Y + 1, -HalfSize.Z + 1) * ShouldOffset
local AnchorWorldPosition = PartCFrame:PointToWorldSpace(AnchorOffset)
local SnappedAnchor = SnapToGrid(Result.Position)
local FinalPosition = SnappedAnchor - PartCFrame:VectorToWorldSpace(AnchorOffset)
return FinalPosition
end
The red arrow is the only difference in the two situations, right? And its determined by the distance of the anchor point to the face on the opposite side (of the dragged part) from the face being moused over.
So, I tried adding the size corresponding to the world axis to it, and it somewhat worked, but the issue is that if you rotate it back, it’ll add that same gap to it.
Oh right I didn’t consider that you can rotate. You need to figure out which face is the opposite after the rotation has been applied. You could rotate the normal you used to find the face in the first place?
Ignoring the fact that you’re making a Plane Crazy clone, this can be solved using an iterated occlusion check, which can come in two different flavors:
You scan the placement volume for colliding voxels, and you move the placement based on which quadrant of the volume the occupied voxel resides. This can be done by summing up the signs of the occupied voxel’s displacement relative to the center of the placement. Essentially, you want to nudge the placement away from the area with the most collisions, ensuring that the placement finds a suitable empty spot.
Pseudocode:
foreach iteration:
collision: {Vector3} = checkForCollisions(center)
sumOfSigns: Vector3 = Vector3.zero
for voxels in collision:
disp = center - voxels
sumOfSigns += disp:Sign()
end
center += sumOfSigns:Sign()
You know what the surface normal of the adjacent block is, you can use that as the direction vector to slowly inch the placement away, repeat it until the placement no longer clips.
I have an old project that does both of those things, in the same order, which I have a video demo of:
Code sample:
@native local function rectifyPlacement(vcr: pPlot.VoxelCastResult): Vector3?
local io: pPlot.PlayerInterface = plot.IO
local pType: pReg.GenericEntry? = io.GhostType
if pType and io.GhostRotation then
--scalable parts are always 1x1x1, so they dont need to be rectified
if pType.Part then
return vcr.Voxel + vcr.Normal
end
--c0 and c1 are the corners of the bounds of the part.
local c0: Vector3 = pType.C0::Vector3
local c1: Vector3 = pType.C1::Vector3
local result: Vector3 = vcr.Voxel --voxel position of the center of the placement; initialized to be where the mouse is aiming at.
--rotate the bounds based on the orientation of the placement preview
local rot: Vector3 = pFuncs.AngleToRA(io.GhostRotation)
local b0: Vector3 = pFuncs.RotVecRA(c0, rot)
local b1: Vector3 = pFuncs.RotVecRA(c1, rot)
local partSize: Vector3 = c1 - c0
local maxTries: number = math.max(partSize.X, partSize.Y, partSize.Z) + 10
local c0, c1: Vector3
local querySuccess: boolean
local timeOut: number = 0
while true do --this is where the occlusion check happens
timeOut += 1
if timeOut > maxTries then break end
c0 = result + b0
c1 = result + b1
local obstructions: Tensor.TensorHash<true> --this is just a hashmap in the form {[Vector3]: true}
querySuccess, obstructions = plot:GetCollisions(c0, c1) --finds all colliding voxels within the c0 and c1 bounds
if querySuccess then break end --no more collisions detected; we can stop now
--otherwise, if there are collisions, move the placement AWAY from occupied voxels
local moveTowards: Vector3 = c0:Lerp(c1, .5)
local totalMove: Vector3 = Vector3.zero
for obstacle: Vector3 in obstructions do
totalMove += (moveTowards - obstacle):Sign()
end
result += totalMove:Sign()
end
--if the occlusion check above fails, here's another one
if not querySuccess then
result = vcr.Voxel
c0 = result + b0
c1 = result + b1
timeOut = 0
while true do
timeOut += 1
if timeOut > maxTries then break end
c0 = result + b0
c1 = result + b1
querySuccess = plot:CanPlace(c0, c1) --:CanPlace() is a wrapper for :GetCollisions()
if querySuccess then break end
result += vcr.Normal --move away from the face that the mouse is aiming at
end
end
if not querySuccess then
--print('smart placement failed')
result = vcr.Voxel
c0 = result + b0
c1 = result + b1
end
return result
end
return
end