NextPath: Next gen script for Nextbots

NextPath

Hello guys, it’s me again. For a while now, I’ve searched the toolbox to find the perfect NPC script or Nextbot script, but they all have always had problems. Either they use Humanoid:MoveTo() to move to a player (which lets the NPC easily get stuck on walls), or use Roblox pathfinding, which isn’t the greatest. I decided to write my own script for NPCs, but specifically for Nextbots. NextPath offers high customization options, while still being easy to manage, and is practically drag-and-drop. You can find the script on the toolbox, or alternatively, you can get an Example Nextbot here. First, I’ll go over configuration.

Settings
You will find these settings at the top of the script titled NextPathHandler.

local ThinkingEnabled = true -- If the NPC should think
local Rate = 0.1 -- Rate of ticking NPC
local MaxDistance = 500 -- Max distance NPC can search in
local VisibilityCheck = false -- Determines if NPC should check for visibility
local RunDistance = 20 -- Distance before NPC switches to Humanoid:MoveTo()
local MaxJumpTicks = 20 -- How many ticks the NPC should wait before jumping (if the target is above it)
local VerticalJumpDistance = 20 -- The distance (in studs) that a target must be above an NPC for it to try to jump
local CanJump = true -- If the Nextbot should be able to try and jump if a player is out of their reach
local Damage = 1000 -- Set to 0 to disable
local TimeBeforeAmbientSound = 30 -- If you have an ambient sound, this is how many ticks it takes to play it

And the script is readable and has comments placed over important parts of it, for those who want to look at it but don’t feel like having to process what each chunk of code does.

The NPC can be customized to see through walls, have ambient, looped, and kill sounds, it will jump to try and reach players if they are a customizable distance above them, and it uses a combination of 2 different approaches for moving towards players.

Behavior Rundown
The NPC will first sort through players to see who it wants to target. It will send a raycast to each player (granted you have the visibility check set to true, if it is set to false, it will automatically queue them) to see if realistically the NPC could see them from where it’s standing. It then queues the players and checks their distance to see who is the closest person to the NPC, which will be set to their target.

When the NPC has a target, it will have 2 modes that it can go into, Pathfinding and Dashing. When the NPC is outside of the Run Distance, it will use SimplePath Pathfinding (thank you @GrayzcaIe for making this, I hate dealing with pathfinding. Also go check out that resource, its really good.) to look for them, which means unlike most NPC scripts that just make the NPC move towards its target, it won’t get stuck on walls very often, as it can just pathfind around them. Then, when the NPC gets within Run Distance, it will switch to using Human oid:MoveTo() to run towards them, meaning it will have a clear path to get to you, and won’t make odd movements. The NPC can also damage players, but this can be disabled if chosen to.

Demonstration
Enough talk, it would be very useful for people stumbling upon this forum post to see some examples, since some people, including me, aren’t a big fan of having to use something in studio just to see what it does. Here are some videos below demonstrating what the script can do:

Nextbot chasing me
This showcases the general behavior of the Nextbot

Nextbot jumping showcase
This demonstrates some of the script which in the case of Roblox pathfinding not being able to detect you, the bot automatically switches to Run Mode.

And thats all for now. This script isn’t performance tested, so I have no clue what any of the stats for it are, but I whipped this up over the course of about a week (off and on because of school), because I wanted to share it, and I really needed a good enough AI script for my games.

No credit is required, and I will never require you to credit me for using any of my resources. I make these resources for other developers to use, so feel free to use it in any projects for any reason!

53 Likes

yooo this is actually really cool, good job!

7 Likes

What about it? I never said I didn’t use roblox pathfinding, I said that the scripts only use one of these, and both methods have their negatives. I used a combination of roblox pathfinding plus moveto because it executes such a feat more efficiently than only using one method.

7 Likes

good job looks great would use if i had a nextbot game

7 Likes

For some reason, the nextbot volume is constant regardless of the distance between them and me. I even changed the code to locate the sounds in a nextbot’s head, since sounds must be in a part to change the volume depending on the distance but there is 0 difference.

I’m not sure if this is intended or a bug, but I’d like to know if there’s a way to disable that.

3 Likes

lil bro if the sounds are in a folder they will play everywhere
put the sounds in an attachment with the same name as the folder

3 Likes

You have to play the sound from a part. If that doesn’t work, mess around with the MaxRollOffDistance and the MinRollOffDistance

3 Likes

lil bro if the sounds are in a folder they will play everywhere
put the sounds in an attachment with the same name as the folder
@F0xBirdmansBFF

not sure why that would make a difference, the folder with the sounds is inside the head, should it not be playing through the head? Also, don’t call me that

You have to play the sound from a part. If that doesn’t work, mess around with the MaxRollOffDistance and the MinRollOffDistance
@PosFind

I am (supposedly) playing the sounds from a part, I tried playing around with my Max and Min rolloffs and even though I’m well over 300 studs away from the nextbots, it still sounds like it’s right on top of me

1 Like

lil bro sounds can only be played from a part either through a part or attachment
why reply when u didnt even test

2 Likes

This doesn’t answer my question, the sounds are under a folder which is under the Nextbot’s head. In theory, it should work, I’m just wondering why it doesn’t

1 Like

can u just read my post and try it

:roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes::roll_eyes:

3 Likes

Reminds me of that one youtube arcadian rift devlog about AI. You planning on making AI as advanced as that?

3 Likes

If I have spare time, I may update this in the future. I’ve thought about making a custom pathfinding module, so that might be my next project

4 Likes

Hi there, sorry for bumping this up but I’m quite struggling to make a “Patrol” system for the script.
I tried to mess around with SimplePath in this specific section that you mentioned:

“You could add code here to make the NPC patrol around an area for example,
but I’m using this for Nextbots, so they will always be able to see somebody.”

But I’m having trouble making the NPC stop going to the patrol waypoint after it sees a player. Example: https://gyazo.com/b13fc3cadd8204d16cdf05f90514c1c4

Providing the script i used wont help, its the exact same just adding these 2 lines at the else statement (thang):

--Make sure target is not nil
if target ~= nil then
	local hrp = target.Character:FindFirstChild("HumanoidRootPart")
	local targetHum = target.Character:FindFirstChild("Humanoid")
	
	if hrp and targetHum then
		--Get distance to player and height difference for calculations
		local distance = target:DistanceFromCharacter(hitbox.Position)
		local heightDifference = hrp.Position.Y - hitbox.Position.Y
		
		local runTo = false
		if distance < RunDistance then
			runTo = true
		end
		if pathError then
			pathError = false
			runTo = true
		end
						
		if runTo then
			hum:MoveTo(hrp.Position)

			if CanJump then
				--Prevent nextbot from jumping when too close to target
				if distance > 4 then
					if heightDifference < VerticalJumpDistance and jumpTick == MaxJumpTicks then
						jumpTick = 0
						hum.Jump = true
					end
				else
					--damage player
					if Damage ~= nil and Damage > 0 then
						targetHum:TakeDamage(Damage)

						if killSound then
							killSound:Play()
						end
					end
				end
			end
		else
			Path:Run(hrp.Position)
		end
	end
	else
	--[[
		You could add code here to make the NPC patrol around an area for example,
		but I'm using this for Nextbots, so they will always be able to see somebody.
	]]
					
	local thang = SimplePath.new(script.Parent)
	thang:Run(Vector3.new(150,0,0))
end

I tried looking into the API of SimplePath, i know it has something called stop Path but I’m unable to make the path stop when the NPC sees a player since they are separate conditions. I hope i made everything clear. (Sorry if I’m dumb cuz its defo something simple but I’m just quite confused)

1 Like

Hey, sorry for the late reply, I haven’t really been active on the Devforum recently like I used to be.
I would assume the way to fix your problem is that the path keeps on running even when the NPC is already running the thang path, so I would try going up to the line with the --Get distance to player and height difference for calculations comment, and calling the :Stop() function on the thang path, so it stops running the path and starts running towards the player.
If you do it this way you will also need to move the thang variable to be higher up in the function, or make it a global variable so you can call the Stop function on it!

If that doesn’t work, or if you notice performance issues with my script, then you may just want to make your own system from scratch. I never did performance tests on this because even though I made this in October, I have learned way more about programming than what I knew back in 2022, and this may not be the best script for everyone.

I do wish you luck though. Just reply if you need any help and hopefully I’ll be on the Devforum and be able to reply!

2 Likes

Thank you so much I managed to figure something out!

For anyone who wants a patrolling system included (not the best but does the job) here is the script:
Put these outside of the task.spawn loop

local Patroling = false
local PatrolPath = SimplePath.new(script.Parent)
PatrolPath.Reached:Connect(function()
	Patroling = false
end)
PatrolPath.Stopped:Connect(function()
	repeat task.wait() until Path.Status == "Idle"
	Patroling = false
end)

And then just copy paste this part of code into the script:

--Make sure target is not nil
				if target ~= nil then
					local hrp = target.Character:FindFirstChild("HumanoidRootPart")
					local targetHum = target.Character:FindFirstChild("Humanoid")
					
					if hrp and targetHum then
						--Get distance to player and height difference for calculations
						local distance = target:DistanceFromCharacter(hitbox.Position)
						local heightDifference = hrp.Position.Y - hitbox.Position.Y
						
						local runTo = false
						if distance < RunDistance then
							runTo = true
						end
						if pathError then
							pathError = false
							runTo = true
						end
						
						if runTo then
							hum:MoveTo(hrp.Position)
							
							if PatrolPath.Status == "Active" then
								PatrolPath:Stop()
							end
							
							if CanJump then
								--Prevent nextbot from jumping when too close to target
								if distance > 4 then
									if heightDifference < VerticalJumpDistance and jumpTick == MaxJumpTicks then
										jumpTick = 0
										hum.Jump = true
									end
								else
									--damage player
									if Damage ~= nil and Damage > 0 then
										targetHum:TakeDamage(Damage)

										if killSound then
											killSound:Play()
										end
									end
								end
							end
						else
							Path:Run(hrp.Position)
							if PatrolPath.Status == "Active" then
								PatrolPath:Stop()
							end
							print("run")
						end
					end
				else
					local RandomPath = math.random(1,3)
					if Patroling == false then
						PatrolPath:Run(game.Workspace.Patrol:FindFirstChild("Part"..RandomPath).Position)
						Patroling = true
					end
				end

As I said its not really the best but it worked fine for me. Once again thanks for the help!

1 Like
old post

Your NextPath script seems to be broken, I only modified a little bit

-- Settings --

local CS = game:GetService("CollectionService")


local DebugMode = true -- Enables debugging

local ThinkingEnabled = true -- If the NPC should think
local Rate = 0.1 -- Rate of ticking NPC
local MaxDistance = 500 -- Max distance NPC can search in
local VisibilityCheck = false -- Determines if NPC should check for visibility
local RunDistance = 20 -- Distance before NPC switches to Humanoid:MoveTo()
local MaxJumpTicks = 20 -- How many ticks the NPC should wait before jumping (if the target is above it)
local VerticalJumpDistance = 10 -- The distance (in studs) that a target must be above an NPC for it to try to jump
local CanJump = true -- If the Nextbot should be able to try and jump if a player is out of their reach
local Damage = 1000 -- Set to 0 to disable
local TimeBeforeAmbientSound = 30 -- If you have an ambient sound, this is how many ticks it takes to play it

-- Main code --

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")

local sp = ServerStorage:FindFirstChild("SimplePath")
if not sp then
	--This message is just so people don't scratch their heads for 10 years wondering why the script didn't work
	--I am also one of those people who would make that mistake
	error("SimplePath was not found in game.ServerStorage!")
end

local Scanner = require(ServerStorage:FindFirstChild("Scanner"))
local SimplePath = require(sp)

local hitbox = script.Parent:FindFirstChild("HumanoidRootPart")
local hum = script.Parent:FindFirstChild("Humanoid")
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
local blacklist = { script.Parent }
local target = nil
local jumpTick = 0
local ambientSoundTick = 0
local pathError = false

local Path = SimplePath.new(script.Parent)
Path.Visualize = true

Path.Error:Connect(function()
	if pathError == false then
		pathError = true
	end
end)

--Scanner is a small module I made to automate filtering players conditionally
-- * Side note: You can add your own conditions here for better filter control!
local aiScanner = Scanner.create(function(plr: Player)
	local char = plr.Character
	return char ~= nil 
		and char:FindFirstChild("Humanoid")
		and char:FindFirstChild("Humanoid").Health > 0
		and plr:DistanceFromCharacter(hitbox.Position) < MaxDistance
		and not char:FindFirstChildOfClass("ForceField")
end)

local trackedPlayers = {}

--spawn the serverside tick loop
task.spawn(function()
	while ThinkingEnabled do
		task.wait(Rate)

		--This controls when the Nextbot should Jump
		if jumpTick < MaxJumpTicks then
			jumpTick += 1
		end

		if #Players:GetChildren() > 0 then
			aiScanner:Scan() --Scan for players

			--Just a safety precaution :>
			if aiScanner.ScanTable ~= nil and #aiScanner.ScanTable > 0 then
				local dist = {}

				--loop through every player and check if they can be seen or not
				for _, p: Player in ipairs(aiScanner.ScanTable) do
					if VisibilityCheck then
						blacklist = { script.Parent }
						local whitelist = {}

						--Filter out anything that may block the raycast, like accessories or hats
						for _, a in pairs(p.Character:GetDescendants()) do
							if a:IsA("BasePart") then
								if a.Parent == p.Character then
									table.insert(whitelist, a)
								else
									table.insert(blacklist, a)
								end
							end
						end

						params.FilterDescendantsInstances = blacklist

						for _, a2 in ipairs(whitelist) do
							local dir = (a2.Position - hitbox.Position).Unit
							local ray = workspace:Raycast(hitbox.Position, dir * 500, params)

							--If raycast worked and hit a part, see what happens with it
							if ray and ray.Instance then
								if table.find(whitelist, ray.Instance) then
									--add player to distance tracking list
									dist[p] = p:DistanceFromCharacter(hitbox.Position)
									break
								else
									--if raycast didn't hit a player, and there is already a target, set to nil
									if target ~= nil and target == p then
										target = nil
									end
								end
							end
						end
					else
						--If you don't want Nextbot to see through walls, just go ahead and player to list
						dist[p] = p:DistanceFromCharacter(hitbox.Position)
					end
				end

				--Loop to calculate closest player
				local biggestDistance = MaxDistance
				for x = 1, #Players:GetChildren() do
					local plr = Players:GetChildren()[x]
					local plrDistance = dist[plr]

					if plrDistance ~= nil then
						if plrDistance < biggestDistance then
							target = plr
							biggestDistance = plrDistance
						end
					end
				end

				--Make sure target is not nil
				if target ~= nil then
					local hrp = target.Character:FindFirstChild("HumanoidRootPart")
					local targetHum = target.Character:FindFirstChild("Humanoid")

					if hrp and targetHum then
						--Get distance to player and height difference for calculations
						local distance = target:DistanceFromCharacter(hitbox.Position)
						local heightDifference = hrp.Position.Y - hitbox.Position.Y

						local runTo = false
						if distance < RunDistance then
							runTo = true
						end
						if pathError then
							pathError = false
							runTo = true
						end

						if runTo then
							hum:MoveTo(hrp.Position)

							if CanJump then
								--Prevent nextbot from jumping when too close to target
								if distance > 4 then
									if heightDifference < VerticalJumpDistance and jumpTick == MaxJumpTicks then
										jumpTick = 0
										hum.Jump = true
									end
								else
									if not CS:HasTag(target.Character, "Enemy") then
										if Damage ~= nil and Damage > 0 then
											targetHum:TakeDamage(Damage)
										end
									end
								end
							end
						else
							Path:Run(hrp.Position)
							--print("run")
						end
					end
				else
					--[[
						You could add code here to make the NPC patrol around an area for example,
						but I'm using this for Nextbots, so they will always be able to see somebody.
					]]
				end
			end
		end
	end
end)

logs:

16:41:51.660  Requested module experienced an error while loading  -  Server - Main:9
  16:41:51.660  Stack Begin  -  Studio
  16:41:51.661  Script 'Workspace.Main', Line 9  -  Studio - Main:9
  16:41:51.661  Stack End  -  Studio
  16:41:52.002  Character is not a valid member of Script "Players.Script"  -  Server - NextPathHandler:72
  16:41:52.002  Stack Begin  -  Studio
  16:41:52.002  Script 'Workspace.luunerstars.NextPathHandler', Line 72  -  Studio - NextPathHandler:72
  16:41:52.002  Script 'ServerStorage.Scanner', Line 19 - function Scan  -  Studio - Scanner:19
  16:41:52.002  Script 'Workspace.luunerstars.NextPathHandler', Line 93  -  Studio - NextPathHandler:93
  16:41:52.003  Stack End  -  Studio
  16:41:52.003  Character is not a valid member of Script "Players.Script"  -  Server - NextPathHandler:72
  16:41:52.003  Stack Begin  -  Studio
  16:41:52.003  Script 'Workspace.luunerstars.NextPathHandler', Line 72  -  Studio - NextPathHandler:72
  16:41:52.003  Script 'ServerStorage.Scanner', Line 19 - function Scan  -  Studio - Scanner:19
  16:41:52.003  Script 'Workspace.luunerstars.NextPathHandler', Line 93  -  Studio - NextPathHandler:93
  16:41:52.003  Stack End  -  Studio
  16:41:52.004  Character is not a valid member of Script "Players.Script"  -  Server - NextPathHandler:72
  16:41:52.004  Stack Begin  -  Studio
  16:41:52.004  Script 'Workspace.luunerstars.NextPathHandler', Line 72  -  Studio - NextPathHandler:72
  16:41:52.004  Script 'ServerStorage.Scanner', Line 19 - function Scan  -  Studio - Scanner:19
  16:41:52.004  Script 'Workspace.luunerstars.NextPathHandler', Line 93  -  Studio - NextPathHandler:93
  16:41:52.004  Stack End  -  Studio
  16:41:52.005  Character is not a valid member of Script "Players.Script"  -  Server - NextPathHandler:72
  16:41:52.005  Stack Begin  -  Studio
  16:41:52.005  Script 'Workspace.aimuley.NextPathHandler', Line 72  -  Studio - NextPathHandler:72
  16:41:52.005  Script 'ServerStorage.Scanner', Line 19 - function Scan  -  Studio - Scanner:19
  16:41:52.005  Script 'Workspace.aimuley.NextPathHandler', Line 93  -  Studio - NextPathHandler:93
  16:41:52.005  Stack End  -  Studio
  16:41:52.005  Character is not a valid member of Script "Players.Script"  -  Server - NextPathHandler:72
  16:41:52.006  Stack Begin  -  Studio
  16:41:52.006  Script 'Workspace.luunerstars.NextPathHandler', Line 72  -  Studio - NextPathHandler:72
  16:41:52.006  Script 'ServerStorage.Scanner', Line 19 - function Scan  -  Studio - Scanner:19
  16:41:52.006  Script 'Workspace.luunerstars.NextPathHandler', Line 93  -  Studio - NextPathHandler:93
  16:41:52.006  Stack End  -  Studio

edit: fixed it myself

This is really cool, and I might seriously consider using it to create a fork off of for a future vehicle AI Controller. Is there a possible way to define a minimum gap distance that our AI can fit through (for example, a player would be 4 studs wide to fit arms in and a car might be 10 studs, if it doesn’t fit through the gap it recalculates). If so, this would be just what I’m looking for. If not, I may fork it anyways.