hello! i have been losing my mind over this. so, i have this ai that just patrols around and when it sees you it starts chasing you. the problem is, when it starts chasing, it will first finish patrolling then start chasing, and even then it goes to your old position. i have tried EVERYTHING. just setting an index will lead to it getting stuck because the waypoint will be the same position as its current position (i tried to skip it and it just didn’t work) if i remove the movetofinished then it will not go around walls and would be the equivalent of just spamming moveto on the player. this is a singleplayer game.
What it does:
First it plays an animation for 30 seconds, then stops it and enables it’s Animate script for walking and stuff. It fires a remote for dying which is handled on the client since on the server you get killed from further away, I know this isn’t good practice but if someone messes around with this they’re just ruining their own experience. It assigns some variables like its patrol points, pathfinding service, its torso etc, and then it gets the player’s torso (again, singleplayer). Find target just shoots out a block cast every frame pretty much to see the player. if they’re seen, it will directly chase them. Walk() just walks to the waypoints. It computes the path, gets the waypoints, loops through them (if I just use an index it will get on top of an obstacle and stop moving because the next few waypoints are its own position for some reason) then it waits for it to finish moving (if I remove that it will walk into walls and not go around due to the loop resetting itself too much)
I hope I explained that well lol
code:
script.Parent.Animate.Enabled = false
local anim = script.Parent.Humanoid:LoadAnimation(script.Parent.Stun)
anim:Play()
task.wait(30)
anim:Stop()
script.Parent.Animate.Enabled = true
game.ReplicatedStorage.RemoteEvents.Awaken:FireAllClients()
local patrol = workspace.Technical.CurrentMap.Value.Waypoints:GetChildren()
local pfs = game:GetService("PathfindingService")
local pathparams = {
["AgentHeight"] = 6.7,
["AgentRadius"] = 2.5,
["AgentCanJump"] = true,
["AgentCanClimb"] = true,
["WaypointSpacing"] = math.huge,
}
local waypoints = {}
local torso = script.Parent.Torso
local humanoid = script.Parent.Humanoid
local root = script.Parent.HumanoidRootPart
local plrhumanoid = game.Players:GetPlayers()[1].Character.HumanoidRootPart
local seen = false
game.ReplicatedStorage.RemoteEvents.Death:FireAllClients(root, humanoid, plrhumanoid)
local function FindTarget()
if not seen then
local rayparams = RaycastParams.new()
rayparams:AddToFilter(root.Parent)
rayparams.FilterType = Enum.RaycastFilterType.Exclude
local ray = workspace:Blockcast(root.Parent.RayCamera.CFrame, Vector3.new(10, 20, 200), Vector3.new(100), rayparams)
pcall(function()
if ray.Instance.Parent.Name == plrhumanoid.Parent.Name then
seen = true
end
end)
end
end
local function Walk(Target)
script.Parent.PrimaryPart:SetNetworkOwner(nil)
local path = pfs:CreatePath(pathparams)
path:ComputeAsync(torso.Position, Target.Position)
local waypoints = path:GetWaypoints()
local success, result = pcall(function()
for _, nextwaypoint in pairs(waypoints) do
if waypoints.Action == Enum.PathWaypointAction.Jump then
humanoid.Jump = true
end
humanoid:MoveTo(nextwaypoint.Position)
humanoid.MoveToFinished:Wait()
end
end)
if not success then
warn(result)
end
end
task.spawn(function()
while task.wait() do
if not seen then
FindTarget() Walk(patrol[math.random(1, #patrol)])
else
Walk(plrhumanoid)
end
end
end)
I don’t use pathfinding service too often, but I believe the issue could be from here:
Even if the seen variable is set to true, any humanoid moving through the waypoints table will continue to walk to the waypoints until the loop finishes (the loop is yielding the iteration of the while loop, preventing it from updating). So, you should store the MoveToFinished RBXScriptConnection into a variable and disconnect it when necessary.
The actual code that would likely need to be written to fix it is kind of a lot (nearly an entire re-write).
But to recap, the issue you’re facing is because the loop doesn’t break out of itself when the player is spotted, making it continue to halt the while loop until it finishes.
i’m aware, i never knew how to stop the movetofinished, the thing is, if i completely remove it, then it will walk into walls due to the for loop resetting itself too fast. i tried indexing the waypoints but i’m not sure how to do that reliably because the first one is literally the current position, and the 2nd one until an unknown number will be the same position if it gets on top of something for some reason. it’s pure pain.
I would say one way to possibly solve this issue are events and threads. Basically, instead of looping use coroutines and events to pause and resume the thread:
-- Example
local running = nil -- the while loop's main thread
local connection = nil -- the humanoid's MoveToFinished event
local function NPC_Pathfind(target, fthread: thread)
running = coroutine.running() -- sets the current running thread to the current thread
-- define the pathfinding stuff
-- yada yada
local index = 1 -- i know you said you used this before, but i think this method is more efficient for something like this
connection = humanoid.MoveToFinished:Connect(function(reached)
if reached and ((index + 1) < #waypoints) then -- make sure there's another point available
humanoid:MoveTo(waypoints[index + 1].Position) -- move to the next point
else
connection:Disconnect() -- disconnect the old connection
coroutine.resume(running) -- continue the main thread
end
end)
humanoid:MoveTo(waypoints[index+1].Position) -- move to the next position
if not seen then
coroutine.yield() -- pause the current thread until it is resumed again
end
end
This function basically does the same thing the for loop did, but allows other parts of the script to interact with it.
Following that, you can basically do the same thing that’s done within the event connection (it’s easier to define another function for this for re-usability):
local function NPC_EnableTracking()
if connection and connection.Connected then connection:Disconnect() end
if running and coroutine.status(running) ~= "running" then coroutine.resume(running) end
end
local function FindTarget()
if not seen then
local rayparams = RaycastParams.new()
rayparams:AddToFilter(root.Parent)
rayparams.FilterType = Enum.RaycastFilterType.Exclude
local ray = workspace:Blockcast(root.Parent.RayCamera.CFrame, Vector3.new(10, 20, 200), Vector3.new(100), rayparams)
pcall(function()
if ray.Instance.Parent.Name == plrhumanoid.Parent.Name then
seen = true
NPC_EnableTracking() -- make the npc track more things
end
end)
end
end
And the function can be inserted in the main while loop (if neccessary):
task.spawn(function()
while task.wait() do
if not seen then
Walk(patrol[math.random(1, #patrol)])
else
NPC_EnableTracking() -- don't halt the thread
Walk(plrhumanoid)
end
end
end)
I didn’t proof-read most of this, so I don’t know if this works 100%
Thank you, I’ll try it in a few hours (something personal happened and i am currently taking a break) I’m not sure if the waypoint index thing will work since the table is overwritten over and over, but to be fair I just woke up and didn’t read this thoroughly so maybe you resetted the value. I did want to add ray casting to the main loop but uh coding at 3 in the morning isn’t good
Honestly I just use a WaitForFirstEvent (Waits for the first event passed to fire before continuing) function then re-run the Walk function.
Though I do have 2 - 3 solutions for this.
WaitForFirstEvent Code
local function WaitForFirst(...)
local thread = coroutine.running()
local connections = {}
local delay_thread
local WaitFor = function()
for _, connection in ipairs(connections) do
connection:Disconnect()
end
table.clear(connections)
if (delay_thread and delay_thread ~= coroutine.running() and coroutine.status(delay_thread) == "suspended") then
-- // Checking specifically for suspended, because can't resume
-- // Dead threads, or threads that's status is "normal"
task.cancel(delay_thread)
end
if (coroutine.status(thread) ~= "suspended") then return end
task.spawn(thread)
end
delay_thread = task.delay(3, function()
WaitFor()
end)
for i = 1, select("#", ...) do
local event = select(i, ...)
table.insert(connections, event:Once(WaitFor))
end
return coroutine.yield()
end
local result = WaitForFirst(endTask.Event, humanoid.MoveToFinished)
if not result then break end
Which within the primary game loop for NPCs or what’s doing the pathfinding would automatically overwrite the pathfinding each step. I usually go for 0.5 seconds, you can do lower but pathfinding is quite computational so it’ll just eat up computations. (You can do an optimization regarding if the user is in plain sight and not behind something to just walkto the target’s position and follow the humanoid)
repeat
-- check/get target
if target then
-- // Firing false so the waitforfirstevent returns false, canceling loop
endTask:Fire(false)
-- // And you're save to re-run the walk function
Walk(target)
end
until false
The thing about this is ai development requires multiple threads but lets look past that for the time being
this is a ai module script i shared with people who dm’d me, a little modified
local AI = {}
AI.__index = AI
function AI.new(Dummy)
--Dummy is the object you pass after calling AI.new() and requireing AI
--make Dummy do stuff
--metatable method, call and use self to address self variables in helper functions
local self = setmetatable({}, AI)
--example vars
self.Character = Dummy
self.Status = "Roam"
self.Target = Instance.New("ObjectValue")
--use helper functions, use self to call.
self:DetermineDecisionBasedOnStatus()
--make use of threads for things like target detection
spawn(function()
end)
end
function AI:DetermineDecisionBasedOnStatus()
if self.Status == "Roam" then
--roam function
end
end
return AI
lets say we have a Interrupt Bool Object, this signals any applicable threads to interrupt their process and stop–if it isnt clear by now we need to interrupt the current walk thread so that it stop
lets assume self:TargetDetection() triggers a change to a self.Target.Value (Object Value), Signal a Interruption by doing Interrupt.Value = true Interrupt.Value = false. the basic self:Move() will have a :GetPropertyChangedSignal to receive the interruption signal to stop any further moving, afterwards, trigger the self:Chase() from the targetdetection thread
while my solution has no bearing on your current code, I think you will understand what needs to be done
edit:
This is a great resource to get started with AI as I had derived the above sample script and other work from learning how to use the resource