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