AI goes to old waypoints

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)
1 Like

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.

1 Like

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%

Edit: Fixed some mistakes I spotted

1 Like

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

1 Like

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

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