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


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!