AI randomly stops moving completely

Problem:
I coded a AI pet system so that players could physically go out and tame Pets, kind of like Minecrafts wild animals in a sense. The issue is, randomly some pets will stop moving and never continue their path, but other pets will be unaffected. So some will stop, and others will continue moving. These halted pets will eventually be cleaned up via a Lifespan function, however I would like to fix this issue as the Lifespan function doesn’t clean them up for a LONG TIME.

Pictures

image
image

Code: (Pastebin if you prefer: here)

local ps = game:GetService("PathfindingService")
local ts = game:GetService("TweenService")
local debris = game:GetService("Debris")
local wildPetsFolder = workspace.Interactive.WildPets
	local wildPets = wildPetsFolder.Pets
	local regions = wildPetsFolder.Regions
local petStorage = game.ServerStorage.Assets.Pets
local qFuncs = require(game.ServerStorage.Modules.quickFunctions) -- Module of shortened functions
local petInfo = require(game.ReplicatedStorage.Modules.PetInfo)
local weightedRoll = require(script.Parent.WeightedChance)
local pets = {}

--// Debug function, creates a physical path
function showPath(waypoints, pet)
	if not workspace:FindFirstChild('pathStorage') then --// If nowhere to store the path, create one
		local ps = Instance.new("Folder")
		ps.Name = 'pathStorage'
		ps.Parent = workspace
	end
	local ss = Instance.new('ObjectValue')
	ss.Name = pet.main.Parent.Name
	ss.Value = pet.main
	ss.Parent = workspace.pathStorage
	--// Create path
	for _, waypoint in pairs(waypoints) do
		local part = Instance.new("Part")
		table.insert(pet.path, part) --// Inserts path into the pets dictionary to be deleted later
		part.Material = "Neon"
		part.BrickColor = BrickColor.Blue()
		part.Size = Vector3.new(0.6, 0.6, 0.6)
		part.Position = waypoint.Position
		part.Transparency = 0
		part.Anchored = true
		part.CanCollide = false
		part.Parent = ss
	end
end

--// Handles the deletion of the physical path made by PathfindingService
function deletePath(pet)
	if not workspace:FindFirstChild('pathStorage') then return end
	local foundStorage = nil
	for _, v in pairs (workspace.pathStorage:GetChildren()) do
		if v.Value == pet.main then
			foundStorage = v
		end
	end
	if foundStorage then
		foundStorage:Destroy()
	end
	pet.path = {}
end

function petsInteractingWithPlayer(plr)
	for _, pet in pairs (pets) do
		if pet.interactingWith == plr then
			return true
		end
	end
	return false
end

--// Grabs a new random target from the pets region and makes it go there
function getRandomTarget(pet)
--	deletePath(pet)
	spawn (function()
		local points = pet.region:GetChildren()
		local p = points[math.random(1, #points)]
		if p and p ~= pet.target then
			pet.target = p
			local path = ps:CreatePath()
			path:ComputeAsync(pet.main.Position, pet.target.Position)
			local waypoints = path:GetWaypoints()
			--showPath(waypoints, pet) --// Showing for debug purposes
			wait()
			local sound = pet.main:FindFirstChild('Bloop')
			local side = 'left'
			for _, point in ipairs (waypoints) do
				local nearPlr = nearestPlr(pet)
				if nearPlr and nearPlr.Character and nearPlr.Character:FindFirstChild('HumanoidRootPart') and pet.interactingWith == nil and not petsInteractingWithPlayer(nearPlr) and qFuncs.getMag(pet.main, nearPlr.Character.HumanoidRootPart) <= 10 then
					pet.interactingWith = nearPlr
					repeat wait(0.1)
						if nearPlr.Character and nearPlr.Character:FindFirstChild('HumanoidRootPart') then
							pet.main.CFrame = CFrame.new(pet.main.Position, nearPlr.Character.HumanoidRootPart.Position)
						end
					until pet.main == nil or not nearPlr.Character or not nearPlr.Character:FindFirstChild('HumanoidRootPart') or pet == nil or pet.interactingWith == nil or (pet.interactingWith and qFuncs.getMag(pet.main, pet.interactingWith.Character.HumanoidRootPart) > 10)
					if pet.main == nil or pet == nil then return end -- pet is no longer with us
					pet.interactingWith = nil
				end
				--// Face pet towards walkPoint
				local forwardVector = (point.Position - pet.main.Position).Unit
			    local upVector = Vector3.new(0, 1, 0)
			    local rightVector = forwardVector:Cross(upVector)
				local upVector2 = rightVector:Cross(forwardVector)
				local angle = side == 'left' and CFrame.Angles(0, 0, math.rad(20)) or CFrame.Angles(0, 0, math.rad(-25)) -- left/right angular movement
				local up = side == 'left' and Vector3.new(0, 0.75, 0) or Vector3.new(0, 1.25, 0) -- up/down bob
				--// Move pet
				local t = ts:Create(pet.main, TweenInfo.new(0.75), {CFrame = CFrame.fromMatrix(point.Position, rightVector, upVector) * angle + up})
				side = side == 'left' and 'right' or 'left'
				t:Play()
				wait(0.5)
				if sound then
					sound:Play()
				end
			end
		elseif p == pet.target or not p then
			getRandomTarget(pet)
		end
	end)
end

--// Finds the nearest player to the pet
function nearestPlr(pet)
	local dist, player = 1000, nil
	for _, plr in pairs (game.Players:GetPlayers()) do
		repeat wait() until plr.Character
		if plr.Character:FindFirstChild('HumanoidRootPart') then
			local mag = qFuncs.getMag(pet.main, plr.Character.HumanoidRootPart)
			if mag < dist then
				dist = mag
				player = plr
			end
		end
	end
	return player
end

--// Determines whether or not to path to a new target, or stop to talk with the player
function updateTarget()
	for _, pet in pairs (pets) do
		if pet.target then
			if (pet.lastPos - pet.main.Position).Magnitude <= 2 then
				pet.stuckTimes = pet.stuckTimes + 1
			else
				pet.stuckTimes = 0
			end
			if pet.stuckTimes >= 20 then
				--print (pet.stuckTimes, pet.main.Parent.Name)
				--getRandomTarget(pet)
				--pet.stuckTimes = 0
			end
			pet.lastPos = pet.main.Position
			--print (qFuncs.getMag(pet.main, pet.target))
			if qFuncs.getMag(pet.main, pet.target) <= 5 then --// Pet is almost on top of its original target, lets change it
				getRandomTarget(pet)
			end
		else --// They dont have a target
			getRandomTarget(pet)
		end
	end
end

--// Check pets lifespans
function checkLifeSpans()
	for i, pet in pairs (pets) do
		if pet.timeAlive < pet.lifeSpan then
			pet.timeAlive = pet.timeAlive + 1
		else
			local region = pet.region
			if pet.main and pet.main.Parent then
				pet.main.Parent:Destroy()
			end
			table.remove(pets, i)
			createPet(region)
		end
	end
end

--// Constantly updates the pathing
spawn(function()
	while wait(1) do
		checkLifeSpans()
		wait()
		updateTarget()
	end
end)

--// Creates all the pets for all the regions
function init()
	for _, region in pairs (regions:GetChildren()) do
		local lim = 10
		if region.Name == 'Region1' then
			lim = 20
		else
			lim = 8
		end
		for i = 1, lim do
			createPet(region)
		end
	end
end

--// Create the pet
function getLifeSpan(tier)
	if not tier or tier == 'Common' then
		return 900
	elseif tier and tier == 'Uncommon' then
		return 1200
	elseif tier and tier == 'Rare' then
		return 1500
	elseif tier and tier == 'Legendary' then
		return 1800
	elseif tier and tier == 'Exotic' then
		return 2100
	else
		return 900
	end
end 

function getPet()
	local roll = weightedRoll.roll({Common = 47, Uncommon = 30, Rare = 15, Legendary = 7, Exotic = 1})
	local choices = {}
	for name, data in pairs (petInfo) do
		if data.Tier == roll then
			table.insert(choices, name)
		end
	end
	if roll == 'Exotic' then
		game.ReplicatedStorage.Remotes.notifyPlayer:FireAllClients('attention', "An Exotic pet has spawned!")
	end
	local pet = petStorage:GetChildren()
	pet = petStorage[choices[math.random(1, #choices)]]:Clone()
	return pet
end

function createPet(r)
	local pet = getPet() --// Creates a random pet object
	if pet then
		pet.PrimaryPart.Anchored = true
		--// Give it a random spawn point within its predetermined region
		local points = r:GetChildren()
		local sp = points[math.random(1, #points)]
		if sp then
			pet.Main.CFrame = sp.CFrame + Vector3.new(0, 0.5, 0)
		end
		--// Create a dictionary to keep track of it
		table.insert(pets, {
			main = pet.Main;
			region = r;
			target = nil;
			interactingWith = nil;
			path = {};
			timeAlive = 0;
			lifeSpan = getLifeSpan(petInfo[pet.Name].Tier);
			lastPos = pet.Main.Position;
			stuckTimes = 0;
		})
		--// Sets the network ownership to server so it won't be laggy when moving, also makes the parts collidable (so they don't go through objects)
		for _, v in pairs (pet:GetDescendants()) do
			if v:IsA("BasePart") then
				if v:CanSetNetworkOwnership() then
					v:SetNetworkOwner(nil)
				end
				if v.Name == 'Main' and v == pet.PrimaryPart then
					v.CanCollide = true
				else
					v.CanCollide = false
				end
			end
		end
		
		local s = game.ServerStorage.Assets.Misc.Bloop:Clone()
		s.Parent = pet.Main
		
		local gui = game.ServerStorage.Assets.Misc.PetName:Clone()
		gui.PN.Text = pet.Name
		gui.Parent = pet.Main
		
		if petInfo[pet.Name].Tier == 'Exotic' then
			gui.PN.TextColor3 = Color3.fromRGB(255, 213, 0)
		elseif petInfo[pet.Name].Tier == 'Legendary' then
			gui.PN.TextColor3 = Color3.fromRGB(255, 0, 0)
		end
		
		pet.Parent = wildPets
	end
end

--// Destroys the pet because we taimed it
function findPet(pet)
	for i, data in pairs (pets) do
		if data.main and pet:FindFirstChild('Main') and data.main == pet.Main then
			return data, i
		end
	end	
	return nil
end

game.ServerStorage.petTamed.Event:Connect(function(player, pet)
	local data, i = findPet(pet)
	if data then
		local region = data.region
		local effect = game.ServerStorage.Assets.Misc.Hearts:Clone()
		effect.Parent = pet.PrimaryPart
		-- Could do a nice little animation here??
		local t = ts:Create(data.main, TweenInfo.new(0.2, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, 1, true, 0.3), {CFrame = data.main.CFrame * CFrame.Angles(math.rad(45), 0, 0) + Vector3.new(0, 3, 0) })
		t:Play()
		t.Completed:Wait()
		game.ReplicatedStorage.Remotes.notifyPlayer:FireClient(player, 'tamedPet')
		if data.main and data.main.Parent then
			data.main.Parent:Destroy()
		end
		table.remove(pets, i)
		-- Create replacement pet
		createPet(region)
	end
end)

init() --// Initialize the script

We’ve tried hopping servers to see if we can figure it out ourselves, but it only seems to happen during servers. The pets hierarcy has only the Main/PrimaryPart anchored, and collidable everything else is welded to the PrimaryPart and is unanchored, and non-collidable.

In the updateTarget function, I tried detecting when pets where stuck so I could delete them on their next update, but I ran into issues with that because sometimes PathfindingService made really small paths that had points 0.5 studs away from each other and it would mistake moving pets.

I know I haven’t given a lot to go off of, and have kind of just thrown code in here, but I cannot figure out the issue. If you have any questions, or possible answers please leave 'em down below!
Thanks for reading this far!

3 Likes

Any error codes thrown out somewhere?

I’ve had similar issues with my own AI, and man is it frustrating. To be frank, I don’t think I could ever figure out what’s wrong with your code unless I wrote it myself. One of these days you’ll probably randomly wake up and realize it was something really simple.

So in my case it was related to what happens when a player leaves the server. For some reason Roblox can still act like a player “exists” even long after the player has left the server. I believe the same is also going to happen if you’re tracking any player characters in a variable. As a result I had to have some way for the game to make sure this set player value actually exists via a simple:

if game.Players:FindFirstChild(player) ~= nil then

I can’t quite understand your code, so what is pet.target exactly? Is it just a Vector3 or is it a value that holds a player? I’m going to go ahead and guess that the issue is almost certainly related to that value or the pet.interactingWith value somehow. If it is connected to players at all, then maybe this weird bug with persistent non-existent players/characters has something to do with it?

Dunno if I’m helping at all, just figured I’d add in something since I’ve encountered the same issue.

So what you wanna do is use this feature, it’s very helpful and can assist your npc making it more AI like.

1 Like

You actually did bring up quite a interesting theory. Robloxs garabagecollection won’t clean up the player object if its attached to variables I think, correct me if I’m wrong. That very well could be the issue, because when we were trying to replicate the issue, no one left the server and pets were working fine. In an actual server, players are leaving all the time so it makes sense.

I’m throwing their information into a dictionary to save references to their PrimaryPart, Lifespan, etc.
pet.target is the endGoal they want to reach. They don’t move towards a player, but a predetermined point.

And no, there aren’t any errors nor warnings.

I am using Pathfinding Service. These models don’t have Humanoids in them, I’m using TweenService to move them.

I can definitely say you are correct, since I’ve had first hand experience with it. If it wasn’t such a hassle I’d try to replicate it and prove it on video. I’d recommend starting up a server in studio and making your test characters leave in the middle of a point where the AI would have been interacting with that player/players in any way. Do it multiple times too, because Roblox isn’t consistent and this doesn’t occur every time. Slap in some print functions throughout your script, and theoretically there’ll be a point where the AI stops printing in a normal pattern when a player left.

I can’t really say if this garbagecollection actually affects your script or not at a glance though, since your code is a bit confusing for an outsider even with the information you gave me. :stuck_out_tongue: I guess it’ll be up to you to figure out if that could be it, because I certainly don’t have a solution for your case.

Honestly it looks like a total mystery if it’s not that. Maybe some really smart person would grace us with a visit in this section of the dev forum eventually.

Well I’ve noticed that they’ll randomly stop, even if a player wasn’t interacting with them. I understand that it can be super hard to try and read someone else’s code, thats why I originally brushed off the idea of posting to the forum, but now I’m desperate lol.

Even though I haven’t seen them break because a player was interacting with them, I do however think it might have something to do with a player, maybe that player interaction block of code is misreading or something.

I just replicated the issue in Studio, had one of the players leave and one of the pets didn’t like that and stopped moving. Oddly enough, it wasn’t the one he was interacting with, however now I know the for sure reason.

Woo! Progress!

At least you finally have a replicable reason for the issue occurring! :smile: Now you just need to figure out how in the world to fix that.

If I had to take a guess now knowing that, I would almost think that the AI seizes up due to the

repeat wait() until plr.Character 

within the nearestPlr() function. Maybe it occurs because the AI spends the rest of eternity trying to find the character of a player that no longer exists?

In which case just remove that line since you wouldn’t need to bother with a player whose character doesn’t exist, and instead just make it

function nearestPlr(pet)
    local dist, player = 1000, nil
    for _, plr in pairs (game.Players:GetPlayers()) do
	    if plr and game.Players:FindFirstChild(plr.Name) ~= nil then --this may be redundant, but better safe than sorry, right?
            if plr.Character ~= nil then
	             if plr.Character:FindFirstChild('HumanoidRootPart') then
	    	        local mag = qFuncs.getMag(pet.main, plr.Character.HumanoidRootPart)
		            if mag < dist then
		     	        dist = mag
		            	player = plr
		            end
	            end
            end
        end
    end
    return player
end

That’s about the best guess and best fix I could make towards that guess.

Haha, just as I found the issue, you found it as well :stuck_out_tongue: I was gonna just mark yours as a solution since it was the closest thing, but it seems you found it too. Thanks for your help man, seems like I just needed an extra set of eyes who has seen a similar issue.

This is how I changed it:

function nearestPlr(pet)
	local dist, player = 1000, nil
	for _, plr in pairs (game.Players:GetPlayers()) do
		if plr and plr.Character and plr.Character:FindFirstChild('HumanoidRootPart') then
			local mag = qFuncs.getMag(pet.main, plr.Character.HumanoidRootPart)
			if mag < dist then
				dist = mag
				player = plr
			end
		end
	end
	return player
end
1 Like