If-statement inside RunService.Heartbeat runs illegally / when it should not

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

I am making a pathfinding code where the AI checks out a spot by using PathfindingService.

  1. What is the issue? Include screenshots / videos if possible!
    Well, the code seems to be running too fast. In fact, I have added an extra boolean debounce to see if it would change anything, but no:
local pathfindingConnection:RBXScriptConnection = RunService.Heartbeat:Connect(function(dt:number)
	--Handling sounds first here, not relevant to the issue so I've left it out
	
	local nearestSound:Vector3 = getNearestSound(allNearestSounds) --This function is not relevant to the issue at all. all it does is literally sorting out a table and getting the nearest position
	if nearestSound and ((currentSoundPath == nil) and (not currentSoundPathDebounce)) then
		print("About to SoundPathing", (currentSoundPath == nil), (not currentSoundPathDebounce), (currentSoundPath == nil) and (not currentSoundPathDebounce))
		local path:Path = PathfindingService:FindPathAsync(rigHRP.Position, nearestSound) --I know that 
                                                                                          --I should use the better and newer version of the pathfinding function, however since I am playing around with pathfinding for the first time I thought I should start out small. as long as I don't fix this issue I don't see a reason to switch methods
		
		currentSoundPath = path
		currentSoundPathDebounce = true
		
		if path.Status == Enum.PathStatus.Success then

			if not walkingTrack.IsPlaying then walkingTrack:Play() end

			--Avoid going to path when in the air
			if humanoid.FloorMaterial ~= Enum.Material.Air then
				pathfindingDebounce = true
				local waypoints:{PathWaypoint} = path:GetWaypoints()

				--Visualises the path
				for i,waypoint in waypoints do
					local waypointPosition:Vector3 = waypoint.Position					

					local t = Instance.new("Part")
					t.Parent = showingMovesFolder
					t.Size = Vector3.one
					t.Shape = Enum.PartType.Ball
					t.CanCollide = false
					t.CanQuery = false
					t.Anchored = true
					t.Position = waypointPosition
					t.BrickColor = BrickColor.Random()
				end

				for i,waypoint in waypoints do
					local waypointPosition:Vector3 = waypoint.Position					

					local waypointAction:Enum.PathWaypointAction = waypoint.Action
					if waypointAction == Enum.PathWaypointAction.Jump then 
						humanoid.Jump = true
					end
					humanoid:MoveTo(waypointPosition)
					humanoid.MoveToFinished:Wait()
					
					if i == #waypoints then
						print("Path done!")
						currentSoundPath = nil
						currentSoundPathDebounce = false
						if walkingTrack.IsPlaying then
							walkingTrack:Stop()
							idleTrack:Play()
						end
					end

				end

			end
		else
			stopPathfinding()
		end
		return
	end

currentSoundPath is a nil variable created outside of the Heartbeat function, same with currentSoundPathDebounce, but that one’s a boolean set to false.
As you can see, the ((currentSoundPath == nil) and (not currentSoundPathDebounce)) does basically nothing: somehow, it takes multiple frames, 5 in this case, before the boolean and path are actually set. It shouldn’t be the case: as soon as the if statement allows it, the path and debounce being set should immediately, for the next frame, make it impossible to run again before the NPC is done checking out 1 place first. Another very annoying fact is how the Path done! print statement prints out 5 times in a row, also instantaneously, basically ignoring the MoveToFinished:Wait() yield. The NPC doesn’t move from it’s place at all, MoveToFinished shouldn’t fire if the NPC hasn’t made a single step!!!

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    Checking out anything available, but as usual, no luck there.

Does anyone have enough experience with NPC or just in general to spot what the issue is? Am I doing the debounce in the wrong way? What is it that I should do otherwise instead? Thanks in advance, any comment is welcome: I feel like going crazy right now over this small issue

2 Likes

(your 2nd sentence wasn’t finished, so we may be missing some information)

Do you have multiple scripts possibly caused by hitting ctrl D or pasting a script into multiple items at the same time?
If the function is firing 5 times you need to find out why from that relevant section of code.

If you have code controlled by variables in if checks then find out why those variables are allowing the if to be passed.
When you find a variable that isn’t reading what you expect you can go to the part of the code where the variable is set to find the issue there.

2 Likes

First things first, thanks for your answer!

I got sidetracked while writing this post, excuse me for it. I removed the 2n sentence because it wouldn’t have delivered any new information.

Not at all, the whole NPC code runs inside of one single serverscript that’s parented inside of the NPC/rig itself.

From what I could tell, it runs 5 times because the Heartbeat event runs every frame. Only after those X times (in this case it’s 5, it’s sometimes 6, 10, it varies from testing to testing) it’s like the script realizes “ooooh, I forgot to actually set the debounce variables!” and they are actually set

Yup, and that brings me here to the devforum: I am not using the variables anywhere else, this is straight up literally the “whole” code (in question, there are other things that run after this code snippet, but those have nothing to do with the snippet in question).

if nearestSound and ((currentSoundPath == nil) and (not currentSoundPathDebounce)) then
		print("About to SoundPathing", (currentSoundPath == nil), (not currentSoundPathDebounce), (currentSoundPath == nil) and (not currentSoundPathDebounce))
		local path:Path = PathfindingService:FindPathAsync(rigHRP.Position, nearestSound) --I know that 
                                                                                          --I should use the better and newer version of the pathfinding function, however since I am playing around with pathfinding for the first time I thought I should start out small. as long as I don't fix this issue I don't see a reason to switch methods
		
		currentSoundPath = path
		currentSoundPathDebounce = true

this should be it, this should be enough. Code runs, if statement results true, and the variables are directly blocked in a way that shouldn’t make it possible for the if statement to run once more. Right now I am checking the variables (in heartbeat) before and after initiating a sound: Before, both conditions are set to true. if statement runs, after the whole pathfinding section both variables are set to false. And when the next frame starts, they’re still set to false. Meaning the debounce worked, however they still triggered the if statement multiple times.

I am beyond lost right now and have no clue on what to do nor why this is happening

1 Like

Hi there, I am afraid I do not have very much experience with pathfinding, but I think I can try to help with what little I know.

  1. The debounce not working, I noticed you were not changing the bool value for the debounce until after the game calculates the path, I think that might be the reason for the delay: it might be taking the engine 5 ticks (I do not know if that is the proper term) to calculate the path, so by the time the script gets to your debounce the function has already run 5 times. Try putting the bool change before anything else in the code runs and see if that fixes it.

  2. NPC is not moving this might be because you are running the MoveTo function every tick, leaving the NPC with not having enough time to move. This problem will probably go away once the debounce is fixed.

Again I am not very familiar with pathfinding so this is really the best I can offer.

2 Likes

After having done some inspection, I am still relatively unsure to have found the source of the issue, but maybe a small hint:

for i,waypoint in waypoints do
	print("waypoint: ", i)
	local waypointPosition:Vector3 = waypoint.Position					

	local waypointAction:Enum.PathWaypointAction = waypoint.Action
	if waypointAction == Enum.PathWaypointAction.Jump then 
		humanoid.Jump = true
	end
	humanoid:MoveTo(waypointPosition)
	humanoid.MoveToFinished:Wait()

	if i == #waypoints then
		print("Path done!")
		currentSoundPath = nil
		currentSoundPathDebounce = false
		if walkingTrack.IsPlaying then
			walkingTrack:Stop()
			idleTrack:Play()
		end
	end

end

When checking this section:
image

The loop is run multiple times at once. Even tho it’s supposed to wait for the humanoid to move to the location first. Only then the currentSoundPath is set to nil and the if statement should be able to run again. I have no idea why the NPC isn’t moving at all, nor why MoveToFinished:Wait isn’t… waiting for MoveToFinished to be done moving, I have no idea if that might help finding the issue, please let me know if anyone has any information about this

1 Like

Hi and thanks for your answer!
I tried changing the boolean debounce variable before calculating the path, and that seems to have stopped instantaneous double printings, so that’s one issue less! However the NPC still refuses to move which is the weird part: once it gets a location, it should be, well, moving to it, that way MoveToFinished can fire.

I tried blocking the Humanoid:MoveTo() line, to see if MoveToFinished actually yielded and what I got:

Turns out it’s not yielding at all, which I find very weird. I will try doing some research before falling asleep, at least now I have some clue to check out, unlike before. Thanks again!

1 Like

No problem, glad I was able to help where I could

2 Likes

Aight, found the issue (turns out it was something outside of the code snippet…), and I can’t believe it was such a dumb small thing: after removing the last return in the code snippet, the code continued, and since the hearing distance is way larger than the view distance, the code, at the very end of the whole NPC, was thinking that the NPC was too far away and stopped the pathfinding. Stopping the pathfinding in this sense is nothing else than making the char move in it’s own position in order to “stay still”. Hence the MoveToFinished wasn’t waiting: everytime the whole code executed, it stopped the pathfinding, moved the character towards it’s own position to stop it from moving, which triggered MoveToFinished.

If it wasn’t for you 2 I wouldn’t have been able to figure it out, however since I can only pick one solution and not 2, I’ll pick @ndporter2’s one since without their “tips” I wouldn’t have done a full investigation and come to that conclusion. Thanks a lot and hopefully this might help out others as well.

TL;DR: Moral of the story: if MoveToFinished is not waiting, you’re using MoveTo somewhere later on in your code, making the code skip the MoveToFinished line and causing you some issues and troubles

2 Likes

Great job! glad the problem was solved and I hope all goes well with your NPC creation endeavors.

3 Likes

Thanks! Although there are multiple paths being created at the same time, I think that issue’s more of a RunService events related one, I’m just glad that now MoveToFinished actually yields as intended :smile:. Hope this will help other people in the future as well
(if I manage to fix the RunService issue, I’ll edit/add a new post here to inform about what I did. Otherwise it’ll mean I’ll have done a workaround/am simply dealing with it ¯\ _ (ツ)_/¯ )

1 Like

Ok, the way I fixed the issue of multiple paths being generated at once:

if lastHeardSound == nil then lastHeardSound = nearestSound+Vector3.new(15,0,0) end

		if (lastHeardSound-nearestSound).Magnitude >= 15 and not soundDebounce then
			soundDebounce = true
			task.delay(2.5, function()
				soundDebounce = false
			end)
			local path:Path = PathfindingService:FindPathAsync(rigHRP.Position, nearestSound)

			currentSoundPath = path
--rest of code that I already posted above

This seems to have done the trick, simply delaying the debounce by a fixed amount in order to save on resources, avoiding lag and it fixes the issue of multiple paths at once. Hope this idea might help someone out in the future, with this, everything should have been covered.

Thanks for everything and cheers!!!

1 Like