Enemy AI randomly stops moving and does not recalculate path

The AI basically roams around the room/area until it finds a player.
But sometimes, on a random occasion, the AI just doesn’t roam around and does not recalculate it’s path.

I’ve been debugging for 2 hours and I can’t seem to find the problem with the code..

function MisprintTemplate.new(misprint: Model, paths: {BasePart})
	local self = setmetatable({}, MisprintTemplate)
	self._trove = Trove.new()
	self._path_trove = self._trove:Add(Trove.new())
	self._pathfinder = self._trove:Add(SimplePath.new(misprint, {
		AgentCanJump = false,
		AgentClimb = false,
	}))
	self._model = self._trove:Add(misprint)
	self._instance_paths = paths
	self._lastTick = os.clock()
	self._last_attack_tick = os.clock()
	
	self._roaming = false
	self._on_aggro = false
	self._attacking = false
	
	self._hitbox_size = vector.create(7, 7, 7)
	self._range = 10
	self._max_angle = 120
	self._loop_check = 5
	self._aggro_distance = 40
	self._hit_cooldown = 1.25
	self._attack_distance = 15
	self._hitbox_check_cooldown = 0.025
	
	self._updateDelay = 0.25 + math.random() * 0.25
	self._default_walkspeed = self._model:GetAttribute("WalkSpeed") or 16
	self._damage = self._model:GetAttribute("Damage") or 1
	self._run_speed = self._model:GetAttribute("RunSpeed") or 24
	--> Init misprint in workspace
	self._model.Parent = LoadedMisprints
	self._model:PivotTo(self._instance_paths[math.random(1, #self._instance_paths)].CFrame)
	
	return self
end

function MisprintTemplate.ChangeWalkSpeed(self: Misprint, walkspeed: number)
	local humanoid = self._model:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid.WalkSpeed = walkspeed
	end
end

function MisprintTemplate.GetNearestPlayer(self: Misprint)
	local character, dist = SimplePath.GetNearestCharacter(self._model:GetPivot().Position)

	if character and dist <= self._attack_distance and os.clock() - self._last_attack_tick >= self._hit_cooldown then
		self:Attack()
		self._last_attack_tick = os.clock()
	end

	if character and dist <= self._aggro_distance then
		self._path_trove:Destroy()

		-- Set states
		self._roaming = false
		self._on_aggro = true
		self._attacking = false

		-- Stop any current path
		if self._pathfinder.Status == SimplePath.StatusType.Active then
			self._pathfinder:Stop()
		end

		self._model:SetAttribute("State", GlobalStates.Chase)
		self:ChangeWalkSpeed(self._run_speed)
		self._pathfinder:Run(character.PrimaryPart)
	else
		-- No player close, go back to roaming
		if not self._roaming then
			self._on_aggro = false
			self._attacking = false

			if self._pathfinder.Status == SimplePath.StatusType.Active then
				self._pathfinder:Stop()
			end
		end
	end
end

function MisprintTemplate.Roam(self: Misprint)
	if self._roaming == false and self._on_aggro == false and self._attacking == false then
		self._roaming = true
		self._pathfinder:Run(self._instance_paths[math.random(1, #self._instance_paths)])

		self._model:SetAttribute("State", GlobalStates.Walk)
		self:ChangeWalkSpeed(self._default_walkspeed)
		
		local reached : RBXScriptSignal
		local errored : RBXScriptSignal
		local blocked : RBXScriptSignal
		
		reached = self._path_trove:Add(self._pathfinder.Reached:Connect(function()
			self._roaming = false
			self._path_trove:Destroy()
			--self._path_trove:Remove(reached)
		end))
		
		errored = self._path_trove:Add(self._pathfinder.Error:Connect(function()
			--self._path_trove:Remove(reached)
			--self._path_trove:Remove(errored)
			
			--if blocked then self._path_trove:Remove(blocked) end
			self._roaming = false
			self._path_trove:Destroy()
			
			print("ERROR!")
		end))
		
		blocked = self._path_trove:Add(self._pathfinder.Blocked:Connect(function()
			self._roaming = false
			self._path_trove:Destroy()
			--self._path_trove:Remove(reached)
			--self._path_trove:Remove(errored)
			--self._path_trove:Remove(blocked)
		end))
	end
end

function MisprintTemplate.Attack(self: Misprint)
	self._trove:AddPromise(Promise.new(function(resolve, reject, onCancel)
		local targets = {}
		for i = 1, self._loop_check do
			local target = Magnitude:GetTarget(self._model, self._hitbox_size, self._range, self._max_angle)
			if target and not table.find(targets, target) then 
				table.insert(targets, target) 
			end
			task.wait(self._hitbox_check_cooldown)
		end
		
		resolve(targets)
	end)):andThen(function(targets: {Model?})
		for _, target in targets do
			if not target then continue end
			target:SetAttribute("Hearts", target:GetAttribute("Hearts") - self._damage)
		end
	end)
end

function MisprintTemplate.Update(self: Misprint)
	if (os.clock() - self._lastTick) >= self._updateDelay then
		self._lastTick = os.clock()
		self:GetNearestPlayer()
		
		print(`Roaming: {self._roaming}, Attacking: {self._attacking}, Aggro: {self._on_aggro}`)

		-- Only roam if not chasing
		if not self._on_aggro then
			self:Roam()
		end
	end
end

function MisprintTemplate.Destroy(self: Misprint)
	self._trove:Destroy()
end

return MisprintTemplate

I think I see why your AI sometimes dosent roam.

From looking at your code the main issue seems to be how _path_trove is being destroyed inside the Reached, Error, and Blocked connections. Each time the pathfinder finishes, errors or gets blocked, you destroy _path_trove, which also disconnects all the signals, including the ones that are supposed to let the AI start roaming again. That can leave the AI stuck with _roaming still true or _pathfinder in an unexpected state, so Roam() never triggers again!!!


A better way is to only remove the specific connection that fired instead of destroying the entire trove. That way, the other connections and your roaming logic remain intact!!

in your Roam function, instead of this:

reached = self._path_trove:Add(self._pathfinder.Reached:Connect(function()
	self._roaming = false
	self._path_trove:Destroy()
end))

errored = self._path_trove:Add(self._pathfinder.Error:Connect(function()
	self._roaming = false
	self._path_trove:Destroy()
	print("ERROR!")
end))

blocked = self._path_trove:Add(self._pathfinder.Blocked:Connect(function()
	self._roaming = false
	self._path_trove:Destroy()
end))

You could do something like:

reached = self._path_trove:Add(self._pathfinder.Reached:Connect(function()
	self._roaming = false
	self._path_trove:Remove(reached)
end))

errored = self._path_trove:Add(self._pathfinder.Error:Connect(function()
	self._roaming = false
	self._path_trove:Remove(errored)
	print("Pathfinder error!")
end))

blocked = self._path_trove:Add(self._pathfinder.Blocked:Connect(function()
	self._roaming = false
	self._path_trove:Remove(blocked)
end))

This keeps the _path_trove alive for future roaming attempts and prevents your AI from getting “stuck” because all connections were destroyed!


Another point is your Roam check:

if self._roaming == false and self._on_aggro == false and self._attacking == false then

Make sure _attacking is always being reset properly after Attack()!! Right now _attacking never seems to be set back to false after an attack, so it could block roaming occasionally…


Its a good idea to log your _pathfinder.Status before running Roam() to see if its still marked as Active. Sometimes the pathfinder dosent reset properly and that prevents Roam() from starting a new path.

print("Pathfinder Status:", self._pathfinder.Status)

These small tweaks removing only the triggered connection, resetting _attacking and checking the pathfinder status should make roaming more reliable!!

I hope this helps and i hope the best for your Enemy AI!!!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.