General Combat NPC Tutorial

Hello everyone, today I want to update my npc tutorial. This one is going to be much more general and what you should do and not do.

Old post for attacking NPCs

Have you ever wondered how to make a npc that attacks you? Well here’s a tutorial for that!

Part 1 - Making the Dummy
First off, let’s create the npcs. For R15 games go to the plugins tab and press this icon in the screenshot to insert a R15 character make sure to un-anchor the root part!

Screenshot

Or if you want a R6 character you could insert this model of a R6 character:
R6 Dummy Rig - Roblox

Part 2 - Scripting Time :sunglasses:
Alright scripting time also known as the best time! :grinning:
So first, create a script inside the dummy/NPC you can name it whatever. So inside the script insert this code. (I won’t be going step by step so I don’t make this to post to long :+1:)

-- Follow Script

local PathFinding = game:GetService("PathfindingService")
local path = PathFinding:CreatePath()

local AttackEvent = script.Parent:WaitForChild("AttackEvent") -- Used to attack

local debounce = false -- Debounce so it doesn't spam attack
local cooldown = 0.5

local maxDistance = 100 -- Max Distance change to your liking
local closestPlayer = nil -- Locks to closest player
local closestDistance = maxDistance
local attackRange = 5 -- Range npc can attack you

local Players = game:GetService("Players")

local npc = script.Parent
local HRP = npc:WaitForChild("HumanoidRootPart")
local humanoid = npc:WaitForChild("Humanoid")

-- New Path Function
function newPath(character)
	-- Compute the path
	path:ComputeAsync(HRP.Position, character.HumanoidRootPart.Position)
	local waypoints = path:GetWaypoints()

	for i, waypoint in pairs(waypoints) do
		npc.Humanoid:MoveTo(waypoint.Position + Vector3.new(1, 0, 1)) -- Move to the waypoint position
	end
end

-- When damaged move the NPC
humanoid:GetPropertyChangedSignal("Health"):Connect(function()
	while true do
		for _, player in pairs(Players:GetPlayers()) do
			local playerRoot = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
			local playerHumanoid = player.Character and player.Character:FindFirstChild("Humanoid")

			if playerRoot then

				if playerHumanoid.Health <= 0 then
					closestPlayer = nil
					closestDistance = maxDistance
					break
				end

				if (playerRoot.Position - HRP.Position).Magnitude <= attackRange then
					AttackEvent:Fire() -- Fire Attacks Event
					break
				end
				
				if (playerRoot.Position - HRP.Position).Magnitude <= maxDistance then
					newPath(player.Character) -- Move characters
					break
				end
			end
		end

		task.wait()
	end
end)

Alright! We got a basic following script! Good job! Now let’s create the attack event! First create a bindable event and put it inside dummy/npc. Call it “AttackEvent.” Now for hit detection. First get the sword it can be a roblox sword or a custom sword it doesn’t matter. So to make it detect the event, instead of the activated just replace it with the event. For example:

local AttackEvent = script.Parent:WaitForChild("AttackEvent") -- Used to attack

function Attack()

end

AttackEvent.Event:Connect(Attack)

Also make sure to add an animate script to give the dummy animations. You could get the animate script by searching inside the toolbox.

Here’s a video of it in action:

Well this is it hopefully you learned something new. Well, have a nice day and thank you for reading :slight_smile: .

Table of Content
  • The Basics of NPCs
  • Controlling NPCs with one Script
  • Utilizing Modules
  • Client NPCs

The Basics

N.P.C. stands for Non-Playable-Character.

NPCs are good for many types of games such as tower defense games, wave games, and much much more. It doesn’t have to be all about fighting NPC. But, in this tutorial I’m going to be talking more about fighting NPC.

So how should you organize your NPCs? You should always have it in a folder. The more organized the better.

Creating NPCs with one Script

BUT, less is more when scripting them. The less NPC scripts the better. You should always try having only one controller script instead of multiple scripts under each NPC. You can do something like this:

--// NPC Folder
local npcFolder = script.Parent

--// Loop NPC Function
local function loopNPC()
	--// Loop All NPC
	for _, npc in pairs(npcFolder:GetChildren()) do
		-- We will wrap this so there's no specific order
		coroutine.wrap(function()				
			local humanoid = npc:FindFirstChild("Humanoid")
			local rootPart = npc:FindFirstChild("HumanoidRootPart")
			
			-- Check if the NPC is a NPC
			if npc:IsA("Model") and humanoid and rootPart then
					-- Path find
				end
			end
		end)()
	end
end

--// Loop
loopNPC()
npcFolder.ChildAdded:Connect(loopNPC)

This is much more efficient since now you have less scripts meaning less lag. :grinning:

Still lag is still a big problem.

  • My npc is stuttering!
  • I have too much NPCs it’s still lagging!

Utilizing Modules

What do you do??? First, I would first recommend Simple Path! It’s easy to use and makes NPCs path find much more smoother.

Here’s the updated code:

--// Service
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")

--// Modules
local Modules = ServerStorage:WaitForChild("Server_Modules")
local SimplePath = require(Modules.SimplePath)

--// NPC Folder
local npcFolder = script.Parent

local Paths = {}

--// Create Path Function
local function createPath(npc)
	if not npc:IsA("Model") then return end
	if not npc:FindFirstChild("Humanoid") then return end
	if Paths[npc] then return end
	
	-- We create a path for the simple path module

	Paths[npc] = SimplePath.new(npc)
end

--// New Path Fuction
local function newPath(npc, target, distance)
	local Path = Paths[npc] 

	if Path and distance <= 200 then -- 200 is the maximum distance
		local Goal = target.Position
		
		-- Using :Run() moves the npc
		Path:Run(Goal)

		return Path:Run(Goal)
	end
end

--// Get Closest Player Function
local function getClosestPlayer(npc, rootPart)
	local current_target
	local current_distance = 200

	for _, target in pairs(Players:GetPlayers()) do
		target = target.Character or target.CharacterAdded:Wait()

		if not target:IsA("Model") or not target:FindFirstChild("HumanoidRootPart") then continue end

		local targetRoot = target:WaitForChild("HumanoidRootPart")
		local targetHumanoid = target:WaitForChild("Humanoid")

		local distance = (rootPart.Position - targetRoot.Position).Magnitude

		if targetHumanoid.Health ~= 0 and distance < current_distance then
			current_target = targetRoot
			current_distance = distance
		end
	end

	return current_target, current_distance
end

--// Loop NPC Function
local function loopNPC()
	--// Loop All NPC
	for _, npc in pairs(npcFolder:GetChildren()) do
		createPath(npc) -- Create Path for NPC
		-- We will wrap this so there's no specific order
		coroutine.wrap(function()				
			local humanoid = npc:FindFirstChild("Humanoid")
			local rootPart = npc:FindFirstChild("HumanoidRootPart")

			-- Check if the NPC is a NPC
			if npc:IsA("Model") and humanoid and rootPart then
				-- Path find
				while npc do
					local sucess = false				

					if humanoid.Health ~= 0 then

						local current_target, current_Distance = getClosestPlayer(npc, rootPart)

						if current_target and current_Distance then
							sucess = newPath(npc, current_target, current_Distance)
						end
					end

					if not sucess then -- We do this just in case the path fails so the everything doesn't crash
						task.wait()	
					end
				end
			end
		end)()
	end
end

--// Loop
loopNPC()
npcFolder.ChildAdded:Connect(loopNPC)

If you want more information how it works I recommend checking the github for simple path!

So perfect! The npc runs smoothly and we have one script controlling all NPCs, perfect! Or is it? we can take it one step farther.

Client NPCs

Instead of having all the NPCs run on the server which can be extremely laggy especially if there’s lots of NPCs, why not run it on the client?

Instructions:
Basically copy the whole script and put it in the client. Make sure to move all your modules into Replicated Storage! Modules like Simple Path, Fastcast, and Raycasthitbox works on the client so no worries!

If you want more information I recommend this awesome video created by @5uphi.

Well that’s all for now! Have fun coding and good luck developers!
– Rapideed/mang

26 Likes

Nice tutorial! Very informative and interesting. Are you going to make this a series btw?

I disagree :sob:

3 Likes

Sorry no, I won’t. But, I will update this post as much as I can!

4 Likes

Cool tutorial you have there! I just have some small suggestions for the code:


You only need to run CreatePath() once, you can just keep on overriding the old path with ComputeAsync(). So you can just put this line somewhere on top of the script.

Not really sure why you need to use a BindableEvent for this, can’t you just add the attack code as a function and run it instead of firing an event?

Now you don’t really have to do what I say, but task.wait() is the same as Heartbeat:Wait(). It’s easier to just type out task.wait() instead of having to get the RunService service and type out Heartbeat:Wait().


Other than that, this is a nice community tutorial. Good job.

4 Likes

Thanks for the feedback! Also I didn’t do one code because it’s more tedious to copy and paste a sword script and so the script isn’t too long.

Also I’ll edit the script. :+1:

2 Likes

It’s a matter of whether the coding will be tediously-long or not.

2 Likes

Finally, someone who understands me! :slightly_smiling_face:

Anyway, this tutorial is really good.

Unlike a lot of tutorials, yours really goes in depth to what the code does.

10/10

3 Likes