Trying to make a 2D eye that tracks the player on the x axis

I’m trying to make a 2D eye that tracks the player on the x axis. I want it to slow down in speed if the player gets further away from the eye, and speed up if the player gets closer. It should look about the same as if I were to tween the eye’s motion with easing style circular. I want the eye to reach 5 studs away (Maximum distance) if the player is 20 studs away.

I’m not good at scripting, so I looked on the developer forum and found nothing. I got a script using AI, but it doesn’t work.
This is the script I have:

local eye = script.Parent -- Assuming the script is attached to the eye object
local player = game.Players.LocalPlayer

local function moveEye()
	local playerPosition = player.Character.HumanoidRootPart.Position
	local eyePosition = eye.Position

	-- Calculate the distance between the eye and the player on the x-axis
	local distance = math.abs(playerPosition.X - eyePosition.X)

	-- Calculate the maximum distance the eye can move
	local maxDistance = 5

	-- Calculate the maximum speed the eye can move
	local maxSpeed = 20

	-- Calculate the speed based on the distance
	local speed = maxSpeed * math.sqrt(1 - (distance / maxDistance)^2)

	-- Determine the direction of movement
	local direction = math.sign(playerPosition.X - eyePosition.X)

	-- Move the eye
	eye.Position = eyePosition + Vector3.new(speed * direction, 0, 0)
end

-- Call the moveEye function every frame
game:GetService("RunService").Heartbeat:Connect(moveEye)

The darker circle is the outer ring of the eye. The lighter circle is the maximum distance the eye can move, being 5 studs in either direction. The part I want to move it the pupil, being the black slit.
This is what the eye looks like:

I’ve been working on a sort of outline for what the script might do, but it’s not finished and it would be complicated. It would basically check if the player has moved, check if the eye’s x axis position is positive or negative, check if the player’s x axis movement was positive or negative, do addition or subtraction to find out the distance the player is away from the eye on the x axis, move the pupil 1 divided by the original distance the player was from the eye plus/minus the distance the player moved. It would also check if the distance the pupil wants to move goes over the center point if the player is still on the other side, and if the amount of movement does go over to the other side, it changes the amount moved to the distance between the pupil and the eye. It’s very complicated.
So far this is what I have. Note this just outlines what the script would do. It doesn’t show a working script and it is not finished:

--local pupil = script.parent
--local player = player
--local playerPosition = player.XAxisPosition
--local eyePosition = script.parent.parent.XAxisPosition
--local oldValue = playerPosition - eyePosition
--
--local function move()
--	local playerPosition = player.XAxisPosition
--	local eyePosition = script.parent.parent.XAxisPosition
--	local value = playerPosition - eyePosition
--	if value = >0 then
--		local moveValue1 = 1/(oldValue+value)
--		if pupil.XAxisPosition - moveValue1 = >eyePosition then
--			pupil.XAxisPosition = pupil.XAxisPosition - moveValue1
--		else
--			local moveValue2 = pupil.XAxisPosition - eyePosition
--			pupil.XAxisPosition = pupil.XAxisPosition - moveValue2
--	if value = <0 then
--		local moveValue3 = 1/(eyePosition - playerPosition)
--
--	
--end
--
--game:GetService("RunService").Heartbeat:Connect(move)
``
6 Likes

My initial thought was to treat the background of the eye as a plane, and to project the target position onto the plane before attempting to move the eye in the desired direction.

e.g. like so:


I came up with the following example:

[Note]: there’s still some work for you to do if you want it to only move on one axis as this example moves both vertically and horizontally.

Example

Example file: EyeDemo.rbxl (56.4 KB)

Example source:

local RunService = game:GetService('RunService')

----------------------------------------
--                                    --
--               Const                --
--                                    --
----------------------------------------

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 10


----------------------------------------
--                                    --
--               Util                 --
--                                    --
----------------------------------------

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
  return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
  local d = normal:Dot(vec) + distance
  return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
  return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
  return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, normalId)
  normalId = normalId or Enum.NormalId.Front

  local transform = part.CFrame
  local translation = part.Position

  -- compute the plane
  local normal = getDirectionVector(transform, normalId)
  local distance = -1 * normal:Dot(translation)

  -- compute the area of the part's surface given the plane's surface normal
  local area = transform:VectorToObjectSpace(normal)
  area = Vector3.new(1 - math.abs(area.x), 1 - math.abs(area.y), 1 - math.abs(area.z)) * part.Size*0.5
  area = area.Magnitude

  return translation, area, normal, distance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
      position, planeOffset,
      planeArea, planeNormal,
      planeDistance, sqrRadius, maxDistance
    )

  sqrRadius = sqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
  maxDistance = maxDistance or MAX_PLANE_DISTANCE

  -- get the target position's closest point on the plane
  local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

  -- get the depth to/from the plane from the target's position
  local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

  -- get the distance of the closest position from the centre point of the plane
  local displacement = closestPoint - planeOffset
  local magnitude = getSqrMagnitude(displacement)

  -- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
  local isOnCorrectSide = distanceToPlane > 0
  -- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
  local isWithinDistance = magnitude <= sqrRadius and distanceToPlane <= maxDistance

  -- if we dont meet either condition, go back to the centre of the plane
  if not isOnCorrectSide or not isWithinDistance then
    return false, planeOffset
  end

  -- compute the new position of the eye when tracking the target position
  displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
  magnitude = math.clamp((0.5*magnitude) / planeArea, 0, 1)

  return true, planeOffset + displacement*magnitude
end


----------------------------------------
--                                    --
--               Main                 --
--                                    --
----------------------------------------

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart')
local targetPart = workspace:WaitForChild('TargetPart')

-- create our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.one*1.5
eyePart.Shape = Enum.PartType.Ball
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace

-- get our initial plane props
local offset, area, normal, distance = computePlaneFromPartSurface(planePart, Enum.NormalId.Front)
local maxAreaSqr = area*area

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
  if updateTask then
    return
  end

  updateTask = task.defer(function ()
    offset, area, normal, distance = computePlaneFromPartSurface(planePart, Enum.NormalId.Front)
    maxAreaSqr = area*area
    updateTask = nil
  end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
  -- can be replaced with whatever target we want to follow
  local position = targetPart.Position

  -- det. whether we're watching the target, and where our desired position is
  local isWatching, desiredPosition = computeEyePosition(position, offset, area, normal, distance, maxAreaSqr)

  -- if we're not already at our desired position - within eps 1e-3 - then interpolate
  -- towards that position
  position = eyePart.Position
  if not desiredPosition:FuzzyEq(position, 1e-3) then
    eyePart.Position = position:Lerp(desiredPosition, dt*6)
  end

  -- e.g. visually show that we're not watching the subject
  eyePart.BrickColor = isWatching and BrickColor.Red() or BrickColor.Black()
end)

Video

4 Likes

I modified the script a bit to recreate the eye, but I have 2 things left that I want to do. I wish the tracking part could move more, but I don’t know how to change that. I also would want to know how to change the target the eye is tracking from the part being used to the player or their humanoid root part.
Other than that, this worked quite well. I was worried that nobody would respond because I checked other sources on here and videos, but there was nothing.

This is the script I used with the minor changes:

local RunService = game:GetService('RunService')

--[>] const

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 10

--[>] utils

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
	return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
	local d = normal:Dot(vec) + distance
	return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
	return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
	return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, normalId)
	normalId = normalId or Enum.NormalId.Front

	local transform = part.CFrame
	local translation = part.Position

	-- compute the plane
	local normal = getDirectionVector(transform, normalId)
	local distance = -1 * normal:Dot(translation)

	-- compute the area of the part's surface given the plane's surface normal
	local area = transform:VectorToObjectSpace(normal)
	area = Vector3.new(1 - math.abs(area.x), 1 - math.abs(area.y), 1 - math.abs(area.z)) * part.Size*0.5
	area = area.Magnitude

	return translation, area, normal, distance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
	position, planeOffset,
	planeArea, planeNormal,
	planeDistance, sqrRadius, maxDistance
)

	sqrRadius = sqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
	maxDistance = maxDistance or MAX_PLANE_DISTANCE

	-- get the target position's closest point on the plane
	local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

	-- get the depth to/from the plane from the target's position
	local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

	-- get the distance of the closest position from the centre point of the plane
	local displacement = closestPoint - planeOffset
	local magnitude = getSqrMagnitude(displacement)

	-- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
	local isOnCorrectSide = distanceToPlane > 0
	-- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
	local isWithinDistance = magnitude <= sqrRadius and distanceToPlane <= maxDistance

	-- if we dont meet either condition, go back to the centre of the plane
	if not isOnCorrectSide or not isWithinDistance then
		return false, planeOffset
	end

	-- compute the new position of the eye when tracking the target position
	displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
	magnitude = math.clamp((0.5*magnitude) / planeArea, 0, 1)

	return true, planeOffset + displacement*magnitude
end

--[>] main

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart1')
local targetPart = workspace:WaitForChild('TargetPart')

-- create our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.new(18,18,0.3)
eyePart.Shape = Enum.PartType.Block
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.Transparency = 1
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace
local eyeDecal = Instance.new('Decal')
eyeDecal.Transparency = 0
eyeDecal.Color3 = Color3.new(0, 1, 0)
eyeDecal.Face = Enum.NormalId.Front
eyeDecal.Texture = 'rbxassetid://16687464344'
eyeDecal.Parent = eyePart

-- get our initial plane props
local offset, area, normal, distance = computePlaneFromPartSurface(planePart, Enum.NormalId.Front)
local maxAreaSqr = area*area

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
	if updateTask then
		return
	end

	updateTask = task.defer(function ()
		offset, area, normal, distance = computePlaneFromPartSurface(planePart, Enum.NormalId.Front)
		maxAreaSqr = area*area
		updateTask = nil
	end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
	-- can be replaced with whatever target we want to follow
	local position = targetPart.Position

	-- det. whether we're watching the target, and where our desired position is
	local isWatching, desiredPosition = computeEyePosition(position, offset, area, normal, distance, maxAreaSqr)

	-- if we're not already at our desired position - within eps 1e-3 - then interpolate
	-- towards that position
	position = eyePart.Position
	if not desiredPosition:FuzzyEq(position, 1e-3) then
		eyePart.Position = position:Lerp(desiredPosition, dt*6)
	end

	-- e.g. visually show that we're not watching the subject
	eyePart.BrickColor = isWatching and BrickColor.Red() or BrickColor.Black()
end)
3 Likes

I’ve commented the code a bit more for you here and improved it slightly so that it might make a little more sense to you. See the example attached, but essentially:

Note: I’m calculating the travel distance in this example by using the cylinder’s size, but you could set the travelDistance variable to any number you prefer instead

  • Change the travelDistance variable if you want it to be able to move more/less:
    • The way this works is that we’re calculating the distance from the eye centre point to the target position and then comparing that to the radius/size of the eye’s background plane; this gives us a number with a range of 0 to 1 (in the attached example, this is the black cylinder).
    • Then we multiply this with the projectedPosition - eyeCentre unit vector to scale the position by the required relative distance
    • And then; finally: we multiply with the travelDistance so that it travels the distance we want
Example

Example file: EyeDemo.rbxl (57.7 KB)

Example code:

local RunService = game:GetService('RunService')

----------------------------------------
--                                    --
--               Const                --
--                                    --
----------------------------------------

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 10


----------------------------------------
--                                    --
--               Util                 --
--                                    --
----------------------------------------

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
  return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
  local d = normal:Dot(vec) + distance
  return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
  return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
  return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, surfaceShape, forwardNormalId, upNormalId)
  surfaceShape = surfaceShape or Enum.PartType.Block
  upNormalId = upNormalId or Enum.NormalId.Top
  forwardNormalId = forwardNormalId or Enum.NormalId.Front

  local size = part.Size
  local transform = part.CFrame
  local translation = part.Position

  -- compute the plane
  local normal = getDirectionVector(transform, forwardNormalId)
  local distance = -1 * normal:Dot(translation)

  -- compute the part's size from its right & up vector
  local upVector = getDirectionVector(transform, upNormalId)
  local rightVector = normal:Cross(upVector)

  local scale = Vector2.new(
    (size*upVector).Magnitude,
    (size*rightVector).Magnitude
  )

  -- compute the travel distance using the largest axis
  local travelDistance = math.max(scale.X, scale.Y)

  -- compute the area of the surface from its given shape
  local area
  if surfaceShape == Enum.PartType.Cylinder then
    area = math.pi*(travelDistance*0.5 + 1)
  else
    --[!] Treat it as a block instead...
    area = (scale.X*scale.Y)*0.5
  end

  return translation, scale, normal, distance, area, travelDistance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
      position, planeOffset,
      planeNormal, planeDistance, planeRadius,
      maxTravelDistance, maxSqrRadius, maxPlaneDistance
    )

  maxSqrRadius = maxSqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
  maxPlaneDistance = maxPlaneDistance or MAX_PLANE_DISTANCE
  maxTravelDistance = maxTravelDistance or planeRadius*0.5

  -- get the target position's closest point on the plane
  local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

  -- get the depth to/from the plane from the target's position
  local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

  -- get the distance of the closest position from the centre point of the plane
  local displacement = closestPoint - planeOffset
  local magnitude = getSqrMagnitude(displacement)

  -- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
  local isOnCorrectSide = distanceToPlane > 0

  -- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
  local isWithinDistance = magnitude <= maxSqrRadius and distanceToPlane <= maxPlaneDistance

  -- if we dont meet either condition, go back to the centre of the plane
  if not isOnCorrectSide or not isWithinDistance then
    return false, planeOffset
  end

  -- compute the new position of the eye when tracking the target position
  displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
  magnitude = math.clamp(magnitude / planeRadius, 0, 1)

  return true, planeOffset + displacement*magnitude*maxTravelDistance
end


----------------------------------------
--                                    --
--               Main                 --
--                                    --
----------------------------------------

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart')
local targetPart = workspace:WaitForChild('TargetPart')

-- create our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.one*1.5
eyePart.Shape = Enum.PartType.Ball
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace

-- get our initial plane props
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front
local eyeSurfaceShape = Enum.PartType.Cylinder

local maxEyeViewingRadius = 20
local maxEyeViewingRadiusSqr = maxEyeViewingRadius*maxEyeViewingRadius

local offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)
travelDistance *= 0.25 -- limit the travel distance our eye can move

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
  if updateTask then
    return
  end

  updateTask = task.defer(function ()
    offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)

    -- limit the travel distance our eye can move
    travelDistance *= 0.25

    updateTask = nil
  end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
  -- can be replaced with whatever target we want to follow
  local position = targetPart.Position

  -- det. whether we're watching the target, and where our desired position is
  local isWatching, desiredPosition = computeEyePosition(
    -- target position & our pre-calculated plane properties
    position, offset, normal, distance, maxAreaSqr,
    -- the maximum distance our eye can travel
    travelDistance,
    -- make sure we only position ourself if our target is within x viewing radius
    maxEyeViewingRadiusSqr,
    -- use our constant value so we're not considering things that are greater than x studs away
    MAX_PLANE_DISTANCE
  )

  -- if we're not already at our desired position - within eps 1e-3 - then interpolate
  -- towards that position
  position = eyePart.Position
  if not desiredPosition:FuzzyEq(position, 1e-3) then
    eyePart.Position = position:Lerp(desiredPosition, dt*6)
  end

  -- e.g. visually show that we're not watching the subject
  eyePart.BrickColor = isWatching and BrickColor.Red() or BrickColor.Black()
end)

It would just be a case of finding a player that meets any condition(s) that you have before targeting them. e.g. that could be as simple as finding the closest player to the eye, as described here (Note: I just searched for ‘find closest player’ to give you an example, I’ve not looked at the code in that thread so make sure you double check that it works!).

Once you’ve found the appropriate player and found a part within their character to target, e.g. its HumanoidRootPart, you would set it as the targetPart.

2 Likes

Thank you. You have been extremely helpful. I can’t seem to figure out how to independently change the amount the eye moves on the x and y axis though. For example, I want the eye to be able to move a maximum of 15 studs on the x axis and only 8 studs on the y axis. Whenever I try to change one of them it changes both of them.

1 Like

No worries :slight_smile:

Probably a few different ways but if you wanted to maintain the current functionality of allowing the eye to be orientated on any axis then something like this would likely work:

--[!] Note: ::getDirectionVector() method should be defined as in the previous script

local function computeMaxTravelDistance(eyeTransform, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)
	maxWidth = maxWidth or 1
	maxHeight = maxHeight or 1
	eyeUpSurface = eyeUpSurface or Enum.NormalId.Top
	eyeFrontSurface = eyeFrontSurface or Enum.NormalId.Front

	local upVector = getDirectionVector(eyeTransform, eyeUpSurface)
	local lookVector = -1*getDirectionVector(eyeTransform, eyeFrontSurface)
	local rightVector = upVector:Cross(lookVector).Unit
	return upVector*maxHeight + rightVector*maxWidth
end

-- USAGE
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front

local maxWidth = 15
local maxHeight = 8
travelDistance = computeMaxTravelDistance(planePart.CFrame, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)


Btw, just to note: if your ellipsis is 14 x 8 you would want to halve those values, so it’d be 7 x 4 for the maxWidth and maxHeight vars

2 Likes

Would I replace part of the script with that or would I just insert it somewhere like near the end?

1 Like

I believe it would be added onto the current script, since this is a new function not used in the previous script.

2 Likes

I now have one last thing. I don’t know how to change the part the eye tracks to the closest player’s humanoid root part.
The script identifies the part it wants to track with local targetPart = workspace:WaitForChild('TargetPart3').

I’m unsure how I would replace it with the code below.

local function getclosestplr()
    local bot_position = YOUR_BOT_POSITION_HERE
    
    local distance = math.huge
    local closest_player_character = nil
    
	for i, player in pairs(workspace:GetChildren()) do
	    -- you have put player, but this is really the players character
		if player:FindFirstChild("Humanoid") then

		    local player_position = player.HumanoidRootPart.Position
		    local distance_from_bot = (bot_position - player_position).magnitude
		    
			if distance_from_bot < distance then
			    distance = distance_from_bot
			    closest_player_character = player
			end
		end
	end
	
	return closest_player_character
end

If everything works completely after this, I’ll probably make a summary of everything and mark it as the solution.

1 Like

You’d just do something like this;

local targetPart = getclosestplr()

You’d want the getclosestplr() function to return a part within the character however, like the Head or the HumanoidRootPart.

2 Likes

I changed workspace:WaitForChild('TargetPart3') to local targetPart = getclosestplr(). I also put that function close to the top of the script, changed YOUR_BOT_POSITION_HERE to game.Workspace:WaitForChild('PlanePart3').Position, and changed return closest_player_character to return closest_player_character:WaitForChild('Head'). If it’s hard to follow the changes, here’s the entire portion of the script.

local function getclosestplr()
	local bot_position = game.Workspace:WaitForChild('PlanePart3').Position

	local distance = math.huge
	local closest_player_character = nil

	for i, player in pairs(workspace:GetChildren()) do
		-- you have put player, but this is really the players character
		if player:FindFirstChild("Humanoid") then

			local player_position = player.HumanoidRootPart.Position
			local distance_from_bot = (bot_position - player_position).magnitude

			if distance_from_bot < distance then
				distance = distance_from_bot
				closest_player_character = player
			end
		end
	end

	return closest_player_character:WaitForChild('Head')
end

--Other stuff here

local targetPart = getclosestplr()

The eye doesn’t track the player though, so I likely did something wrong. I don’t know if it’s related as well, but if I run the game and move the target part, it moves the pupil. It doesn’t behave the same though when I play the game as a player and move the target part instead.

1 Like

You might need to put the local targetPart = getclosestplr() in a while loop. Currently the targetPart is being defined once, and never being updated again.

while task.wait() do
	local targetPart = getclosestplr()
	-- Rest of code for making the eye track the targetPart
end
2 Likes

I don’t really know how much of the code would need to go in that loop or where the loop should be in the script, if that matters. I found 1 reference to targetPart in the script, so I assume that should be the only code included in the loop, but I don’t know for sure.
Currently this is what I have for the script:

local RunService = game:GetService('RunService')

----------------------------------------
--                                    --
--               Const                --
--                                    --
----------------------------------------

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 10


----------------------------------------
--                                    --
--               Util                 --
--                                    --
----------------------------------------

local function getclosestplr()
	local bot_position = game.Workspace:WaitForChild('PlanePart3').Position

	local distance = math.huge
	local closest_player_character = nil

	for i, player in pairs(workspace:GetChildren()) do
		-- you have put player, but this is really the players character
		if player:FindFirstChild("Humanoid") then

			local player_position = player.HumanoidRootPart.Position
			local distance_from_bot = (bot_position - player_position).magnitude

			if distance_from_bot < distance then
				distance = distance_from_bot
				closest_player_character = player
			end
		end
	end

	return closest_player_character:WaitForChild('Head')
end

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
	return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
	local d = normal:Dot(vec) + distance
	return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
	return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
	return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, surfaceShape, forwardNormalId, upNormalId)
	surfaceShape = surfaceShape or Enum.PartType.Block
	upNormalId = upNormalId or Enum.NormalId.Top
	forwardNormalId = forwardNormalId or Enum.NormalId.Front

	local size = part.Size
	local transform = part.CFrame
	local translation = part.Position

	-- compute the plane
	local normal = getDirectionVector(transform, forwardNormalId)
	local distance = -1 * normal:Dot(translation)

	-- compute the part's size from its right & up vector
	local upVector = getDirectionVector(transform, upNormalId)
	local rightVector = normal:Cross(upVector)

	local scale = Vector2.new(
		(size*upVector).Magnitude,
		(size*rightVector).Magnitude
	)

	-- compute the travel distance using the largest axis
	local travelDistance = math.max(scale.X, scale.Y)

	-- compute the area of the surface from its given shape
	local area
	if surfaceShape == Enum.PartType.Cylinder then
		area = math.pi*(travelDistance*0.5 + 1)
	else
		--[!] Treat it as a block instead...
		area = (scale.X*scale.Y)*0.5
	end

	return translation, scale, normal, distance, area, travelDistance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
	position, planeOffset,
	planeNormal, planeDistance, planeRadius,
	maxTravelDistance, maxSqrRadius, maxPlaneDistance
)

	maxSqrRadius = maxSqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
	maxPlaneDistance = maxPlaneDistance or MAX_PLANE_DISTANCE
	maxTravelDistance = maxTravelDistance or planeRadius*0.5

	-- get the target position's closest point on the plane
	local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

	-- get the depth to/from the plane from the target's position
	local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

	-- get the distance of the closest position from the centre point of the plane
	local displacement = closestPoint - planeOffset
	local magnitude = getSqrMagnitude(displacement)

	-- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
	local isOnCorrectSide = distanceToPlane > 0

	-- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
	local isWithinDistance = magnitude <= maxSqrRadius and distanceToPlane <= maxPlaneDistance

	-- if we dont meet either condition, go back to the centre of the plane
	if not isOnCorrectSide or not isWithinDistance then
		return false, planeOffset
	end

	-- compute the new position of the eye when tracking the target position
	displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
	magnitude = math.clamp(magnitude / planeRadius, 0, 1)

	return true, planeOffset + displacement*magnitude*maxTravelDistance
end


----------------------------------------
--                                    --
--               Main                 --
--                                    --
----------------------------------------

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart3')
local targetPart = getclosestplr()

-- create our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.new(18,18,0.3)
eyePart.Shape = Enum.PartType.Block
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.Transparency = 1
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace
local eyeDecal = Instance.new('Decal')
eyeDecal.Transparency = 0
eyeDecal.Color3 = Color3.new(0, 1, 0)
eyeDecal.Face = Enum.NormalId.Front
eyeDecal.Texture = 'rbxassetid://16687464344'
eyeDecal.Parent = eyePart

-- get our initial plane props
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front
local eyeSurfaceShape = Enum.PartType.Cylinder

local maxEyeViewingRadius = 50
local maxEyeViewingRadiusSqr = maxEyeViewingRadius*maxEyeViewingRadius

local offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)
travelDistance *= 0.25 -- limit the travel distance our eye can move

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
	if updateTask then
		return
	end

	updateTask = task.defer(function ()
		offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)

		-- limit the travel distance our eye can move
		travelDistance *= 0.25

		updateTask = nil
	end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
	-- can be replaced with whatever target we want to follow
	local position = targetPart.Position

	-- det. whether we're watching the target, and where our desired position is
	local isWatching, desiredPosition = computeEyePosition(
		-- target position & our pre-calculated plane properties
		position, offset, normal, distance, maxAreaSqr,
		-- the maximum distance our eye can travel
		travelDistance,
		-- make sure we only position ourself if our target is within x viewing radius
		maxEyeViewingRadiusSqr,
		-- use our constant value so we're not considering things that are greater than x studs away
		MAX_PLANE_DISTANCE
	)

	-- if we're not already at our desired position - within eps 1e-3 - then interpolate
	-- towards that position
	position = eyePart.Position
	if not desiredPosition:FuzzyEq(position, 1e-3) then
		eyePart.Position = position:Lerp(desiredPosition, dt*6)
	end

	-- e.g. visually show that we're not watching the subject
	eyePart.BrickColor = isWatching and BrickColor.Red() or BrickColor.Black()
end)

--[!] Note: ::getDirectionVector() method should be defined as in the previous script

local function computeMaxTravelDistance(eyeTransform, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)
	maxWidth = maxWidth or 1
	maxHeight = maxHeight or 1
	eyeUpSurface = eyeUpSurface or Enum.NormalId.Top
	eyeFrontSurface = eyeFrontSurface or Enum.NormalId.Front

	local upVector = getDirectionVector(eyeTransform, eyeUpSurface)
	local lookVector = -1*getDirectionVector(eyeTransform, eyeFrontSurface)
	local rightVector = upVector:Cross(lookVector).Unit
	return upVector*maxHeight + rightVector*maxWidth
end

-- USAGE
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front

local maxWidth = 5
local maxHeight = 2.5
travelDistance = computeMaxTravelDistance(planePart.CFrame, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)
1 Like

Looking at your code, I don’t think you need a loop since you have the RunService.Stepped connection already.

Try updating the targetPart inside of the RunService.Stepped instead.

RunService.Stepped:Connect(function (gt, dt)
	targetPart = getclosestplr()
	local position = targetPart.Position
    -- rest of the RunService.Stepped code
end
1 Like

How might I make it so that instead of having to reference a different part with a different name each time, it can make a reference that would work with multiple copies of the script and part(s) without error? Once such reference is script.Parent, but I don’t know how to make a reference like that with WaitForChild.

Also, something seemingly random keeps happening. One eye in particular gets the error " 18:31:48.471 Workspace.Script4:42: attempt to index nil with ‘WaitForChild’ - Server - Script4:42." I didn’t edit any of the script except for renaming stuff like PlanePart3 to PlanePart4. I found a fix to this, which is adding an r6 model, probably also r15, to the game. This model will need to be at a distance away from the eye, otherwise it will target it over the player. I have absolutely no idea why this is happening for only one eye when I only changed the part it’s referencing. I even copied and pasted the entire thing.

The are of code it’s talking about is in here:

local function getclosestplr()
	local bot_position = game.Workspace:WaitForChild('PlanePart6').Position

	local distance = math.huge
	local closest_player_character = nil

	for i, player in pairs(workspace:GetChildren()) do
		-- you have put player, but this is really the players character
		if player:FindFirstChild("Humanoid") then

			local player_position = player.HumanoidRootPart.Position
			local distance_from_bot = (bot_position - player_position).magnitude

			if distance_from_bot < distance then
				distance = distance_from_bot
				closest_player_character = player
			end
		end
	end

	return closest_player_character:WaitForChild('Head') --Specifically here
end

Looking at that code: I’m doubtful it relates to the differences between the R6 and R15 models. The issue is likely as a result of you calling the ::WaitForChild method on the closest_player_character variable - this variable may never be defined within the loop if there are no players present, you would need to add a condition before calling the ::WaitForChild method to determine whether the reference exists.

Is this for for the line you have changed to local planePart = workspace:WaitForChild('PlanePart3')? If so, you have a few options, for example:

  1. If you want to use multiple scripts, change the script such that it uses a relative reference, e.g. use script.Parent.PlanePart with the Script inside of the model/folder that contains PlanePart
  2. Utilise CollectionService

Using your most recent comment, it would look something like this:

Snippet
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')

----------------------------------------
--                                    --
--               Const                --
--                                    --
----------------------------------------

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 10


----------------------------------------
--                                    --
--               Util                 --
--                                    --
----------------------------------------

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
	return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
	local d = normal:Dot(vec) + distance
	return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
	return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
	return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, surfaceShape, forwardNormalId, upNormalId)
	surfaceShape = surfaceShape or Enum.PartType.Block
	upNormalId = upNormalId or Enum.NormalId.Top
	forwardNormalId = forwardNormalId or Enum.NormalId.Front

	local size = part.Size
	local transform = part.CFrame
	local translation = part.Position

	-- compute the plane
	local normal = getDirectionVector(transform, forwardNormalId)
	local distance = -1 * normal:Dot(translation)

	-- compute the part's size from its right & up vector
	local upVector = getDirectionVector(transform, upNormalId)
	local rightVector = normal:Cross(upVector)

	local scale = Vector2.new(
		(size*upVector).Magnitude,
		(size*rightVector).Magnitude
	)

	-- compute the travel distance using the largest axis
	local travelDistance = math.max(scale.X, scale.Y)

	-- compute the area of the surface from its given shape
	local area
	if surfaceShape == Enum.PartType.Cylinder then
		area = math.pi*(travelDistance*0.5 + 1)
	else
		--[!] Treat it as a block instead...
		area = (scale.X*scale.Y)*0.5
	end

	return translation, scale, normal, distance, area, travelDistance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
	position, planeOffset,
	planeNormal, planeDistance, planeRadius,
	maxTravelDistance, maxSqrRadius, maxPlaneDistance
)

	maxSqrRadius = maxSqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
	maxPlaneDistance = maxPlaneDistance or MAX_PLANE_DISTANCE
	maxTravelDistance = maxTravelDistance or planeRadius*0.5

	-- get the target position's closest point on the plane
	local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

	-- get the depth to/from the plane from the target's position
	local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

	-- get the distance of the closest position from the centre point of the plane
	local displacement = closestPoint - planeOffset
	local magnitude = getSqrMagnitude(displacement)

	-- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
	local isOnCorrectSide = distanceToPlane > 0

	-- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
	local isWithinDistance = magnitude <= maxSqrRadius and distanceToPlane <= maxPlaneDistance

	-- if we dont meet either condition, go back to the centre of the plane
	if not isOnCorrectSide or not isWithinDistance then
		return false, planeOffset
	end

	-- compute the new position of the eye when tracking the target position
	displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
	magnitude = math.clamp(magnitude / planeRadius, 0, 1)

	return true, planeOffset + displacement*magnitude*maxTravelDistance
end

-- compute the max travel distance of the eye on its horizontal and vertical axis
-- given the eye transform, its respective axes, and the maximum values for each axis
local function computeMaxTravelDistance(eyeTransform, eyeUpSurface, eyeFrontSurface, maxHorizontal, maxVertical)
	maxVertical = maxVertical or 1
	maxHorizontal = maxHorizontal or 1
	eyeUpSurface = eyeUpSurface or Enum.NormalId.Top
	eyeFrontSurface = eyeFrontSurface or Enum.NormalId.Front

	local upVector = getDirectionVector(eyeTransform, eyeUpSurface)
	local lookVector = -1*getDirectionVector(eyeTransform, eyeFrontSurface)
	local rightVector = upVector:Cross(lookVector).Unit
	return upVector*maxVertical + rightVector*maxVertical
end

-- det. whether a character is alive by checking
-- if (1) they're a descendant of workspace; (2) they have a humanoid; and (3) that the humanoid is alive
local function isCharacterAlive(character)
  if typeof(character) ~= 'Instance' or not character:IsA('Model') or not character:IsDescendantOf(workspace) then
    return false
  end

  local humanoid = character:FindFirstChildOfClass('Humanoid')
  if not humanoid or humanoid:GetState() == Enum.HumanoidStateType.Dead then
    return false
  end

  return true
end

-- find the closest valid player to target
local function getBestValidTarget(planeOffset, planeNormal, planeDistance, maxSqrRadius, maxPlaneDistance)
	maxSqrRadius = maxSqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
	maxPlaneDistance = maxPlaneDistance or MAX_PLANE_DISTANCE

  local bestTarget
  local bestDistance

  -- iterate through every player in the game
  for _, player in next, Players:GetPlayers() do
    local character = player.Character

    -- ignore this player if its character is dead
    if not isCharacterAlive(character) then
      continue
    end

    -- ignore this character if it doesn't have a valid root part
    local rootPart = character.PrimaryPart
    if not rootPart then
      continue
    end

    -- get the character's root position
    local position = rootPart.Position

    -- get the target position's closest point on the plane
    local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

    -- get the depth to/from the plane from the target's position
    local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

    -- get the distance of the closest position from the centre point of the plane
    local displacement = closestPoint - planeOffset
    local magnitude = getSqrMagnitude(displacement)

    -- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
    local isOnCorrectSide = distanceToPlane > 0

    -- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
    local isWithinDistance = magnitude <= maxSqrRadius and distanceToPlane <= maxPlaneDistance

    -- if this character isn't valid then ignore it
    if not isWithinDistance or not isOnCorrectSide then
      continue
    end

    -- accept this player if we don't have a target; or if this target is closer than our previous target
    if not bestDistance or magnitude < bestDistance then
      bestTarget = { Player = player, TargetPart = rootPart }
      bestDistance = magnitude
    end
  end

  return bestTarget
end


----------------------------------------
--                                    --
--               Main                 --
--                                    --
----------------------------------------

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart3')

-- instantiate our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.new(18,18,0.3)
eyePart.Shape = Enum.PartType.Block
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.Transparency = 1
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace

local eyeDecal = Instance.new('Decal')
eyeDecal.Transparency = 0
eyeDecal.Color3 = Color3.new(0, 1, 0)
eyeDecal.Face = Enum.NormalId.Front
eyeDecal.Texture = 'rbxassetid://16687464344'
eyeDecal.Parent = eyePart

-- get our initial properties
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front
local eyeSurfaceShape = Enum.PartType.Cylinder

local eyeMaxVerticalMovement = 2.5
local eyeMaxHorizontalMovement = 5

local maxEyeViewingRadius = 50
local maxEyeViewingRadiusSqr = maxEyeViewingRadius*maxEyeViewingRadius

local offset, scale, normal, distance, maxAreaSqr = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)
local travelDistance = computeMaxTravelDistance(planePart.CFrame, eyeUpSurface, eyeFrontSurface, eyeMaxHorizontalMovement, eyeMaxVerticalMovement)

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
	if updateTask then
		return
	end

	updateTask = task.defer(function ()
		offset, scale, normal, distance, maxAreaSqr = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)

		-- limit the travel distance our eye can move
		travelDistance = computeMaxTravelDistance(planePart.CFrame, eyeUpSurface, eyeFrontSurface, eyeMaxHorizontalMovement, eyeMaxVerticalMovement)

		updateTask = nil
	end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
  local target = getBestValidTarget(
    -- our plane props
    offset, normal, distance,
		-- ensure the target is within our view radius
		maxEyeViewingRadiusSqr,
		-- ensure the target is within x studs of the plane's surface
		MAX_PLANE_DISTANCE
  )

  local isWatching, desiredPosition
  if target and target.TargetPart then
    -- now that we have a target, find the desired position to that target
    local position = target.TargetPart.Position

    -- det. whether we're watching the target, and where our desired position is
    isWatching, desiredPosition = computeEyePosition(
      -- target position & our pre-calculated plane properties
      position, offset, normal, distance, maxAreaSqr,
      -- the maximum distance our eye can travel
      travelDistance,
      -- make sure we only position ourself if our target is within x viewing radius
      maxEyeViewingRadiusSqr,
      -- use our constant value so we're not considering things that are greater than x studs away
      MAX_PLANE_DISTANCE
    )
  else
    -- since there's no valid target, let's go back to the plane origin
    isWatching, desiredPosition = false, offset
  end

  -- if we're not already at our desired position - within eps 1e-3 - then interpolate
  -- towards that position
  local position = eyePart.Position
  if not desiredPosition:FuzzyEq(position, 1e-3) then
    eyePart.Position = position:Lerp(desiredPosition, dt*6)
  end
end)


P.S. I recommend you read through the script and the comments attached - it will answer some of your previous questions with regard to where it should be placed, how to integrate it, and your previous question with regard to how to watch different players etc.

Also, just as a side note: I noticed you placed the method at the bottom of the script. I think it may be a good idea for you to take a look through the Lua documentation, and possibly to take a look at how scopes / blocks work too - find Lua documentation both here and here

1 Like

It seems quite complicated to me. I’ll likely leave everything as it is since I don’t know how to do that stuff. I’ll get working on a summary for everything soon and mark it as the solution, giving credit to both you and the other person who helped me create this script.

I finished the eye. Here are some examples. Also, here’s the script I used, although you’ll need to change the reference for PlanePart to the part in that instance. Also, you might need to insert a humanoid rig to fix an random error. Just make sure it’s a good distance away from the eye. Also make sure the script is under workspace. Thanks to DevSynaptix and overtures I was able to get this done.



Place:
EyeDemo.rbxl (98.3 KB)

Script:

local RunService = game:GetService('RunService')

----------------------------------------
--                                    --
--               Const                --
--                                    --
----------------------------------------

-- i.e. margin of err for fp calc
local EPSILON = 1e-6

-- i.e. max distance between the plane surface and the target position
local MAX_PLANE_DISTANCE = 30


----------------------------------------
--                                    --
--               Util                 --
--                                    --
----------------------------------------

local function getclosestplr()
	local bot_position = game.Workspace:WaitForChild('PlanePart5').Position

	local distance = math.huge
	local closest_player_character = nil

	for i, player in pairs(workspace:GetChildren()) do
		-- you have put player, but this is really the players character
		if player:FindFirstChild("Humanoid") then

			local player_position = player.HumanoidRootPart.Position
			local distance_from_bot = (bot_position - player_position).magnitude

			if distance_from_bot < distance then
				distance = distance_from_bot
				closest_player_character = player
			end
		end
	end

	return closest_player_character:WaitForChild('Head')
end

-- calculate the square magnitude of a vector
local function getSqrMagnitude(vec)
	return vec.X*vec.X + vec.Y*vec.Y + vec.Z*vec.Z
end

-- calculate the closest point on a plane
local function getClosestPointOnPlane(normal, distance, vec)
	local d = normal:Dot(vec) + distance
	return vec - normal*d
end

-- calculate the distance to/from a plane given a plane's properties and a vector
local function getDistanceToPlane(normal, distance, vec)
	return normal:Dot(vec) + distance
end

-- given a transform and a normal id, compute the direction vector e.g. LookVector from Enum.NormalId.Front
local function getDirectionVector(transform, normalId)
	return transform * Vector3.FromNormalId(normalId) - transform.Position
end

-- compute a plane given a part, which we use as a plane origin,
-- and a normal id, which we use to derive the normal vector
local function computePlaneFromPartSurface(part, surfaceShape, forwardNormalId, upNormalId)
	surfaceShape = surfaceShape or Enum.PartType.Block
	upNormalId = upNormalId or Enum.NormalId.Top
	forwardNormalId = forwardNormalId or Enum.NormalId.Front

	local size = part.Size
	local transform = part.CFrame
	local translation = part.Position

	-- compute the plane
	local normal = getDirectionVector(transform, forwardNormalId)
	local distance = -1 * normal:Dot(translation)

	-- compute the part's size from its right & up vector
	local upVector = getDirectionVector(transform, upNormalId)
	local rightVector = normal:Cross(upVector)

	local scale = Vector2.new(
		(size*upVector).Magnitude,
		(size*rightVector).Magnitude
	)

	-- compute the travel distance using the largest axis
	local travelDistance = math.max(scale.X, scale.Y)

	-- compute the area of the surface from its given shape
	local area
	if surfaceShape == Enum.PartType.Cylinder then
		area = math.pi*(travelDistance*0.5 + 1)
	else
		--[!] Treat it as a block instead...
		area = (scale.X*scale.Y)*0.5
	end

	return translation, scale, normal, distance, area, travelDistance
end

-- compute the eye position given a target position,
-- and the pre-computed plane props
local function computeEyePosition(
	position, planeOffset,
	planeNormal, planeDistance, planeRadius,
	maxTravelDistance, maxSqrRadius, maxPlaneDistance
)

	maxSqrRadius = maxSqrRadius or MAX_PLANE_DISTANCE*MAX_PLANE_DISTANCE
	maxPlaneDistance = maxPlaneDistance or MAX_PLANE_DISTANCE
	maxTravelDistance = maxTravelDistance or planeRadius*0.5

	-- get the target position's closest point on the plane
	local closestPoint = getClosestPointOnPlane(planeNormal, planeDistance, position)

	-- get the depth to/from the plane from the target's position
	local distanceToPlane = getDistanceToPlane(planeNormal, planeDistance, position)

	-- get the distance of the closest position from the centre point of the plane
	local displacement = closestPoint - planeOffset
	local magnitude = getSqrMagnitude(displacement)

	-- since the eye is pseudo-2d we need to check whether the eye is in front or behind the plane
	local isOnCorrectSide = distanceToPlane > 0

	-- check whether the eye is within the radius that we care about, and if they're not too far from the plane (in terms of depth)
	local isWithinDistance = magnitude <= maxSqrRadius and distanceToPlane <= maxPlaneDistance

	-- if we dont meet either condition, go back to the centre of the plane
	if not isOnCorrectSide or not isWithinDistance then
		return false, planeOffset
	end

	-- compute the new position of the eye when tracking the target position
	displacement = magnitude == 1 and displacement or (magnitude > EPSILON and displacement.Unit or Vector3.zero)
	magnitude = math.clamp(magnitude / planeRadius, 0, 1)

	return true, planeOffset + displacement*magnitude*maxTravelDistance
end


----------------------------------------
--                                    --
--               Main                 --
--                                    --
----------------------------------------

-- await our prefabs
local planePart = workspace:WaitForChild('PlanePart5')
local targetPart = getclosestplr()

-- create our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'DebugPart'
eyePart.Size = Vector3.new(18,18,0.12)
eyePart.Shape = Enum.PartType.Block
eyePart.Position = planePart.Position
eyePart.Anchored = true
eyePart.CanTouch = false
eyePart.CanCollide = false
eyePart.CanCollide = false
eyePart.Transparency = 1
eyePart.BrickColor = BrickColor.Green()
eyePart.TopSurface = Enum.SurfaceType.Smooth
eyePart.BottomSurface = Enum.SurfaceType.Smooth
eyePart.Parent = workspace
local eyeDecal = Instance.new('Decal')
eyeDecal.Transparency = 0
eyeDecal.Color3 = Color3.new(0, 1, 0)
eyeDecal.Face = Enum.NormalId.Front
eyeDecal.Texture = 'rbxassetid://16687464344'
eyeDecal.Parent = eyePart

-- get our initial plane props
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front
local eyeSurfaceShape = Enum.PartType.Cylinder

local maxEyeViewingRadius = 100
local maxEyeViewingRadiusSqr = maxEyeViewingRadius*maxEyeViewingRadius

local offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)
travelDistance *= 0.25 -- limit the travel distance our eye can move

-- update the plane's properties if the plane is moved
local updateTask
planePart:GetPropertyChangedSignal('CFrame'):Connect(function ()
	if updateTask then
		return
	end

	updateTask = task.defer(function ()
		offset, scale, normal, distance, maxAreaSqr, travelDistance = computePlaneFromPartSurface(planePart, eyeSurfaceShape, eyeFrontSurface, eyeUpSurface)

		-- limit the travel distance our eye can move
		travelDistance *= 0.25

		updateTask = nil
	end)
end)

-- update the eye position at runtime
RunService.Stepped:Connect(function (gt, dt)
	-- can be replaced with whatever target we want to follow
	targetPart = getclosestplr()
	local position = targetPart.Position

	-- det. whether we're watching the target, and where our desired position is
	local isWatching, desiredPosition = computeEyePosition(
		-- target position & our pre-calculated plane properties
		position, offset, normal, distance, maxAreaSqr,
		-- the maximum distance our eye can travel
		travelDistance,
		-- make sure we only position ourself if our target is within x viewing radius
		maxEyeViewingRadiusSqr,
		-- use our constant value so we're not considering things that are greater than x studs away
		MAX_PLANE_DISTANCE
	)

	-- if we're not already at our desired position - within eps 1e-3 - then interpolate
	-- towards that position
	position = eyePart.Position
	if not desiredPosition:FuzzyEq(position, 1e-3) then
		eyePart.Position = position:Lerp(desiredPosition, dt*6)
	end

	-- e.g. visually show that we're not watching the subject
	eyePart.BrickColor = isWatching and BrickColor.Red() or BrickColor.Black()
end)

--[!] Note: ::getDirectionVector() method should be defined as in the previous script

local function computeMaxTravelDistance(eyeTransform, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)
	maxWidth = maxWidth or 1
	maxHeight = maxHeight or 1
	eyeUpSurface = eyeUpSurface or Enum.NormalId.Top
	eyeFrontSurface = eyeFrontSurface or Enum.NormalId.Front

	local upVector = getDirectionVector(eyeTransform, eyeUpSurface)
	local lookVector = -1*getDirectionVector(eyeTransform, eyeFrontSurface)
	local rightVector = upVector:Cross(lookVector).Unit
	return upVector*maxHeight + rightVector*maxWidth
end

-- USAGE
local eyeUpSurface = Enum.NormalId.Top
local eyeFrontSurface = Enum.NormalId.Front

local maxWidth = 5
local maxHeight = 2
travelDistance = computeMaxTravelDistance(planePart.CFrame, eyeUpSurface, eyeFrontSurface, maxWidth, maxHeight)

The code for what I mentioned is in the ‘Snippet’ tag on my last comment - I added some comments for you to read through to explain the changes

1 Like