How to make a part follow your mouse across a horizontal plane

Oh you want a custom range? I think you’d need to use raycasting and .Unit for a custom range:

local mousepart = workspace.MovePart
local rs = game:GetService("RunService")
local mouse = game.Players.LocalPlayer:GetMouse()
rs.RenderStepped:Connect(function()
local direction = (mouse.Hit.Position - character.HumanoidRootPart.Position).Unit * 50 -- A max distance of 50 studs
local raycast = workspace:Raycast(character.HumanoidRootPart.Position, direction)
if raycast then
mousepart.Position = raycast.Position
end
end)

Although, firing a raycast every frame may be crazy on resources, so maybe you’d wanna change the whole runservice thing?

This would also stop the part from hovering on the air sorta like mouse.Target like I said before, mouse.Target would be better on resources since raycasts are more costly in a frame event like this.

requirement:
needs to be able to hover in the air with NOTHING below it. Like follow the mouse in the air

Do you want something like this (Part of the code AI wrote lol)

local UIS = game:GetService("UserInputService")
local Part = script.Part
Part.Anchored=true
Part.CanCollide=false
Part.CanQuery=false
Part.CanTouch=false
Part.Parent=workspace
local planeY: number = 0.937

local camera: Camera = workspace.CurrentCamera
local mouse: Mouse = game.Players.LocalPlayer:GetMouse()
local Param = RaycastParams.new()

local function getMousePositionOnPlane(): Vector3
	local Hit = mouse.Hit.Position
	if Hit and Hit.Y < planeY then
		local Direction = (Hit - camera.CFrame.Position)
		Hit -= (Direction.Unit * (camera.CFrame.Position.Y - planeY))
	end
	return Hit
end
game:GetService("RunService").PreRender:Connect(function()
	local result = getMousePositionOnPlane()
	if result then
		Part.Position = result
	end
end)

I took a different method by negating it position instead (Actually there a problem I shouldn’t let AI wrote the negating don’t bother with my respond)

I don’t understand, is that not what’s happening in your current video?

AI writes suprisingly neat code, i’m actually impressed. But then again…

the video was adressing the problem…

So the problem is that it’s hovering over the air? Because that’s what’s happening in the video, or is that what you want??

Okay lets rewind. What I want is for the part to be able to follow the mouse on an invisible plane, or to put it simply, nothing. with the Y position being the same all the time. When you move your mouse over the void the part should follow your mouse like it does if you were to hover over the baseplate.

Hm, maybe it’s my bad english. Hopefully someone else smarter will come and solve.

Ok then try this

This will keep the old height in a variable, if the mouse is hovering the void then keep the same old height.

local mousepart = workspace.MovePart
local rs = game:GetService("RunService")
local mouse = game.Players.LocalPlayer:GetMouse()

rs.RenderStepped:Connect(function()
	local localY = mousePart.Position.Y

	if mouse.Target ~= nil then
		mousepart.Position = Vector3.new(mouse.hit.Position.X, 0, mouse.hit.Position.Z)
	else
		mousepart.Position = Vector3.new(mouse.hit.Position.X, localY, mouse.hit.Position.Z)
	end
end)

Back with new code try this, it is collision aware. It doesn’t go lower than told planeY
@davcio12344

local UIS = game:GetService("UserInputService")
local Part = script.Part
Part.Anchored=true
Part.CanCollide=false
Part.CanQuery=false
Part.CanTouch=false
Part.Parent=workspace
local planeY: number = 0.937

local mouse: Mouse = game.Players.LocalPlayer:GetMouse()
local camera: Camera = workspace.CurrentCamera
local Param = RaycastParams.new()

local function getMousePositionOnPlane(): Vector3
	local Hit = mouse.Hit
	if Hit.Position.Y < planeY then
		local direction = (Hit.Position-camera.CFrame.Position)
		local t = (planeY-camera.CFrame.Position.Y)/direction.Y
		return camera.CFrame.Position + direction*t
	end
	return Hit.Position
end
game:GetService("RunService").PreRender:Connect(function()
	Part.Position = getMousePositionOnPlane()
end)

I get the mouse.Hit and do some magic with it.

guys I think I know what the problem is.
here is a visualization of mouse hovering over the baseplate:

and here is a visualization of the mouse hovering over the void:

the second image is the problem.

how about this:

local mousepart = workspace.MovePart
local rs = game:GetService("RunService")
local mouse = game.Players.LocalPlayer:GetMouse()
local YCap = 0
mouse.TargetFilter = mousepart

rs.RenderStepped:Connect(function()
	if mouse.Target then
		YCap = mouse.Hit.Position.Y
	end
	mousepart.Position = Vector3.new(mouse.hit.Position.X, YCap, mouse.hit.Position.Z)
end)

EDIT: pretty sure the YCap will increase from the part hovering over your mouse all the time, so i added TargetFilter

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

Did you read this tho? That shows the problem with hovering the mouse over the void

Did my code not work the way you want? The position you have named “Expected Part Position” is the intersection point of the line that goes through camera position in the mouse direction and the horizontal plane that is at planeY. Based on my testing, when the aforementioned intersection point is within maxDist from the distance reference point (camera position or HumanoidRootPart position), my code does seem to return it.

My code does not use Mouse.Hit or anything that depends on the world geometry (parts) so it should work the same way regardless of whether there are parts or not.

I was just asking before I use the code…

Have you tried this function? I added upon to have a ‘ceiling’

getClampMouseY = function(mouse:Mouse, camera:Camera, minY:number, maxY:number) : Vector3
			local Hit = mouse.Hit
			if type(minY)=='number' and Hit.Position.Y < minY then
				local direction = (Hit.Position-camera.CFrame.Position)
				local t = (minY-camera.CFrame.Position.Y)/direction.Y
				return camera.CFrame.Position + direction*t
			elseif type(maxY)=='number' and Hit.Position.Y > maxY then
				local direction = (Hit.Position-camera.CFrame.Position)
				local t = (maxY-camera.CFrame.Position.Y)/direction.Y
				return camera.CFrame.Position + direction*t
			end
			return Hit.Position
		end

It part of the code I send yesterday and it works for me

https://gyazo.com/ab36aaece1314bebccc3ded2fa9b2f60