Best way of implementing pathfinding/AI for my purposes and where can I learn about how to do it?

I’m relatively new to both developing games and posting on the Dev Forum and development for my first game has hit a brick wall. It’s one of those moments where you think something that is actually super complicated is actually relatively simple, and in hindsight, it probably shouldn’t have been the first game that I tried developing. Regardless, what’s decided is decided and I’d like to develop a habit of sticking through with a game to completion before moving on, which is why I’m making this topic.

I don’t want to reveal too much about the game’s details, but the problem I’m having is concerning the implementation of AI enemies and pathfinding. My planned game is basically a zombie survival game, where there are multiple different types of enemies that act and react completely different from one another. Here are the basic categories:

  • The most common type, humanoid zombies of different sizes and speeds with different amounts of health that deal different amounts of damage. These enemies would behave like most zombies you’ve seen would and would chase the player and melee attack them. Ideally, they would use the pathfinding service to navigate their environment in order to make them more versatile and dangerous than the average zombie.
  • A stationary type that would shoot projectiles at the player. May make it so that some projectiles track, if possible, which may require the pathfinding service, I don’t know.
  • Another stationary type that would only react when players are in proximity to it. No part of it would move whatsoever. One of its reactions would be to spawn enemies of any category.

Ideally, these enemies would get more and more numerous as the game session progressed until everyone died, like in most survival games. When I first thought all this up, I knew about changing humanoids and basic pathfinding as explained in most YouTube tutorials on the pathfinding service, as well as how to make humanoids track other humanoids. I was fully aware that I would run into problems but I was kind of taken on a ride and now I have completely no idea what I’m doing.

I ran into problems with the initial pathfinding implementation (PathfindingService run in a loop, basically), which you can see discussed in this topic here, and got recommended the open-source module SimplePath as a solution. The module was great, but I still ran into issues, particularly with AI with different movement speeds. I was then told that those issues could be fixed by doing pathfinding on the client, which I didn’t even know was possible at the time. I quickly looked up how to do clientside pathfinding and got very, very few results. I’ve only seen a few topics talking about clientside pathfinding and of all the ones I’ve seen, none of them actually discuss how to do it, rather only that it can be done. I found only one YouTube video discussing a way of doing it and got in contact with the developer who made it, but at that point even if I could twist and shape his system into supporting my needs I would’ve been shooting completely in the dark, moreso than if I was just taking stuff that I knew and using it in new ways (no fault on the part of the developer).

That’s all why I decided to make this topic. I just want as straight of an answer as possible as to how to design this system to be as efficient and effective as possible, and resources that I can look at that will teach me this stuff in a way that I understand.

tl;dr: I want guidance on how to design a system for hundreds of “zombie” NPCs of different types (tracking, shooting, stationary), how to implement pathfinding in that system, and where I could possibly learn about it in a way that I can understand.

DISCLAIMER: If this topic is in the wrong section, please feel free to direct me to the appropriate section to post it in

I’m still looking for help with this. Does anyone have any advice they can give me here?

Having pathfinding on the client is the way to go, as it doesn’t have to contact Roblox’s servers which takes extra time rather than just doing it directly on your client.

I’d say if you want different behaviors as you mentioned ‘tracking, shooting and stationary’ I suggest using Behavior Trees!

A behavior tree is a tree with a starting point known as a ‘root’ which is a parent for all it’s ‘sub-nodes’
It works by going down the tree’s ‘sub-nodes’ which have different behaviors.

I suggest reading up on it although it works by using a 3-status system as I call it.
You have OK, FAIL, SUCCESS. These determine how your tree should work.

You can therefore easily use PathfindingService with it.

Someone has made a awesome BehaviorTree module, I suggest checking it out!

It seems fine as to what you’re doing and if I misunderstood your question please let me know :slightly_smiling_face:

Not against this. But how would the player see the exact same pathfinding as the other? If your saying “remoteevents” this destroys the whole purpose of “dosent have to contact Roblox servers”

It differs on your use of it. Although I might’ve worded this a little wrongly;
It’s really dependant on if multiple players or one player has to see this. In this case I thought that @Superdestructo09 was doing it for only one player.

I’d say if it is for multiple players, having the pathfinding calculated on the server is needed.

1 Like

Yes, this is meant for a multiplayer game. I should have specified that. What I’ve noticed for serverside pathfinding is that it really doesn’t work for AI with faster walking speeds. They’ll arrive near where you are and then slow down to typical walking speed and won’t catch up to you unless you stop moving.

Usually I have a combination of raycasts + pathfinding.

You can use raycasting for when it’s not needed and use MoveTo instead of pathfinding.
Then use pathfinding when it’s needed.

A example would be if there’s a wall infront of the NPC it will therefore use pathfinding, else it will just MoveTo.

Pretty basic example, but that’s a performant way of doing it.

I wrote a quick script based on what you said and I ran into this problem again:
robloxapp-20220206-1707593.wmv (4.1 MB)

Here’s the script, it uses the “SimplePath” module for pathfinding, which I linked in the OP:

local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerStorage")
local SP = require(SS:WaitForChild("SimplePath"))

local sprinter = script.Parent
local hum = sprinter:WaitForChild("Humanoid")
local HRP = sprinter:WaitForChild("HumanoidRootPart")

-- SimplePath path creation
local path = SP.new(sprinter)
path.Visualize = true

-- this and checknames() are for distinguishing between friend or foe
local names = {}
for _, name in pairs(RS:WaitForChild("Enemies"):GetChildren()) do
	table.insert(names, name.Name)
end

function checkNames(query)
	for _, name in pairs(names) do
		if query == name then
			return true
		end
	end
	return false
end

-- finds a player target and checks if they're blocked by a wall
function findTarget()
	local aggro = 200
	local target
	local blocked = true
	for _, character in pairs(workspace:GetChildren()) do
		local human = character:FindFirstChild("Humanoid")
		local RP = character:FindFirstChild("HumanoidRootPart")
		if human and RP and human.Health > 0 and not checkNames(RP.Parent.Name) then
			if (RP.Position - HRP.Position).Magnitude < aggro then
				aggro = (RP.Position - HRP.Position).Magnitude
				target = RP
				local targetRay = Ray.new(HRP.Position, (RP.Position - HRP.Position).Unit * 200)
				local hit, position = workspace:FindPartOnRayWithIgnoreList(targetRay, sprinter:GetChildren())
				if hit == RP then
					blocked = false
				end
			end
		end
	end
	return target, blocked
end

-- main loop
while true do
	wait()
	-- find the closest player
	local target, blocked = findTarget()
	-- if they exist
	if target then
		-- if they're not blocked by a wall use MoveTo()
		if not blocked then
			hum:MoveTo(target.Position)
		-- otherwise use pathfinding
		elseif blocked then
			path:Run(target)
		end
	end
end

I would highly suggest using CollectionService for most of this although;

Also the issue seems to be

It’s actually workspace:Raycast().

I hope this fixes it :slightly_smiling_face:

Swapping out that code with yours, the console gave me the error “Unable to cast RaycastResult to Ray”.

local targetRay = workspace:Raycast(HRP.Position, (RP.Position - HRP.Position).Unit * 200)
local hit, position = workspace:FindPartOnRayWithIgnoreList(targetRay, sprinter:GetChildren())

Even if it worked, I don’t see how that would fix the problem I’m having. The Raycasting was already working to some degree, the issue is that the AI cannot catch up to my character when it’s moving, as shown in the video I provided above.

Ah I’m sorry I hadn’t looked at the video you posted;

You can use the second parameter of MoveTo()

You are by default setting the reach position to the target therefore reaching the Humanoid.WalkToPart destination each time it reaches it.

As I’m not too familiar with MoveTo() I could be entirely wrong although;

Try and do:

hum:MoveTo(target.Position, workspace.Terrain)

This will set the end goal to workspace.Terrain meaning it will never reach Humanoid.WalkToPart

I just implemented this change and it made no difference. Based on the visible waypoints from pathfinding, it seems like the position that they’re aiming for is lagging behind me instead of accurately sticking to my character.

The green dot is meant to be the end target of the NPC that is still using pathfinding. It doesn’t actually line up with where I am unless I stand completely still.

Please note that I’m still looking for a solution here, either to the problem I’ve been working on with As8D or with organizing my AI in general. Any help is appreciated.

:FindPartOnRay and everything related to Ray.new is deprecated, you’re supposed to replace it with workspace:Raycast

local RayInfo = RaycastParams.new()
RayInfo.FilterType = Enum.RaycastFilterType.Blacklist
RayInfo.FIlterDescendantsInstances = {...}

local Hit = workspace:Raycast(Origin, Direction, RayInfo)
--Hit.Position, Hit.Instance, Hit.Normal ...

While that’s usually a good idea, it can result in problems where the zombie can see the player, but the path to get to the player isn’t a straight line, leading to problems like this


Where, the zombie doesn’t see the player and pathfinds, the zombie sees the player and stops pathfinding, and just uses :MoveTo, and the zombie loses sight of the player because the path to the player isn’t a straight line, anddd repeat.

As I mentioned this is a basic example;

You can check the height, have some max height constant and see if the player is over the zombie.
Therefore you can use pathfinding.

Hmm, I don’t see why as to it would lag behind.
Could you try using the superseded methods as mentioned by @Judgy_Oreo to see if it fixes it

That also wouldn’t work, if there was a gap between the zombie and the player. If the zombie originated at the top of the stairs, following the logic it would walk down the stairs as the player is in that general direction (like in the video I sent), pathfind to the top where’s it’s at the same height as the player, then walk back down and get stuck in an infinite loop.

Of course, I’m not here to shoot down your ideas just because I want to, I just thought it was worth explaining the problems that might arise to anybody reading these replies. I’ll just leave your ideas alone after this reply.

I’m a bit confused as to how FilterDescendantsInstances works. I read the wiki and it didn’t really elaborate very well. If my current ignore list is all the parts of the AI’s model, then what would I set FilterDescendantsInstances equal to: script.Parent:GetChildren() or just script.Parent?

FilterDescendantsInstances just means the list of things to ignore or the list of things to whitelist, just like :FindPartOnRayWithIgnoreList or :FindPartOnRayWithWhitelist.

--Either
RayInfo.FilterDescendantsInstances = {script.Parent)
--Or
RayInfo.FilterDescendantsInstances = script.Parent:GetChildren()
--Both do the same this

It’s important to note that RayInfo.FilterType controls whether FilterDescendantsInstances is a blacklist/ignorelist, or a whitelist.