Need Help Deciding Which Enemy "Pathfinding" Method Is Better

Hello, thanks for taking the time to read my post.

I am creating a system in which enemies spawned in will automatically chase the player. When originally making this, I created pretty simple way to achieve the intended functionality, but soon started having doubts about it.

So I decided to attempt another method, and as I worked on it, I couldn’t help but feel stupid. It worked, but it definitely seems more performance-intensive and I was struggling to understand why I should not just stick with the first idea. Both can be improved upon, but do currently work. Before I continue this work, I would just like some opinions about which idea i should focus more on in the long-term.

The first idea I had was incredibly simple and done completely on the Server. The following function was called on an enemy model whenever it was cloned into the workspace:

function NPCFollowModule.FollowPlayer(npcHumanoid, player)
	local humanoidAlive		= true
	local testDB			= false

	local character			= player.Character
	local humanoid			= character:FindFirstChild("Humanoid")
	local rootPart			= character:FindFirstChild("HumanoidRootPart")
	
	if humanoid then
		npcHumanoid:MoveTo(rootPart.Position)
	end
	
	task.spawn(function()
		while humanoidAlive do
			task.wait(.25)
			
			npcHumanoid:MoveTo(rootPart.Position)
		end
		--warn("Loop is broken")
	end)
	
	
	
	npcHumanoid.Touched:Connect(function(hitPart)
		if not testDB then
			testDB = not testDB
			if hitPart.Parent.Name == player.Name then
				print("Damage time")
			end
			task.wait(1)
			testDB = not testDB
		end
	end)
	
	npcHumanoid.Died:Connect(function()
		--warn("NPC humanoid has died! Loop should break!")
		humanoidAlive = not humanoidAlive
	end)
end

As stated earlier, it is pretty simple. I just started having doubts about it’s scalability specifically.

So then I thought of another method that goes as follows.

Part is created on client and then bound to RunService that follows in front of the player.

-- client side
local newRegion3					= Region3Module.CreateNewRegion3(player)

	local newPart						= Instance.new("Part")
	newPart.CanCollide					= false
	newPart.Anchored					= true
	newPart.Transparency				= .5
	newPart.Size						= Vector3.new(math.abs(newRegion3.Size.X), math.abs(newRegion3.Size.Y), math.abs(newRegion3.Size.Z))
	newPart.CFrame						= newRegion3.CFrame
	newPart.Parent						= workspace

Coroutine is created (that runs until player death or round is finished) that constantly sends this part’s information into a ModuleScript that sends the part properties to the Server. (Yes, I am aware that the Module function is not strictly necessary and I could just add the code into the LocalScript).

-- Client:
-- Create a coroutine that will run the EnemyDectection logic every 1 second
	local enemyDetectionCoroutine		= coroutine.create(function()
		while true do
			EnemyDetectionModule.DetectEnemiesWithinPart(newPart, character, humanoid)
			task.wait(.5)
		end
	end)

-- Coroutine is resumed further down in the script

ModuleScript that uses the information:

function EnemyDetectionModule.DetectEnemiesWithinPart(partToUse : Part, character : Model, humanoid : Humanoid)
	
	-- Create a dictionary of the part's properties to send to the Server
	local partProperties = {
		Size		= partToUse.Size,
		Position	= partToUse.Position,
		Shape		= partToUse.Shape,
		Anchored	= true,
		CanCollide	= partToUse.CanCollide,
	}
	
	-- Send this table to the Server to create a dummy part to check for collisions
	RemoteEventModule.DetectEnemiesEvent:FireServer(partProperties, character, humanoid)
	
end

This is when I began to feel like I was making a mistake using this method because it required constant client → server communication. I am unaware of exactly how important this is, but I figured if I could just do it all on the Server, why even use this method.

Lastly, here is the Server-Side script that uses the passed part properties:

-- Local function that will make the NPCs chase the player and attempt to remove them from the hitTable upon death
local function enemyMovement(characterTable, character : Model)
	-- Loop through the table and connect move events
	for _, enemyModel in characterTable do
		local enemyHumanoid			= enemyModel:FindFirstChild("Humanoid")
		
		enemyHumanoid:MoveTo(character:FindFirstChild("HumanoidRootPart").Position)
	end
end

-- Local function to use for testing hitbox detection with enemies
local function detectEnemies(player : Player, partProperties, character : Model, humanoid : Humanoid)
	-- player's HRP
	local humanoidRootPart
	
	
	-- define humanoid state
	local humanoidState
	
	-- make sure the humanoid sent, get its state
	if humanoid then
		humanoidState = humanoid:GetState()
	end
	
	-- check if the humanoid state is still alive, if not then do return end
	if humanoidState == Enum.HumanoidStateType.Dead then return end
	
	-- Create the dummy part based off the client sent information table
	local hitboxPart				= Instance.new("Part")
	
	-- define part properties
	for propertyName, propertyValue in pairs(partProperties) do
		hitboxPart[propertyName]	= propertyValue
	end
	
	-- Set the newly created part's collision group
	hitboxPart.CollisionGroup		= "HitboxGroup"
	
	-- Test detection with the above code
	local partsHit					= workspace:GetPartsInPart(hitboxPart, overlapParams)
	
	for i, partHit in partsHit do
		if partHit.Parent:FindFirstChildOfClass("Humanoid") and not table.find(hitCharacters, partHit.Parent) and partHit.Parent.Name ~= player.Name then
			
			-- TODO:_REWORK_REMOVING_THE_PLAYER_FROM_DETECTION
			table.insert(hitCharacters, partHit.Parent)
			
		end
	end
	
	enemyMovement(hitCharacters, character)
	
end

-- Connect Client To Server Remote Event To Detection Function
RemoteEventModule.DetectEnemiesEvent.OnServerEvent:Connect(detectEnemies)

Basically, a part will be consistently created/removed from the Server using the client part’s properties, and then I use :GetPartsInParts to detect collisions with enemies that use the same collision group as the created part.

I know that this method does not have all the functionality as the first, but since I know how to achieve that functionality, I wanted to ask for some opinions on the matter before coding this in.

While I was making the second method, I could not help thinking “Why am I doing this/Why is this method any better”?

I just wanted to ask for some opinions/maybe see how you all achieved something similar to this. Thanks in advance,

1 Like

You don’t need to create part every time, it’s very expensive, use raycast in front of enemy, or workspace:GetPartsInRadius()

1 Like

What do you mean by “Use Raycast in Front of an Enemy”?

i mean, use raycast in front of enemy to detect players, as creating part is not performant

1 Like

I guess I am still not understanding what you mean. Do you mean firing from the player’s perspective towards enemies, or firing from the enemy position towards the player?

I probably should have given more context, but the game I am making is not super complex. Enemies only need to be detected directly in front of the player, so I understand why you suggested raycasting. But groups of enemies spawn at random intervals/different positions, so I am unsure how to account for all of those enemies through just a single raycast (or even multiple, considering they are all at different positions).