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

I already added it at the end previously

Is that for the same code you referenced in the comment I’ve replied to here? If so, those are two different scripts; the other one addresses the other issues you had with regard to referencing nil values and correctly integrates the new method to derive travel distance

Unsure if it’s in the .rbxl file as I’m not on my computer rn, apologies if it is!

1 Like

The code you mentioned does appear to be different. When I tested it, it only appeared to look at the player if they were somewhere in front of the eye, not beside it.

There are two reasons for this, you may have seen them described in the code already within the comments, but essentially:

  1. You may need to increase the MAX_PLANE_DISTANCE variable - this describes the minimum distance between the target and the plane origin

  2. The maxEyeViewingRadius variable may need to be increased - this describes the minimum spherical distance from the origin of the plane to the target; this is likely why you are seeing it not look at the target when beside it

See the following:

File

EyeDemo.rbxl (91.0 KB)

Code
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 = 50

----------------------------------------
--                                    --
--               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                 --
--                                    --
----------------------------------------

local planePart = script.Parent

-- instantiate our eye
local eyePart = Instance.new('Part')
eyePart.Name = 'EyePart'
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 = script.Parent

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 = 60
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)

Note: I’ve updated your .rbxl file - the changes to the code / file include using relative references; so you need to place the script inside the ‘PlanePart’ now instead of placing them in workspace. If you need to increase the variables described above, look inside the ‘PlanePart’ and change the variables :slight_smile:

1 Like