When testing, I realised that my previous code will also return undesired results when the mouse direction is away from the plane. The previous code placed the part behind the camera in that case. My new code gives sensible results when the mouse is pointing away from the plane. It returns a success boolean and a point. If the success boolean is false, in which case the plane is too far away from the distance reference point, the point should not be used and instead you should handle the situation in whatever way is appropriate for your use case. With distance reference point I mean the point to which the part must not have a distance bigger than the max distance. If the success boolean is true, the point is a point on the plane and within the max distance from the reference point.
If you want to limit distance from camera, use this code.
local function areApproximatelyEqual(a: number, b: number): boolean
return math.abs(b - a) <= 1e-4
end
local function getPositionThatIsClampedToBeCloseEnoughToCamera(direction: Vector3): Vector3
local cameraPosProjectedToHorizontalPlane: Vector3 = Vector3.new(camera.CFrame.X, planeY, camera.CFrame.Z)
local horizontalDirection: Vector3 = Vector3.new(direction.X, 0, direction.Z).Unit
local horizontalDistance: number = math.sqrt(maxDist^2 - (planeY - camera.CFrame.Y)^2)
return cameraPosProjectedToHorizontalPlane + horizontalDirection * horizontalDistance
end
local function getMousePositionOnPlaneWithLimitedDistanceToCamera(): (boolean, Vector3)
if math.abs(planeY - camera.CFrame.Y) > maxDist then
print(`Camera is too far from plane.`)
return false, Vector3.zero
end
local mousePosOnScreen: Vector2 = UserInputService:GetMouseLocation()
local mouseWorldSpaceRay: Ray = camera:ViewportPointToRay(mousePosOnScreen.X, mousePosOnScreen.Y)
local origin: Vector3, direction: Vector3 = mouseWorldSpaceRay.Origin, mouseWorldSpaceRay.Direction
local directionVerticalComponent: number = direction.Y
if areApproximatelyEqual(directionVerticalComponent, 0) or math.sign(directionVerticalComponent) ~= math.sign(planeY - origin.Y) then
return true, getPositionThatIsClampedToBeCloseEnoughToCamera(direction)
end
local multiplier: number = (planeY - origin.Y) / direction.Y
-- The direction of the ray given by ViewportPointToRay is a unit vector so multiplier is the same as distance from
-- the camera position to the intersection point
if multiplier > maxDist then
-- The distance is too big but the direction is guaranteed to have a horizontal component because
-- if the direction was purely vertical, then either the distance would be valid or the function
-- would return false, Vector3.zero.
return true, getPositionThatIsClampedToBeCloseEnoughToCamera(direction)
end
return true, origin + direction * multiplier
end
For limiting distance from HumanoidRootPart, I have two options. I’m not sure which one I’d consider better.
One option is this code.
local function areApproximatelyEqual(a: number, b: number): boolean
return math.abs(b - a) <= 1e-4
end
local function getMousePositionOnPlaneWithLimitedDistanceToHumanoidRootPart(): (boolean, Vector3)
local character: Model? = Players.LocalPlayer.Character
if character == nil then
return false, Vector3.zero
end
local humanoidRootPart: Part? = character:FindFirstChild("HumanoidRootPart") :: Part
if humanoidRootPart == nil then
return false, Vector3.zero
end
local hrpPos: Vector3 = humanoidRootPart.Position
local planeYOffsetFromHRP: number = planeY - hrpPos.Y
if math.abs(planeYOffsetFromHRP) > maxDist then
return false, Vector3.zero
end
local mousePosOnScreen: Vector2 = UserInputService:GetMouseLocation()
local mouseWorldSpaceRay: Ray = camera:ViewportPointToRay(mousePosOnScreen.X, mousePosOnScreen.Y)
local origin: Vector3, direction: Vector3 = mouseWorldSpaceRay.Origin, mouseWorldSpaceRay.Direction
local directionVerticalComponent: number = direction.Y
if areApproximatelyEqual(directionVerticalComponent, 0) or math.sign(directionVerticalComponent) ~= math.sign(planeY - origin.Y) then
local hrpPosProjectedToHorizontalPlane: Vector3 = Vector3.new(hrpPos.X, planeY, hrpPos.Z)
local horizontalDirection: Vector3 = Vector3.new(direction.X, 0, direction.Z).Unit
local maxHorizontalDistFromHRP: number = math.sqrt(maxDist^2 - planeYOffsetFromHRP^2)
return true, hrpPosProjectedToHorizontalPlane + horizontalDirection * maxHorizontalDistFromHRP
end
local multiplier: number = (planeY - origin.Y) / direction.Y
local intersectionPoint: Vector3 = origin + direction * multiplier
if (intersectionPoint - hrpPos).Magnitude > maxDist then
local hrpPosProjectedToHorizontalPlane: Vector3 = Vector3.new(hrpPos.X, planeY, hrpPos.Z)
local intersectionPointHorizontalDirectionFromHRP: Vector3 = (intersectionPoint - hrpPosProjectedToHorizontalPlane).Unit
local maxHorizontalDistFromHRP: number = math.sqrt(maxDist^2 - planeYOffsetFromHRP^2)
return true, hrpPosProjectedToHorizontalPlane + intersectionPointHorizontalDirectionFromHRP * maxHorizontalDistFromHRP
end
return true, intersectionPoint
end
The other option is this code.
local function areApproximatelyEqual(a: number, b: number): boolean
return math.abs(b - a) <= 1e-4
end
local function getClampedPositionForHRPDistanceLimitingOption2(origin: Vector3, direction: Vector3, hrpPos: Vector3): Vector3
local hrpPosProjectedToHorizontalPlane: Vector3 = Vector3.new(hrpPos.X, planeY, hrpPos.Z)
local horizontalDirection: Vector3 = Vector3.new(direction.X, 0, direction.Z).Unit
local lineHorizontalNormal: Vector3 = direction:Cross(Vector3.yAxis).Unit
local lineSidewaysOffsetFromHRP: number = (origin - hrpPos):Dot(lineHorizontalNormal)
if lineSidewaysOffsetFromHRP > maxDist then
return hrpPosProjectedToHorizontalPlane + maxDist * lineHorizontalNormal
end
return hrpPosProjectedToHorizontalPlane + lineSidewaysOffsetFromHRP * lineHorizontalNormal + math.sqrt(maxDist^2 - lineSidewaysOffsetFromHRP^2) * horizontalDirection
end
local function hrpDistanceLimitingOption2(): (boolean, Vector3)
local character: Model? = Players.LocalPlayer.Character
if character == nil then
return false, Vector3.zero
end
local humanoidRootPart: Part? = character:FindFirstChild("HumanoidRootPart") :: Part
if humanoidRootPart == nil then
return false, Vector3.zero
end
local hrpPos: Vector3 = humanoidRootPart.Position
local planeYOffsetFromHRP: number = planeY - hrpPos.Y
if math.abs(planeYOffsetFromHRP) > maxDist then
return false, Vector3.zero
end
local mousePosOnScreen: Vector2 = UserInputService:GetMouseLocation()
local mouseWorldSpaceRay: Ray = camera:ViewportPointToRay(mousePosOnScreen.X, mousePosOnScreen.Y)
local origin: Vector3, direction: Vector3 = mouseWorldSpaceRay.Origin, mouseWorldSpaceRay.Direction
local directionVerticalComponent: number = direction.Y
if areApproximatelyEqual(directionVerticalComponent, 0) or math.sign(directionVerticalComponent) ~= math.sign(planeY - origin.Y) then
return true, getClampedPositionForHRPDistanceLimitingOption2(origin, direction, hrpPos)
end
local multiplier: number = (planeY - origin.Y) / direction.Y
local intersectionPoint: Vector3 = origin + direction * multiplier
if (intersectionPoint - hrpPos).Magnitude > maxDist then
return true, getClampedPositionForHRPDistanceLimitingOption2(origin, direction, hrpPos)
end
return true, intersectionPoint
end