Highly Customizable Pathfinding Script

Hello, Roblox community! I have previously crafted a non-customizable RPG-style script. Now that I have rewritten the script, it is better than ever. If you would like to use this script, follow the directions below.

Instructions:

  1. Add your desired enemy to the workspace.

  2. Add a server-sided script to the enemy [Regular script]
    Screenshot (8)

  3. Paste this code in the script and customize it.

  4. Be sure that all of the enemy’s body parts are all UnAnchored and CanCollide is turned on.

The script:

--Script made my Elite_Remote. Have fun(:
local damage = 10 --The damage the enemy does to the player after the attackSpeed.
local attackSpeed = 2 --Wait time before the NPC attacks again.
local respawnTime = 5 -- Wait time before the enemy respawns.
local defaultWalkSpeed = 16 --The walk speed your enemy will be defaulted to.

local hostileDistance = 25 --The distance at which the enemy will start walking towards the player
local attackDistance = 7.5 --The distance the enemy can attack the player at.

local randomWalkEnabled = true --When a player is not around and this is true, the enemy will walk around randomly.
local randomWalkDistance = 10 --When a player is not around and randomWalkEnabled is true, it will pathfind this distance away from the enemy.


--Advanced Options below

local constantWhileLoopWaitTime = 0.2 --Wait time for the function run(). .2 is a good place for this.
local randomWalkWaitTime = 2 -- If random walk is enabled, it will wait this amount of seconds before repathfinding randomly

local displayDamageUI = true -- When the enemy is damaged, if this is true, there will be text displaying how much damage the enemy took. Look below for customization.
local damageDiaplayColor = BrickColor.Red() --When the enemy takes damage, this color, along with the text, will display how much damage the monster took.
local damageDisplayFont = Enum.Font.SciFi --When teh enemy takes damage, this font, along with the text, will display how much damage the monster took.

local attackType = { --I will add to this but for now, friendly playes one animation and melee is close-ranged.
	melee = true;
	ranged = false;
	friendly = false;
}

local rangedWeapon-- = game.ServerStorage.Handle --Set this if you have the ranged attackType enabled. MAKE sure that this is a part, union, or a meshpart.
local rangedVelocity = 100 --How hard your enemy will throw your rangedWeapon, if you have ranged enabled.

local animations = { --Change the numbers of the strings below to change the animation or leave empty if you want no animation.
	spawnAnim = "rbxassetid://"; --When the enemy spawns in, it will use thi animation.
	attackAnim = "rbxassetid://180416148"; --The animation that the enmy uses when a player is close enough to attack.
	friendlyAnim = "rbxassetid://180416148"; --The animation used when the enemy's attackType is friendly [Plays the animation constantly]
}

local sounds = {
	spawnSound = "rbxassetid://574999280";
	attackSound = "rbxassetid://574999020";
	hurtSound = "rbxassetid://6443096290";
	deathSound = "rbxassetid://131138848"
}

local changeStatsAfterDeath = { --Add the leaderstats name [ex Gold] and the value it gains [+5]. Leave empty if you don't have leaderstats.
	--Cash = 10;
	--Kills = 1;
}

local drops = { -- Add possible drops and chances to your drops. 1 Being guaranteed. If the drop is a tool, it will go into the players inventory, however, if it is not a tool, it will go onto the enemy's standing position.
	--[game.ServerStorage.sword1] = 1;
}

--Customization ends here!


local path = game:GetService("PathfindingService"):CreatePath()
local playerThatKilled = ""
local humanoid = script.Parent.Humanoid
local humanoidRootPart = script.Parent.HumanoidRootPart
local npcClone = script.Parent:Clone()
local lastHealth = humanoid.MaxHealth
local isRandomWalking = false
local isFollowingPlayer = false

local function pathFind(pos)
	path:ComputeAsync(humanoidRootPart.Position, pos)
	
	for i,v in pairs(path:GetWaypoints()) do
		humanoid:MoveTo(v.Position)
		task.wait(0.2)
		
		if i == #path:GetWaypoints() then
			return true
		end
	end
end

local function randomWalk(d)
	task.wait(randomWalkWaitTime)
	if isRandomWalking then

	else
		local randomPos = Vector3.new(humanoidRootPart.Position.X + math.random(-d,d),0,humanoidRootPart.Position.Z + math.random(-d,d))
		if pathFind(randomPos) then
			isRandomWalking = false
		end
	end
end

function getCharacters()
	local plrTable = {}

	for _,plr in pairs(game.Players:GetChildren()) do
		for _,plrObject in pairs(workspace:GetChildren()) do
			if plr.Name == plrObject.Name then
				table.insert(plrTable, plrObject)
			end
		end
	end

	return plrTable
end

local function animate(animType)
	if not script.Parent:FindFirstChildWhichIsA("Animation") then
		for name,ID in pairs(animations) do
			local Anim = Instance.new("Animation", script.Parent)
			Anim.Name = name
			Anim.AnimationId = ID
		end
	end

	for name,ID in pairs(animations) do
		local anim = script.Parent:FindFirstChild(name)

		if anim then
			local loadedAnim = humanoid.Animator:LoadAnimation(anim)
			loadedAnim:Play()
			task.wait(loadedAnim.Length)
			return true
		end
	end
end

local function playSound(sound)
	if not script.Parent:FindFirstChildWhichIsA("Sound") then
		for name,ID in pairs(sounds) do
			local sound = Instance.new("Sound", script.Parent)
			sound.Name = name
			sound.SoundId = ID
		end
	end
	
	local soundItem = script.Parent:FindFirstChild(sound)
	
	if soundItem then
		soundItem:Play()
	end
	
	return true
end

local function run()

	if attackType.melee then
		for _,char in pairs(getCharacters()) do
			local distanceToEnemy = (humanoidRootPart.Position - char.HumanoidRootPart.Position).Magnitude

			if distanceToEnemy <= hostileDistance and not isFollowingPlayer then
				isRandomWalking = false
				isFollowingPlayer = true

				if pathFind(char.HumanoidRootPart.Position) then
					isFollowingPlayer = false
				end	
			end

			if distanceToEnemy <= attackDistance then
				if animate("attackAnim") and humanoid.Health > 0 then
					playSound("attackSound")
					
					if (humanoidRootPart.Position - char.HumanoidRootPart.Position).Magnitude < attackDistance then
						char.Humanoid.Health -= damage
					end
				end
			end
		end

		if randomWalkEnabled and isRandomWalking == false and not isFollowingPlayer then
			randomWalk(randomWalkDistance)
		end
	end
	
	if attackType.ranged then
		for _, char in pairs(getCharacters()) do
			local distanceToEnemy = (humanoidRootPart.Position - char.HumanoidRootPart.Position).Magnitude
			
			if distanceToEnemy <= hostileDistance then
				
				animate("attackAnim")
				playSound("attackSound")
				
				humanoid.WalkSpeed = 0
				
				humanoidRootPart.CFrame = CFrame.lookAt(humanoidRootPart.Position, char.HumanoidRootPart.Position)
				
				local weaponClone = rangedWeapon:Clone()
				weaponClone.Parent = workspace
				weaponClone.Position = humanoidRootPart.Position
				weaponClone.CanCollide = false
				weaponClone.Anchored = false
				weaponClone.CFrame = CFrame.lookAt(humanoidRootPart.Position, char.HumanoidRootPart.Position)
				weaponClone.Velocity = humanoidRootPart.CFrame.LookVector * rangedVelocity
			else
				humanoid.WalkSpeed = defaultWalkSpeed
			end
		end
		
		if randomWalkEnabled and isRandomWalking == false then
			randomWalk(randomWalkDistance)
		end
	end

	if attackType.friendly and randomWalkEnabled then
		while animate("friendlyAnim") do
			animate("friendlyAnim")
		end
	end
end

humanoid.HealthChanged:Connect(function(currentHealth)
	
	playSound("hurtSound")
	
	if displayDamageUI then
		local dps = lastHealth - currentHealth
		lastHealth = currentHealth

		local partParent = Instance.new("Part", workspace)
		partParent.Anchored = true
		partParent.Transparency = 1
		partParent.CanCollide = false
		partParent.Position = humanoidRootPart.Position + Vector3.new(math.random(-3,3),math.random(-3,3),math.random(-3,3))

		local billboardGui = Instance.new("BillboardGui")
		billboardGui.Size = UDim2.new(0, 100, 0, 100)
		billboardGui.AlwaysOnTop = false
		billboardGui.Enabled = true
		billboardGui.Parent = partParent

		local textLabel = Instance.new("TextLabel")
		textLabel.Text = dps
		textLabel.Size = UDim2.new(0, 100, 0, 50)
		textLabel.TextScaled = true
		textLabel.BackgroundTransparency = 1
		textLabel.Font = damageDisplayFont
		textLabel.Parent = billboardGui
		textLabel.TextColor = damageDiaplayColor

		game.Debris:AddItem(partParent, 1)
	end
end)

humanoid.Died:Connect(function()
	for statName,statValue in pairs(changeStatsAfterDeath) do
		local plr = game.Players:FindFirstChild(playerThatKilled)

		if plr then
			plr.leaderstats[statName].Value += statValue
		end
	end
	
	for dropItem, dropChance in pairs(drops) do
		local plr = game.Players:FindFirstChild(playerThatKilled)
		local chance = math.random(1, dropChance)
		
		if plr and dropChance == 1 then
			if dropChance == 1 then
				local drop  = dropItem:Clone()
				drop.Parent = plr.Backpack
			end
		end
	end
	
	playSound("deathSound")
	
	task.wait(respawnTime)
	npcClone.Parent = workspace
	script.Parent:Destroy()
end)

humanoidRootPart.Touched:Connect(function(hit)
	if hit.Parent.Parent:IsA("Model") and game.Players:FindFirstChild(hit.Parent.Parent.Name) then
		playerThatKilled = hit.Parent.Parent.Name
	end
end)

animate("spawnAnim")
playSound("spawnSound")

while task.wait(constantWhileLoopWaitTime) do
	run()
end

I will be modifiying this script based on what other people would like to see within the script. If you feels as if there is a unwanted feature or a feature that you feel should be added, please feel free to post your ideas down below.

You can also click here to try the script out if the instructions confuse you.
Customizable Enemy AI - Roblox

Changelog:

Change 1: Fixed the damage portion of the script.
Change 2: Added some features recommended by other people.
Change 3: Enchanced Directions.
Change 4: Changed wait()s to task.wait()s.
Change 5: Made the script more coherent and optimal.
Change 6: Added a GUI displaying the damage took.
Change 7: Major Update! Added range mode, sounds, and drops.
Change 8: Made melee mode less buggy, added damage billboard options,

14 Likes

wait() is deprecated, usetask.wait().

4 Likes

image
This won’t work, every time it loops it overrides the old MoveTo. Therefore, it would only move to the last waypoint (without going in between)

2 Likes

For some reason, the AI doesn’t Attack the player

1 Like

Did you toggle the friendly option by chance? You also have to add a script inside of the model of the enemy. When you use the Rig Builder in Roblox you must unanchor and turn on cancollide all of the body parts.

Would humanoid.MoveToFinished:Wait() fix the issue? I tried it and the enemy seems more buggy. I think the code without humanoid.MoveToFinished:Wait() is better, but I’ll leave a comment for anyone who wants it.

I tried that, and it errored out. Must be a different version of Lua that you are using.

No, every single Roblox script uses Luau & Roblox’s built-ins. task.wait is integrated. If switching wait( to task.wait( makes something break, that means there’s something seriously wrong with your code.

1 Like

I thought it was usetask.wait(); task.wait() does work and is more efficient. I will implement task.wait() into my code instead of wait().

Try:

  1. Getting the Magnitude of the distance between the NPC & the next waypoint
  2. Calculate the time it’ll take to travel there (TravelTime) with the NPC’s Humanoid WalkSpeed & the Magnitude
  3. task.wait(TravelTime - any small number) to get the NPC to continue walking to the next waypoint slightly before it normally would with Humanoid.MoveToFinished:Wait() so it feels more fluent?

I haven’t used this method above but I just thought about it for this case. Not sure how effective it’d be but it’s worth a shot.

2 Likes

Can we just do

humanoid:MoveTo(v.Position)
local distance = (v.Position - hrp.Position).Magnitude
repeat
   distance = (v.Position - hrp.Position).Magnitude
   task.wait(0.1)
until distance < 1
2 Likes

It doesn’t seem that ranged still works does it still work for anyone else or is it something I’m doing wrong possibly

very cool, i tested with custom r6 rig. but sometimes it does no damage and walks away.