Roblox Pathfinding Monster Gets Stuck After Transforming

  1. What do you want to achieve? Keep it simple and clear!
    I would like to fix my pathfinding script to stop my monster for going back and forward

  2. What is the issue? Include screenshots / videos if possible!
    I want to fix my pathfinding monster, so basically the monster patrols and has waypoints that it can go to and it can also transform randomly at each waypoint into a random player in the server which all works well. But one problem, after it transforms to a player SOMETIMES (not always) the model will get stuck going back and forward and won’t go to a waypoint. Eventually though it will go to a waypoint but sometimes it just stops moving and its like the script is stuck on the walking to the waypoint because I put a print(“Hi”) in the while task.Wait(0.8) do and it doesnt print it which means the script is stuck in a function.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I have tried YT, DevForum and ChatGPT and Roblox’s Assistant.

Here is my script, it is a script in workspace and I have my monster model in workspace too named “Monster”, the script has a folder named “Animations” in it and it has animations inside of it like, walking and running. I reset the variables needed after transformation and there are no errors in the output when the glitch happens. Also ignore the “timesWalked” thing it was a variable that I made because the script would try and transform right when the game started because it created a new path, so i just used that to check if it has walked to a part before so it doesn’t transform right at the start.

-- Services --
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local JumpscareRE = ReplicatedStorage:WaitForChild("JumpscareRE")
local animations = script:WaitForChild("Animations")
local anim = animations:WaitForChild("Walk")

-- Changing Variables --
local rig = workspace:WaitForChild("Monster")
local hitBox = rig:WaitForChild("Hitbox")
local walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

-- Variables --
local deb = false
local isAttacking = false 
local isChasing = false
local cloned = false
local timesWalked = 0

-- Functions --
local patrol
local calculatePath
local walkToDestination
local CloneToPlayer
local CloneMe
local CloneToMonster
local Jumpscare
local checkForCharacter
local findNearestPlayer
local attack

Jumpscare = function(character)
	if cloned == true and isAttacking == false then
		CloneToMonster()
	end
	
	task.wait(0.1)
	
	local attackAnimation = rig.Humanoid.Animator:LoadAnimation(animations.Attack)
	attackAnimation:Play()
	
	isChasing = false
	isAttacking = true

	local player = game.Players:GetPlayerFromCharacter(character)
	local cameraPart = rig:WaitForChild("CameraPart")

	character:WaitForChild("Humanoid").WalkSpeed = 0
	character:WaitForChild("Humanoid").JumpHeight = 0

	character:FindFirstChild("Knocked").Value = true

	JumpscareRE:FireClient(player, cameraPart, true)
	rig:WaitForChild("HumanoidRootPart"):WaitForChild("Jumpscare"):Play()
	task.wait(4)
	JumpscareRE:FireClient(player, cameraPart, false)
	character:WaitForChild("Humanoid").WalkSpeed = 8
	attackAnimation:Stop(0)
	task.wait(5)
	isAttacking = false
	patrol()
end

checkForCharacter = function(character)
	local rayOrigin = rig:FindFirstChild("HumanoidRootPart").Position
	local rayDirection = (character.HumanoidRootPart.Position - rayOrigin).Unit * 40

	local raycastResult = workspace:Raycast(rayOrigin, rayDirection, RaycastParams.new())

	if raycastResult then
		local raycastInstance = raycastResult.Instance
		if raycastInstance:IsDescendantOf(character) then
			return true
		end
	else
		return false
	end
end

findNearestPlayer = function()
	local players = Players:GetPlayers()
	local nearestPlayer = nil
	local maxDistance = 100

	for _, player in pairs(players) do
		if player.Character ~= nil then
			local targetCharacter = player.Character
			local distance = (rig.HumanoidRootPart.Position - targetCharacter.HumanoidRootPart.Position).Magnitude

			if distance < maxDistance and checkForCharacter(targetCharacter) then
				if targetCharacter:FindFirstChild("Knocked").Value == false and targetCharacter:FindFirstChild("Humanoid").Health > 0 and targetCharacter:FindFirstChild("Hiding").Value == false then
					nearestPlayer = targetCharacter
					maxDistance = distance
				end
			end
		end
	end
	return nearestPlayer
end

attack = function(character)
	local distance = (rig.HumanoidRootPart.Position - character.HumanoidRootPart.Position).Magnitude

	if distance > 5 and character:FindFirstChild("Knocked").Value == false and character:FindFirstChild("Humanoid").Health > 0 and character:FindFirstChild("Hiding").Value == false then
		if walkAnimation.IsPlaying then
			walkAnimation:Stop()
		end
		rig.Humanoid:MoveTo(character.HumanoidRootPart.Position)
		isChasing = true
	else
		Jumpscare(character)
	end
end


calculatePath = function(destination)
	local agentParams = {
		["AgentHeight"] = hitBox.Size.Y,
		["AgentRadius"] = hitBox.Size.X,
		["AgentCanJump"] = false
	}

	local path = PathfindingService:CreatePath(agentParams)
	path:ComputeAsync(rig.HumanoidRootPart.Position, destination)
	
	if timesWalked ~= 2 then
		timesWalked += 1
	end
	
	if timesWalked >= 2 then
		local ranNumber = math.random(1,10)

		if ranNumber >= 5 then
			local Number = math.random(1,2)
			
			if Number == 1 then
				if cloned == true and isAttacking == false and isChasing == false then
					CloneToMonster()
				end
			else
				if cloned == false and isAttacking == false and isChasing == false then
					CloneToPlayer()
				end
			end
		end
	end
	
	return path
end

walkToDestination = function(destination)
	local path = calculatePath(destination)

	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in pairs(path:GetWaypoints()) do
			local nearestPlayer = findNearestPlayer()
			if nearestPlayer and not isAttacking then
				attack(nearestPlayer)
				break
			else
				rig.Humanoid:MoveTo(waypoint.Position)
				
				if isChasing == true then
					isChasing = false
				end
				
				rig.Humanoid.MoveToFinished:Wait()
			end
		end
	else
		rig.Humanoid:MoveTo(destination - (rig.HumanoidRootPart.CFrame.LookVector * 10))
	end
end

patrol = function()
	local waypoints = workspace.Waypoints:GetChildren()
	local randomNumber = math.random(1, #waypoints)

	if not walkAnimation.IsPlaying then
		walkAnimation:Play()
		walkAnimation.Looped = true
	end

	walkToDestination(waypoints[randomNumber].Position)
end

CloneMe = function(char)
	char.Archivable = true
	local clone = char:Clone()
	char.Archivable = false
	return clone
end

CloneToPlayer = function()
	local PlayersService = Players:GetPlayers()

	if #PlayersService >= 1 then
		local ChosenPlayer = PlayersService[math.random(1, #PlayersService)]

		if ChosenPlayer.Character then

			local charClone = CloneMe(ChosenPlayer.Character)
			charClone.Parent = workspace
			charClone.Name = "Monster"

			local startPosition = rig.PrimaryPart.Position
			charClone:SetPrimaryPartCFrame(CFrame.new(startPosition))

			charClone.Humanoid.WalkSpeed = 16
			rig.Parent = game.ServerStorage
			rig.WalkingSoundScript:Clone().Parent = charClone
			rig = charClone

			walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

			if charClone:FindFirstChild("Knocked") then
				charClone:FindFirstChild("Knocked"):Destroy()
			end
			if charClone:FindFirstChild("Hiding") then
				charClone:FindFirstChild("Hiding"):Destroy()
			end

			local cameraPartNew = Instance.new("Part")
			cameraPartNew.Transparency = 1
			cameraPartNew.CanCollide = false
			cameraPartNew.Size = Vector3.new(1, 1, 1)
			local newPosition = charClone:WaitForChild("Head").Position + (charClone:WaitForChild("Head").CFrame.LookVector * 3)
			cameraPartNew.Position = newPosition
			cameraPartNew.Name = "CameraPart"
			cameraPartNew.Parent = charClone
			cameraPartNew.Anchored = false
			cameraPartNew.Orientation = Vector3.new(0, 180, 0)

			local weld = Instance.new("WeldConstraint")
			weld.Parent = cameraPartNew
			weld.Part0 = cameraPartNew
			weld.Part1 = charClone:WaitForChild("Head")

			local hitboxNew = Instance.new("Part")
			hitboxNew.Size = charClone:GetExtentsSize()
			hitboxNew.Position = charClone.PrimaryPart.Position
			hitboxNew.Parent = charClone
			hitboxNew.Name = "HitBox"
			hitboxNew.Anchored = false
			hitboxNew.Transparency = 1
			hitboxNew.CanCollide = false
			hitBox = hitboxNew

			local weld2 = Instance.new("WeldConstraint")
			weld2.Parent = hitboxNew
			weld2.Part0 = hitboxNew
			weld2.Part1 = charClone.PrimaryPart

			script.Jumpscare:Clone().Parent = charClone.HumanoidRootPart
			script.WalkingSound:Clone().Parent = charClone.HumanoidRootPart

			cloned = true

			task.delay(1, function()
				patrol() 
			end)
		end
	end
end

CloneToMonster = function()
	local oldRig = game.ServerStorage:FindFirstChild("Monster")
	oldRig.Parent = workspace
	oldRig:MoveTo(rig.PrimaryPart.Position)
	rig:Destroy()
	rig = oldRig
	hitBox = oldRig:WaitForChild("Hitbox")

	walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

	cloned = false

	task.delay(1, function()
		patrol() 
	end)
end
while task.wait(0.8) do
	
	if isAttacking == false then
		patrol()
	end

	hitBox.Touched:Connect(function(hit)
		if hit.Parent:FindFirstChild("Humanoid") then
			local player = Players:GetPlayerFromCharacter(hit.Parent)
			local character = hit.Parent

			if player and deb == false and character:FindFirstChild("Knocked").Value == false and character:FindFirstChild("Humanoid").Health > 0 and character:FindFirstChild("Hiding").Value == false and isAttacking == false then
				deb = true
				Jumpscare(character)
				task.wait(1)
				deb = false
			end
		end
	end)
end

I am NOT asking for free scripts, just a little pointer into which direction I should go because I know I need to do something when it calculates the path or walks to it if it has transformed but I can’t figure out what it is I need to change, Thanks!

3 Likes

If the size of the monster changes significantly when it transforms, then remember to recalculate the path in-order to adjust the AgentHeight and AgentRadius according to the monster’s new dimensions. You’ll also need to wait until the monster has finished transforming before doing so (unless the transformation is instantaneous) in-order for the dimensions to be correct

2 Likes

it sets the hitbox variable to the new hitbox when it transforms and then patrols which re calculates everything

1 Like

It could be that the issue is being caused by not checking whether the monster is already in partol before calling the patrol function, so doing something like this should fix the issue:

-- Services --
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local JumpscareRE = ReplicatedStorage:WaitForChild("JumpscareRE")
local animations = script:WaitForChild("Animations")
local anim = animations:WaitForChild("Walk")

-- Changing Variables --
local rig = workspace:WaitForChild("Monster")
local hitBox = rig:WaitForChild("Hitbox")
local walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

-- Variables --
local deb = false
local isAttacking = false 
local isChasing = false
local isPatrolling = false -- new variable for the patrol function debounce
local cloned = false
local timesWalked = 0

-- Functions --
local patrol
local calculatePath
local walkToDestination
local CloneToPlayer
local CloneMe
local CloneToMonster
local Jumpscare
local checkForCharacter
local findNearestPlayer
local attack

Jumpscare = function(character)
	if cloned == true and isAttacking == false then
		CloneToMonster()
	end
	
	task.wait(0.1)
	
	local attackAnimation = rig.Humanoid.Animator:LoadAnimation(animations.Attack)
	attackAnimation:Play()
	
	isChasing = false
	isAttacking = true

	local player = game.Players:GetPlayerFromCharacter(character)
	local cameraPart = rig:WaitForChild("CameraPart")

	character:WaitForChild("Humanoid").WalkSpeed = 0
	character:WaitForChild("Humanoid").JumpHeight = 0

	character:FindFirstChild("Knocked").Value = true

	JumpscareRE:FireClient(player, cameraPart, true)
	rig:WaitForChild("HumanoidRootPart"):WaitForChild("Jumpscare"):Play()
	task.wait(4)
	JumpscareRE:FireClient(player, cameraPart, false)
	character:WaitForChild("Humanoid").WalkSpeed = 8
	attackAnimation:Stop(0)
	task.wait(5)
	isAttacking = false
	patrol()
end

checkForCharacter = function(character)
	local rayOrigin = rig:FindFirstChild("HumanoidRootPart").Position
	local rayDirection = (character.HumanoidRootPart.Position - rayOrigin).Unit * 40

	local raycastResult = workspace:Raycast(rayOrigin, rayDirection, RaycastParams.new())

	if raycastResult then
		local raycastInstance = raycastResult.Instance
		if raycastInstance:IsDescendantOf(character) then
			return true
		end
	else
		return false
	end
end

findNearestPlayer = function()
	local players = Players:GetPlayers()
	local nearestPlayer = nil
	local maxDistance = 100

	for _, player in pairs(players) do
		if player.Character ~= nil then
			local targetCharacter = player.Character
			local distance = (rig.HumanoidRootPart.Position - targetCharacter.HumanoidRootPart.Position).Magnitude

			if distance < maxDistance and checkForCharacter(targetCharacter) then
				if targetCharacter:FindFirstChild("Knocked").Value == false and targetCharacter:FindFirstChild("Humanoid").Health > 0 and targetCharacter:FindFirstChild("Hiding").Value == false then
					nearestPlayer = targetCharacter
					maxDistance = distance
				end
			end
		end
	end
	return nearestPlayer
end

attack = function(character)
	local distance = (rig.HumanoidRootPart.Position - character.HumanoidRootPart.Position).Magnitude

	if distance > 5 and character:FindFirstChild("Knocked").Value == false and character:FindFirstChild("Humanoid").Health > 0 and character:FindFirstChild("Hiding").Value == false then
		if walkAnimation.IsPlaying then
			walkAnimation:Stop()
		end
		rig.Humanoid:MoveTo(character.HumanoidRootPart.Position)
		isChasing = true
		isPatrolling = false -- The monster has found a player, so it's no longer patrolling
	else
		Jumpscare(character)
	end
end


calculatePath = function(destination)
	local agentParams = {
		["AgentHeight"] = hitBox.Size.Y,
		["AgentRadius"] = hitBox.Size.X,
		["AgentCanJump"] = false
	}

	local path = PathfindingService:CreatePath(agentParams)
	path:ComputeAsync(rig.HumanoidRootPart.Position, destination)
	
	if timesWalked ~= 2 then
		timesWalked += 1
	end
	
	if timesWalked >= 2 then
		local ranNumber = math.random(1,10)

		if ranNumber >= 5 then
			local Number = math.random(1,2)
			
			if Number == 1 then
				if cloned == true and isAttacking == false and isChasing == false then
					CloneToMonster()
				end
			else
				if cloned == false and isAttacking == false and isChasing == false then
					CloneToPlayer()
				end
			end
		end
	end
	
	return path
end

walkToDestination = function(destination)
	local path = calculatePath(destination)

	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in pairs(path:GetWaypoints()) do
			local nearestPlayer = findNearestPlayer()
			if nearestPlayer and not isAttacking then
				attack(nearestPlayer)
				break
			else
				rig.Humanoid:MoveTo(waypoint.Position)
				
				if isChasing == true then
					isChasing = false
				end
				
				rig.Humanoid.MoveToFinished:Wait()
			end
		end
	else
		rig.Humanoid:MoveTo(destination - (rig.HumanoidRootPart.CFrame.LookVector * 10))
	end
end

patrol = function()
	local waypoints = workspace.Waypoints:GetChildren()
	local randomNumber = math.random(1, #waypoints)

	if not walkAnimation.IsPlaying then
		walkAnimation:Play()
		walkAnimation.Looped = true
	end

	walkToDestination(waypoints[randomNumber].Position)
end

CloneMe = function(char)
	char.Archivable = true
	local clone = char:Clone()
	char.Archivable = false
	return clone
end

CloneToPlayer = function()
	local PlayersService = Players:GetPlayers()

	if #PlayersService >= 1 then
		local ChosenPlayer = PlayersService[math.random(1, #PlayersService)]

		if ChosenPlayer.Character then

			local charClone = CloneMe(ChosenPlayer.Character)
			charClone.Parent = workspace
			charClone.Name = "Monster"

			local startPosition = rig.PrimaryPart.Position
			charClone:SetPrimaryPartCFrame(CFrame.new(startPosition))

			charClone.Humanoid.WalkSpeed = 16
			rig.Parent = game.ServerStorage
			rig.WalkingSoundScript:Clone().Parent = charClone
			rig = charClone

			walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

			if charClone:FindFirstChild("Knocked") then
				charClone:FindFirstChild("Knocked"):Destroy()
			end
			if charClone:FindFirstChild("Hiding") then
				charClone:FindFirstChild("Hiding"):Destroy()
			end

			local cameraPartNew = Instance.new("Part")
			cameraPartNew.Transparency = 1
			cameraPartNew.CanCollide = false
			cameraPartNew.Size = Vector3.new(1, 1, 1)
			local newPosition = charClone:WaitForChild("Head").Position + (charClone:WaitForChild("Head").CFrame.LookVector * 3)
			cameraPartNew.Position = newPosition
			cameraPartNew.Name = "CameraPart"
			cameraPartNew.Parent = charClone
			cameraPartNew.Anchored = false
			cameraPartNew.Orientation = Vector3.new(0, 180, 0)

			local weld = Instance.new("WeldConstraint")
			weld.Parent = cameraPartNew
			weld.Part0 = cameraPartNew
			weld.Part1 = charClone:WaitForChild("Head")

			local hitboxNew = Instance.new("Part")
			hitboxNew.Size = charClone:GetExtentsSize()
			hitboxNew.Position = charClone.PrimaryPart.Position
			hitboxNew.Parent = charClone
			hitboxNew.Name = "HitBox"
			hitboxNew.Anchored = false
			hitboxNew.Transparency = 1
			hitboxNew.CanCollide = false
			hitBox = hitboxNew

			local weld2 = Instance.new("WeldConstraint")
			weld2.Parent = hitboxNew
			weld2.Part0 = hitboxNew
			weld2.Part1 = charClone.PrimaryPart

			script.Jumpscare:Clone().Parent = charClone.HumanoidRootPart
			script.WalkingSound:Clone().Parent = charClone.HumanoidRootPart

			cloned = true

			task.delay(1, function()
				patrol() 
			end)
		end
	end
end

CloneToMonster = function()
	local oldRig = game.ServerStorage:FindFirstChild("Monster")
	oldRig.Parent = workspace
	oldRig:MoveTo(rig.PrimaryPart.Position)
	rig:Destroy()
	rig = oldRig
	hitBox = oldRig:WaitForChild("Hitbox")

	walkAnimation = rig:WaitForChild("Humanoid").Animator:LoadAnimation(anim)

	cloned = false

	task.delay(1, function()
		patrol() 
	end)
end
while task.wait(0.8) do
	
	if isAttacking == false and isPatrolling == false then -- Check whether the monster is already patrolling
		isPatrolling = true
		patrol()
	end

	hitBox.Touched:Connect(function(hit)
		if hit.Parent:FindFirstChild("Humanoid") then
			local player = Players:GetPlayerFromCharacter(hit.Parent)
			local character = hit.Parent

			if player and deb == false and character:FindFirstChild("Knocked").Value == false and character:FindFirstChild("Humanoid").Health > 0 and character:FindFirstChild("Hiding").Value == false and isAttacking == false then
				deb = true
				Jumpscare(character)
				task.wait(1)
				deb = false
			end
		end
	end)
end
2 Likes

the monster now just walks back and forward, it never reaches a waypoint

1 Like

The only test left that I can think of is to try checking to see if one of the if statements is being triggered incorrectly, and to make sure that the functions are being called in the correct order

I suspect that something is calling the patrol function more frequently than it should, which is why the monster is walking back and forth

2 Likes

i checked using print, patrol stops getting called. The code gets stuck somewhere in the walkToDestination and calculatePath

2 Likes

Try replacing this (in the calculatePath function):

with:
path:ComputeAsync(hitBox.Position, destination)

My reasoning is that if the hit-box is following the monster and the hit-box’s size is changing, then it makes sense to use its position as the path’s starting point

2 Likes

didnt work, but thanks for helping. I decided to not let the monster have the ability to transform

2 Likes

I can’t believe it took me this long to notice, but you have a memory leak here:

You’re creating the hit-box’s Touched connection within the while loop, and it’s almost never a good idea to create connections within an infinite loop unless you’re extremely careful about disconnecting them

I don’t think it’s what caused the original issue though, I just wanted to mention this since creating connections inside of infinite loops is a bad habit to have :slight_smile::+1:


@RetroAmythest

I had the time to attempt to rewrite the script (I also did it because I like the concept of a monster that’s able to stay hidden by disguising itself as a player), although admittedly I haven’t added all of the functionality of the original. It’s more of a reference you can use if you wish to return to the original concept in the future

The new script (I've minimized it so that this comment isn't too long)
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")

local patrolWaypoints = Workspace:WaitForChild("Waypoints")


local currentPatrolWaypoint = nil
local thread1 = nil
local thread2 = nil


local function getNearestPlayer(rigPosition: Vector3): Player?
	local nearestPlayer = nil
	local previousDistance = 128 -- Change this to match the desired detection radius (in studs)

	for _, player in Players:GetPlayers() do
		if player.Character then
			local distance = player:DistanceFromCharacter(rigPosition)

			if distance < previousDistance then
				nearestPlayer = player
				previousDistance = distance
			end
		end
	end

	return nearestPlayer
end

-- This function will guarantee that a new random waypoint is returned for patrolling
local function getRandomPatrolWaypoint(): BasePart
	local waypoints = patrolWaypoints:GetChildren()
	local waypoint = waypoints[math.random(#waypoints)]

	if waypoint == currentPatrolWaypoint then
		getRandomPatrolWaypoint()
	else
		currentPatrolWaypoint = waypoint
		return waypoint
	end
end

local function walkToPosition(humanoid: Humanoid, path: Path, rigPosition: Vector3, target: Vector3)
	path:ComputeAsync(rigPosition, target)

	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in path:GetWaypoints() do
			humanoid:MoveTo(waypoint.Position)
			humanoid.MoveToFinished:Wait()
		end
	else
		warn("Failed to compute path")
	end
end

local function rigStart(humanoid: Humanoid, path: Path, rigPosition: Vector3)
	while true do
		task.wait(0.8)

		if thread2 then -- Using threads will prevent any issues with overlapping movements
			task.cancel(thread2)
		end

		local player = getNearestPlayer(rigPosition)

		thread2 = task.spawn(
			walkToPosition,
			humanoid,
			path,
			rigPosition,
			if player then player.Character:GetPivot().Position else getRandomPatrolWaypoint())
	end
end

local function transform(into: Model)
	if thread1 then
		task.cancel(thread1)
	end

	local hitBox = into:WaitForChild("HitBox")
	local humanoid = into:WaitForChild("Humanoid")

	local path = PathfindingService:CreatePath({
		AgentHeight = hitBox.Size.Y,
		AgentRadius = hitBox.Size.X,
		AgentCanJump = false})

	thread1 = task.spawn(rigStart, humanoid, path, into:GetPivot().Position)
end

transform(Workspace:WaitForChild("Monster"))
2 Likes

its not the original issue, also i changed it after i posted this because i forgot i put it in there