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!
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:
-
You may need to increase the
MAX_PLANE_DISTANCE
variable - this describes the minimum distance between the target and the plane origin -
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