(if this is too big of a request to post here, let me know and I'll remove this)
I'm trying to develop an npc that follows players around. Goals of this npc:
-searches for target based on closest player & line-of-sight
-checks for obstructions whenever it is moving
-if gets stuck trying to chase the player, will pathfind to the last position the player was seen at
-if it does not find a target again, it patrols (random movement) until it finds a target again
Currently, this script is super buggy:
- the patrol() function sometimes doesn't make the NPC move at ALL
- target acquisition is bad
- pathfinding doesn't always activate
- target isn't always removed after a certain amount of time not being able to find target
If anyone can help me out with this, here is the code: (I've explained what everything does, but let me know if I can help clarify further)
link to NPC model: https://www.roblox.com/NPC-v1-item?id=459931346
--master variables
----------------------------------------------------
----------------------------------------------------
bot = script.Parent
target = nil
maxDist = 300
targDist = maxDist
--variables for determining what counts as "stuck"
stuckDist = 10
stuckTime = 2
-- how long it can try to find a target until it sets target to nil
maxTargetTime = 5
targetTime = 0
-- last vector3 the target was seen at
lastSeenPos = nil
--the amount a time the NPC has been moving for (for checking if stuck after a certain time)
moveTime = 0
--variables for checking current vs old position
curPos = nil
lastPos = nil
lastPosReset = false -- moveTime is set to 0 if lastPosReset is false (happens every 2 seconds)
-- self explainatory
pathfindingService = game:GetService("PathfindingService")
pathing = false
-- bool that SHOULD be set to true whenever the NPC is moving
moving = false
-- patrol variables, including debounce, the position to WalkTo, and current time spent patrolling (for checking if stuck)
patrolDeb = false
patrolPos = nil
patrolTime = 0
--movement functions
----------------------------------------------------
----------------------------------------------------
function moveToTarget(patrolPos)
if target then
local torso = target:findFirstChild("Torso")
if torso then
print("torso found, moving to it")
print("torso pos = " .. tostring(torso.Position))
bot.Humanoid:MoveTo(torso.Position)
end
elseif lastSeenPos then
bot.Humanoid:MoveTo(lastSeenPos)
elseif patrolPos then
bot.Humanoid:MoveTo(patrolPos)
patrolPos = nil
elseif not target then
end
end
----------------------------------------------------
----------------------------------------------------
--AI/pathfind functions
----------------------------------------------------
----------------------------------------------------
-- patrol randomly
patrolDirs = {math.pi,math.pi/2,math.pi/4,math.pi*2}
function patrol()
if not patrolDeb then
patrolDeb = true
local dir = math.random(1,#patrolDirs)
local dirChosen = patrolDirs[dir]
print("new Dir = " .. dirChosen)
local newCF = bot.Torso.CFrame * CFrame.Angles(0,math.rad(math.random(30,170)),0)
local direction = newCF.lookVector * (math.random(10,30)*10)
print("direction = " .. tostring(direction))
patrolPos = direction
patrolTime = 0
moveToTarget(patrolPos)
else
if ((bot.Torso.Position - patrolPos).magnitude) < 5 then
patrolDeb = false
elseif patrolTime > stuckTime then
print("stuck on patrol")
patrolDeb = false
end
end
patrolTime = patrolTime + .1
end
-- check to makesure it can see the player using raycasting
function checkSight(char)
local sight = false
local direction = (char.Torso.Position - bot.Head.Position).unit; -- Unit makes it length 1.
direction = direction * maxDist; -- Allow raycasting up to maxDist studs away
local partHit, positionHit = workspace:FindPartOnRay( Ray.new(bot.Head.Position, direction) , bot );
-- We ignore the local player's character for the raycast.
-- (This would require this script to be in a local script)
-- The laser should stretch from "from" to "positionHit".
if partHit.Parent == char then
sight = true
lastSeenPos = char.Torso.Position
end
return sight
end
-- find targets
function findTarget()
local potTarget = nil
targDist = maxDist
local pList = game.Players:GetPlayers()
for i,v in pairs(pList) do
local char = v["Character"]
if char then
print("char found")
local hum = char:findFirstChild("Humanoid")
if hum and (hum.Health ~= 0) then -- valid humanoid
local torso = char:findFirstChild("Torso")
if torso then
local distance = (torso.Position - bot.Torso.Position).magnitude
if (distance < maxDist) and (distance < targDist) and checkSight(char) then
print("target passed checks, setting to PotTarget")
targDist = distance
potTarget = char
end
end
-- target = char
-- print("target = " .. target.Name )
end
end
end
if potTarget then
target = potTarget
else
target = nil
end
--print("target = " ..target.Name)
end
-- check for obstructions using a raycasting
function checkObstruction()
-- assume "from" is some Vector3, in your example the Torso's Position
-- assume "target" is some other Vector3, in your example
-- the mouse's hover position (mouse.Hit.p)
if target then
checkTarg = target
else
checkTarg = nil
end
local newCF = bot.Torso.CFrame * CFrame.new(0,-.5,0)
local direction = newCF.lookVector -- Unit makes it length 1.
direction = direction * 3; -- Allow raycasting up to 1000 studs away
local partHit, positionHit = workspace:FindPartOnRay( Ray.new(bot.Torso.Position, direction) , checkTarg );
-- We ignore the local player's character for the raycast.
-- (This would require this script to be in a local script)
-- The laser should stretch from "from" to "positionHit".
if partHit then
bot.Humanoid.Jump = true
else
bot.Humanoid.Jump = false
end
end
-- a loop that checks for obstacles whenever "moving" is true
checkObsLoop = coroutine.wrap(function()
while true do
wait(.1)
if moving then
print("moving")
checkObstruction()
else
print("not moving")
end
end
end)
-- function to check if the NPC got stuck (hasn't moved out of a certain radius in x number of seconds)
function checkStuck(cur,last)
local stuck = false
local curDist = (last - cur).magnitude
if curDist < stuckDist then
stuck = true
else
end
return stuck
end
-- pathfinding function (also has a check to make sure it doesn't get stuck)
function pathfind(lastPos)
local start = bot.Torso.Position
local path = pathfindingService:ComputeRawPathAsync(start, lastPos, 500)
local points = path:GetPointCoordinates()
pathing = true
if #points < 1 then return end
moving = true
for _, point in ipairs(points) do
print("moving to: ", point)
bot.Humanoid:MoveTo(point)
moveTime = 0
lastPosReset = false
-- Don't try moving to the next point until we are close enough to the current point in the path
repeat
wait(.1)
local distance = (point - bot.Torso.Position).magnitude
curPos = bot.Torso.Position
if not lastPosReset then
moveTime = 0
lastPos = curPos
lastPosReset = true
else
if moveTime > stuckTime then -- check to see if got stuck during the pathing
if checkStuck(curPos,lastPos) then
target = nil
lastSeenPos = nil
break
end
end
end
moveTime = moveTime + .1
until distance < 3
end
pathing = false
moving = false
print("path done")
end
checkObsLoop() -- activates this thread
----------------------------------------------------
----------------------------------------------------
-- master true loop
----------------------------------------------------
----------------------------------------------------
while true do
wait(.1)
-- this chunk of the loop is for Finding/Moving towards the target, or moving towards the last position it saw them @
if target then
print("yes target")
local distance = (target.Torso.Position - bot.Torso.Position).magnitude
if distance > 3 then
print("dist is larger")
if checkSight(target) then
print("target in sight")
moving = true
moveToTarget()
else
target = nil
moving = true
moveToTarget(lastSeenPos)
end
end
if targetTime > maxTargetTime then
target = nil
targetTime = 0
moving = false
end
elseif not target then
patrol()
findTarget()
end
-- this chunk of the loop is for determining if the NPC has gotten stuck during its movement
curPos = bot.Torso.Position
if not lastPosReset then
moveTime = 0
lastPos = curPos
lastPosReset = true
else
if moveTime > stuckTime then
if checkStuck(curPos,lastPos)then
if lastSeenPos then
if not pathing then
if target then
pathfind(lastSeenPos)
lastPosReset = false
moveTime = 0
else
lastPosReset = false
moveTime = 0
patrol()
end
else
lastPosReset = false
moveTime = 0
patrol()
end
else
lastPosReset = false
moveTime = 0
patrol()
end
else
lastPosReset = false
moveTime = 0
end
end
end
targetTime = targetTime + .1
moveTime = moveTime + .1
end
----------------------------------------------------
--end of script
