Zombie Pathfinding OOP Module doesn't work

I’m trying to get this zombie module to work with pathfinding

For some reason it won’t work. It attacks when I get close to it half the time, and whenever it does, it walks towards me for like half a second and stops, and nothing errors.

All the print statements I made (!!, ?, pathToTarget, LostTarget) All get printed in the output, so that’s just some info if u need it I guess

Here’s the TagHuman function in Tags module:

--Setting up Utility
local tagsUtility = {}


function tagsUtility.tagHuman(instance: Instance)

	local success, err = pcall(function()

		local human = instance:FindFirstChildWhichIsA("Humanoid")
		if human then
			human:AddTag("Human")
		end

	end)

end


return tagsUtility

Here’s the Zombie OOP module (Maybe check if the threadSpawn function is right):

Zombie Module

--Services
local CollectionService = game:GetService("CollectionService")
local PathfindingService = game:GetService("PathfindingService")


--Main module
local zombie = {}
zombie.__index = zombie


--Setting up zombie
function zombie.new(char: Model, health: number, damage: number, attackRange: number, detectRange: number, smoothness: number,position: CFrame)
	
	--Setting up Metatable
	setmetatable({}, zombie)
	
	--Setting up values and variables
	zombie.char = char
	zombie.root = char:FindFirstChild("HumanoidRootPart")
	zombie.humanoid = char:FindFirstChild("Humanoid") :: Humanoid
	zombie.head = char:FindFirstChild("Head")
	zombie.torso = char:FindFirstChild("Torso")
	
	zombie.animator = zombie.humanoid:FindFirstChild("Animator")
	zombie.grabAnim = zombie.animator:LoadAnimation(zombie.char:FindFirstChild("Grab"))
	
	zombie.target = nil
	
	--Properties
	zombie.health = health
	zombie.humanoid.MaxHealth = health
	zombie.humanoid.Health = health
	
	zombie.damage = damage
	zombie.attackRange = attackRange
	zombie.detectRange = detectRange
	zombie.smoothness = smoothness
	
	--Thread variables
	zombie.threads = {}
	
	
	zombie.__type = "Zombie"
	
	--Run code
	zombie.humanoid:AddTag("Zombie")
	zombie:position(position)
	
	
	return zombie
	
end


------------------------------------------------------------
--METAMETHODS


------------------------------------------------------------
--INITIALIZE ZOMBIE
function zombie:INITIALIZE()

	if self.__type ~= "Zombie" then return end


	--Initializing zombie
	self.threadSpawn(self, function()
		while task.wait(1) do
			if self then
				self:updateTarget()
			else
				return
			end
		end
	end)

	--Attack checking
	self.threadSpawn(self, function()

		self:attackChecking()

	end)
	
	self:initializePathfinding()
	
	
	return self

end

--------------------------------------------------
--MOVEMENT FUNCTIONS


--Move zombie to position instantly
function zombie:position(position: CFrame)
	
	if self.__type ~= "Zombie" then return end

	
	self.root:PivotTo(position)
	
	
	return self
	
end


--------------------------------------------
--PATHFINDING FUNCTIONS


--Check distance(NORMAL FUNCTION)
function zombie.checkDist(part1: Part | Vector3, part2: Part | Vector3): number
	if typeof(part1) ~= Vector3 then part1 = part1.Position end
	if typeof(part2) ~= Vector3 then part2 = part2.Position end

	return (part1 - part2).Magnitude
end


--Update target
function zombie:updateTarget()
	
	if self.__type ~= "Zombie" then return end
	
	--Variables
	local humans = CollectionService:GetTagged("Human")
	
	--Setting up zombie
	self.target = nil
	
	for _, human in pairs(humans) do
		local root = human.RootPart
		if (root) and (human.Health > 0) and (self.checkDist(root, self.root) < self.detectRange) and (not table.find(human:GetTags(), "Zombie")) then
			self.detectRange = self.checkDist(root, self.root)
			self.target = root
		end
	end
	
	
	return self
	
end


--Path to Target
function zombie:pathToTarget()
	
	if self.__type ~= "Zombie" then return end
	
	print("pathToTarget")
	
	--Creating path
	local path = PathfindingService:CreatePath()
	path:ComputeAsync(self.root.Position, self.target.Position)
	
	--Variables
	local waypoints = path:GetWaypoints()
	local currentTarget = self.target
	
	--Setting up pathfinding
	for i, waypoint in pairs(waypoints) do
		
		-----------------
		local part = Instance.new("Part")
		part.CanCollide = false
		part.Anchored = true
		part.Material = "Neon"
		part.BrickColor = BrickColor.new("Royal purple")
		part.Position = waypoint.Position
		part.Size = Vector3.new(1,1,1)
		
		-----------------
		
		--Creating waypoints and moving zombie there
		if waypoint.Action == Enum.PathWaypointAction.Jump then
			self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
		else
			self.humanoid:MoveTo(waypoint.Position)
			
			self.threadSpawn(self, function()
				task.wait(0.5)
				if self.humanoid.WalkToPoint.Y > self.root.Position.Y then
					self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
			end)
		end
		
		--Makes sure zombie walking is correct
		self.humanoid.MoveToFinished:Wait()
		
		print("!!")
		
		--Making sure tracking works
		if not self.target then
			print("Lost target")
			break
		elseif (self.checkDist(currentTarget, waypoints[#waypoints]) > self.smoothness) or currentTarget ~= self.target then
			print("?")
			self:pathToTarget()
			break
		end
		
	end
	
	
	return self
	
end


--Movement handler
function zombie:moveHandler()
	
	if self.__type ~= "Zombie" then return end
		
	--Setting up zombie movement
	while task.wait(1) do
		--Makes sure zombie is moving only if zombie is alive
		if self.humanoid.Health <= 0 then
			break
		end
		
		--Makes sure the zombie has a target
		if zombie.target then
			self:pathToTarget()
		end
	end
	
	
	return self	
	
end


--moveToEntrance
--[[
function zombie:moveToEntrance()
	
end
]]

--------------------------------------------
--Loops


--Initialize zombie attacks
function zombie:attackChecking()
	
	while task.wait(0.5) do

		if self then
			--Checking if zombie should attack
			if self.target then
				if self.checkDist(self.target, self.root)  < self.attackRange then
					self:attack(self.damage)
				end
			end
		else
			return
		end
	end
	
	
	return self

end


--------------------------------------------
--UTILITY FUNCTIONS


--Remove zombie
function zombie:removeZombie()

	if self.__type ~= "Zombie" then return end


	task.wait(5)
	
	--Close all threads
	for _, thread in pairs(self.threads) do
		coroutine.close(thread)
		thread = nil
	end
	
	self.char:Destroy()
	
	self = nil
	

	return self

end


--Attack
function zombie:attack()
	
	if self.__type ~= "Zombie" then return end
	
	--Attack code
	local human = self.target.Parent.Humanoid :: Humanoid
	human:TakeDamage(self.damage)
	
	--Attack Animation
	self.grabAnim:Play()
		
		
	return self
	
end


--Initialize pathfinding
function zombie:initializePathfinding()

	if self.__type ~= "Zombie" then return end

	--When a zombie is eliminated
	self.humanoid.Died:Connect(function()
		self:removeZombie()
	end)

	--Making sure server has full control of zombie
	for i, v in pairs(zombie.char:GetDescendants()) do
		if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
			v:SetNetworkOwner(nil)
		end
	end

	--Handle zombie movement
	self.threadSpawn(self, function()
		self:moveHandler()
	end)
	

	return self

end



--Spawn thread
function zombie.threadSpawn(zombieObject, functionObject, ...)
	
	if zombieObject.__type ~= "Zombie" then return end
	
	
	local co = coroutine.wrap(functionObject)
	
	table.insert(zombieObject.threads, co)
	
	co(...)
	
end


------------------------------------------------------------


return zombie

1 Like

In the: zombie:updateTarget() function, whenever I remove the line of code: self.target = nil, the zombie works until I die, constantly attacking in the same spot and breaking after a little while

Are you sure that ComputeAync is successfully finding a path? I see that you never check if the path was actually created. In the PathfindingService documentation there is some code that checks if it actually made a path. PathfindingService | Documentation - Roblox Creator Hub

For one of my projects if the zombie fails to find a path, I would just move in the direction of the player

image

-- Compute the path
local success, errorMessage = pcall(function()
	path:ComputeAsync(startPosition, finishPosition)
end)

-- Confirm the computation was successful
if success and path.Status == Enum.PathStatus.Success then
	-- For each waypoint, create a part to visualize the path
	for _, waypoint in path:GetWaypoints() do
		local part = Instance.new("Part")
		part.Position = waypoint.Position
		part.Size = Vector3.new(0.5, 0.5, 0.5)
		part.Color = Color3.new(1, 0, 1)
		part.Anchored = true
		part.CanCollide = false
		part.Parent = Workspace
	end
else
	print(`Path unable to be computed, error: {errorMessage}`)
end

Just noticed something here, you’re referencing the class zombie itself in your constructor function. This is not the right way to OOP as you aren’t creating your variables properly. Remember, setmetatable returns a metatable which we have to store in our variable called, self. Here’s the right way to do it.

I may have missed some details but there are already a lot of OOP material out there for you to study and read posts about.

function zombie.new(...)
	--Setting up Metatable
	local self = setmetatable({}, zombie)
	
	--Setting up values and variables
	self.char = char
	self.root = char:FindFirstChild("HumanoidRootPart")
	...
	return self
end
1 Like

It is a successful path, yet it only works for like a second, and then prints, “Target Lost”

Once I collide with the zombie or go near it, half the time it will start working again and attack me, until I walk away and it stops moving towards me after around a second, and If I collide with the zombie or go near it again, the same happens.

Here is the output of this:

pathToTarget
    !! (x5)
  pathToTarget
  !!
  Lost target
  pathToTarget
    !! (x3)
  pathToTarget
    !! (x2)
  pathToTarget
    !! (x2)
  Lost target

Also, I’m basically reformatting the code from a tutorial video by Y3llow Mustang titled “Roblox - Controlling Multiple AI with One Script (advanced) - Scripting Tutorial” into OOP form, with some changes to it, so hopefully that helps :happy2:

After a long time of trying to get the module to work in my studio, consider commenting out these two lines.

1 Like

Could you show how you created the object of a class in a different script, because it won’t work for me and maybe it’s to do with that.

Here’s the server script I’m using to create a zombie object of the zombie class. (I moved the self:INITIALIZE() function inside of the .new() function itself.)

Code:

local zombie = zombieClass.new(game.Workspace.Zombie, 100, 15, 10, 200, 10, CFrame.new(0,15,0))

Sorry, I can’t really help you because I can’t get the module working properly on my end (and I’ve spent a lot of time on this post) but I’d suggest trying to separate your module to two distinct module scripts.

  1. ZombieSpawnModule
  2. PathfindingModule

I see that there was two initialize functions in the single module. Though not inherently bad, having just one initialize function to kickstart the process will definitely help as there is one single point where you can show each step of the initialization process.

As for the pathfinding module, we can give it a .new constructor passing in our character for it to initialize its own pathfind system in the zombie module init like I have here in one of my projects.

function ZombieModule:Init(zombie: Model)
	local pathfind = Pathfinding.new(zombie)
	pathfind:Start()
	...

This might seem a little bit nit picky but please remove the comments! The function name should explicitly tell you what exactly the function does. Well that’s it for me, I hope I gave you some help though. :slight_smile:

1 Like