How to Make a Working NPC in Roblox [UPDATE 6/16/20]

[DISCLAIMER] This topic is my first tutorial, so please expect errors and please correct those errors in the comments. Also, please be aware this topic is not recommended for beginners, so please take this note into consideration.

[VERSION THREE POSTPONED]- Due to ill-timed vacation, I will be postponing version three for July. Please be considerate and understand that I did this for quality purposes of this topic.

Thank you!

NPC’s is one of the most important feature of a game. It is often used in games for enemies, or even allies. NPCs is a great feature to have, but how do you make such, and what is NPC anyway? This topic will explain how NPCs works and how to make a reliable NPC system through levels of complexity. I will be updating this topic every week to fix errors in my topic, or to add to my topic.

Definition

NPC stands for Non Playable Character. It is used to guide the player’s journey into a game, or try to slow down his journey throught the game as an enemy or an allie. For example, in a game called Portal, there is a level which the player has to come across Turrets to finish the level, in which the turrets fire anything that moves in they’re perspective. Now that you’ve got the basic idea of an NPC, let’s make one!

Making the NPC's brain

Before we go into Studio, because we’re dealing with advanced scripting knowledge, it’s best if we design the system first. For this topic, we’ll be designing four versions, depending on the level of complexity(Version One: Chase, Version Two: Pathfinding, Version Three: AI Vision). With that in mind, let’s get started!

Version One: Chase

For design one, we’ll be going through the most beginning step in using NPCs in Roblox. For now, we’ll be analyzing the following designs, then we’ll make it in Studio.


-Distance Check Design

-Chase Design

Analysis: Distance Check Design

In this design, we have three dots, being NPC as yellow, Player 1 as pink, and Player 2 as blue. The AI of the NPC is performing a task to see which person is closer. The design shows that the distance between the NPC and Player 1 is 10 studs, while the distance between the NPC and Player 2 is 20 studs, so we already know that Player 1 is closer.

Analysis: Chase Design

Now that we know that Player 1 is closer to the NPC, how do we make the NPC walk to the player? Well, it’s rather simple… we make the NPC walk to the player’s HumanoidRootPart, which the blue point represents.

Making in Studio

Now that we got all we need designed, we’re ready to work on Studio. This will a step by step process.

  1. Get a test humanoid. You can get it by finding the Rig Builder in your plugins tab
  2. Make sure every limb of the humanoid is UNANCHORED because it will be stuck in place if it isn’t.
  3. Make a script inside the NPC’s model. This script will cover the AI of the NPC.

Now we will be scripting the Humanoid to do exactly what we designed.

Scripting

We need to script two functions that can check the distances of every players between it’s HumanoidRootPart and the NPC and chase whoever is the closest.

First we need to label our variables…

  --doyouevenbruh33--                                             
  NPC = script.Parent;
  players = game:GetService("Players"); -- it is optional to use game.Players, but since we're generalizing, it is best to do game:GetService() in this situation.
  game.Loaded:Connect(function()
       startpoint = {players:GetPlayers()[1], workspace:WaitForChild(players:GetPlayers()[1].Name)};
  end)

The NPC variable is to locate the NPC when we want it to do something or when we compare distances between the players

The players variable is to get the players to compare distances between one and the NPC

Now that we have have labelled our variables, we are ready to write the function.

function CheckDistance()
        local closestpoint = nil;
        while closestpoint == nil do
        for i, v in pairs(players:GetPlayers()) do
	             local distance = math.abs(workspace:WaitForChild(v.Name).HumanoidRootPart.Position.Magnitude - NPC.HumanoidRootPart.Position.Magnitude)
	              if closestpoint == nil then
			            closestpoint = {v, distance};
		        else
			             if distance < closestpoint[2] then
				                 closestpoint = {v,distance};
			             end
		           end
	     end
        wait();
        end
        return closestpoint[1];
end

In this function, we made a local variable(closestpoint) to add two data pieces in it, { The player itself, and the absolute distance between the player and the NPC.} and because it’s nil, we want to add something to it, and it will execute a for loop that checks every player to see which is the closest. If a player is closer than what the closestpoint variable say, then it updates the variable to the closer player, then we returned the function with the closest player so we can use that to make the NPC chase the player.

Now that we got the closest player, we’re ready to make the NPC chase the player.

 function Chase(player)
         if player ~= nil then
                 NPC.Humanoid:MoveTo(player.Character.HumanoidRootPart.Position);
         end
 end

For more info on moving NPC’s between points, please visit this link:
Moving NPCs Between Points

Remember where we returned the closest player in the DistanceCheck function for a certain reason… this is that. We made sure that the player argument wasn’t nil because at the CheckDistance function, we made the closestpoint variable a nil at the beginning. if it’s not nil, then we made the NPC walk towards the player’s HumanoidRootPart, which is the player’s character’s Primary Part. If it’s nil, then it would carry along with the next set of code.

We’ve made our functions, but how do we call them?

while true do
local closest_player = CheckDistance();
if closest_player then
	Chase(closest_player)
end
end

We made a while loop that calls the function in a variable so we can store the function’s return value and we made an if statement to check if the function’s return value was nil, if not then it runs the chase function.

We’re done with the first version, now let’s test it to see how it works!

It is completely optional to convert the functions into a BindableFunction instead of a single script.

Testing & Feedback
Solo Playing

If we play it, The NPC should almost immediately be walking towards us. So as a starter, it’s already working.

https://gyazo.com/ecb9ecba8a8434573bfa95f24608bd68

But, we have two problems. The NPC can’t jump and the NPC is unaware that there are obsticles near by. This could be problem, let’s see how.

https://gyazo.com/994a1d20f8acf6314d93ea49aed0717f

As we can see, the NPC does not know when to jump, thus causing a problem. Let’s see how the NPC reacts to walls, or parts blocking the vision of the player.

https://gyazo.com/824908575a982af783493d0311d2b965

Because the NPC does not know there’s a wall, the NPC keeps running into the wall.

Server Playing

https://gyazo.com/c891ad12f84a3096dfe75eb803fb960f

It appears to work! :smiley:

Feedback

The NPC did walk to the closest player, and even in a multiplayer server, the NPC still did well, but it can’t jump and it has certain trouble with obstacles like walls.

Version Two:Pathfinding

Like last week, we’ll be looking at design(s), then making it in Roblox Studio.


-Pathfinding Design

"Pathfinding Design- Analysis

In the design, the NPC is chasing the player, but not in a way like last week. Because the player is
hiding behind a wall on the other side of the NPC, we need to make the NPC know how to get to the player, so the NPC follow these points that lead to the player without the NPC running into walls.

Making in Studio

I made some changes to my place by adding BindableFunctions instead of using a single script. I also added the NPC in a different location[For more info, please visit the OPEN-SOURCED Example]. Like last week, this will be a step by step process.

  1. If you haven’t, convert the functions in the script to bindable functions to make it more easier for this lesson.
  2. Make a BindableFunction.
  3. Make a script inside of the BindableFunction.
Scripting the Pathway

We need the new BindableFunction to calculate pathways in which the player can follow. In the script, we need to add our variables.

  --doyouevenbruh33--
  NPC = script.Parent.Parent -- addresses the NPC
  PathfindingService = game:GetService("PathfindingService") -- this is the main variable to calculate paths

Now that we’ve got the variables, let’s add the function.

 function InitPathway(closest_player)
	local path = PathfindingService:CreatePath()
	path:ComputeAsync(NPC.HumanoidRootPart.Position, 
    player.Character.HumanoidRootPart.Position)
	local waypoints = path:GetWaypoints()
	return waypoints
end

In this function, the closest_player has to be the argument for the function. We made a variable containing the path, but the path is empty, so we did path:ComputeAsync... because we needed to add something to it. In the ComputeAsync the two arguments represent start to finish, it starts from the NPC’s HumanoidRootPart to the closest_player’s HumanoidRootPart. Now that we got the path created, we need to get the waypoints that leads to the player, so we made a variable that holds the waypoints. [Keep in mind, the waypoints are represented in a table] We returned the waypoints so it could go between BindableFunctions. So far, we doing great, but we need to call the function each time the BindableFunction gets invoked.

 script.Parent.OnInvoke = InitPathway

For more info on Pathfinding, please visit these links

Character Pathfinding
PathfindingService

Cut to the Chase

We need to change the chase script because it currently chase to the player without using the pathway, so we need to change that. Go to your chase script in the Chase BindableFunction.

 --doyouevenbruh33--
 NPC = script.Parent.Parent;

 function Chase(pathway)
     if pathway then
	      for i, v  in pairs(pathway) do
		      NPC.Humanoid:MoveTo(v.Position)
		      if v.Action == Enum.PathWaypointAction.Jump then
			       NPC.Humanoid.Jump = true
		     end	
		     NPC.Humanoid.MoveToFinished:Wait()
	       end
      end
 end

 script.Parent.OnInvoke = Chase

In the function, we need the pathway instead of the player like last week because we need the NPC to chase the points of the pathway. We made an if statement to check if the pathway is there when it was called. If it was there, we made a for loop of all the points, making the NPC walk to each point and check if the player needed to jump when walking to each point, and it waits til the NPC’s done walking to each point. Like the ‘InitPathway’ BindableFunction, we called the function when the Chase BindableFunction is Invoked.

Main Script

We have our BindableFunctions, but we need to call them. Just like last week, we’re gonna make a while loop that calls each function and checks to see if the function returns anything, then proceeds. To do such, make another script in the NPC and write this piece of code:

 while true do
      local player = script.Parent.CheckDistance:Invoke()
      if player then
	       local pathway = script.Parent.InitPathway:Invoke(player)
	       if pathway then
		        script.Parent.Chase:Invoke(pathway)
	       end
      end
    end
"Testing & Feedback

If you play my Open-Sourced Example, you can see that the NPC does jump and walk past obstacles.
https://gyazo.com/9ffb3cb390f57a172e5a55b5ffd7c729

But there are some problems. There are times where the NPC cannot climb trusses, but sometimes they do.

https://gyazo.com/e4147fd3dd1f492ba6c0313719435375

The two other problems is sometimes, the NPC stops where it’s at and waits for like 30 seconds before walking, and the pathfindingservice is very blocky and not smooth, but other than that, it is fine.

Version Three: AI Vision

Coming in June!

Conclusion

Coming in June!

To see my open-sourced example, click here!

[NPC MODELS] - I have made models of the npc, depending on their version.

Version One - NPCVersionOne - Roblox
Version Two - NPCVersionTwo - Roblox

40 Likes

Great tutorial! I can see you put a lot of time into making this. Note that this isn’t actually AI because AI is a self learning algorithm that is much more complicated. Anyways, keep up this good work! I look forward to Version 2 already :smiley:

4 Likes

Loads of information injected into my brain! I have a question.

How would I go on about adding the default animations?

2 Likes

I didn’t add the animations to it. Just take the local script from your player from studio, delete the player.Chatted parts to it, and copy the code onto a script and place it into the NPC.

3 Likes

I’ve removed the tags from your title–that’s what the “optional tags” section is for. :slight_smile:

2 Likes

Wouldn’t this error?

Actually, it did error for me.

15:59:30.683 - HumanoidRootPart is not a valid member of Humanoid

2 Likes

oh sorry. I meant inside the NPC, not the humanoid object.

2 Likes

sorry I corrected my error. 30chfhfkj

3 Likes

When you press play on studio, there is a model of your character with a localscript called “Animate”. Copy the code and on edit mode, make a script called “Animate” and put the code inside of there. Make sure to delete the player.Chatted stuff cause it’s not local anymore.

1 Like

What is the type of script?
[30 char]

wdym? 30chars s a kfdkgjhlashfsjlkhdjk

Nevermind, the issue got solved.

There are a few issues with the scripting side of things that I would like to address. As I have no experience in the other areas, I will leave comments on them to other, more experienced people.

In your CheckDistance function, there area few things that can be improved. Considering the fact that distance cannot be negative, you can get rid of that math.abs function call. In addition to that, you are unnecessarily using a table. It would be much simpler to merely do

local closest_player, smallest_distance
for _, player in ipairs(players:GetPlayers()) do
     if smallest_distance then
          local distance = -- etc.
          if smallest_distance > distance then
               closest_player, smallest_distance = player, distance
          end
     else
          closest_player, smallest_distance = player, distance
     end
end
return closest_player

This gets rid of a lot of redundant nils and eliminates that entirely useless while loop.

The second issue is with your loop. First of all, your while loop will iterate forever if the CheckDistance function returns nil. That is logic that you never want to have in your game. I see that you’re trying to wait until it returns something else, but it just won’t because it can’t. Once that loop starts, it’s never going to end. Get rid of it. You’re better off with the simple logic of

while true do
     local closest_player = CheckDistance()
     if closest_player then
          -- chase them
     end
     -- etc.
end

The final two criticisms I have are on efficiency and your use of wait. It is better to use your own implementation of wait. Roblox’s is unreliable. More on that here.

As for efficiency, you’re barely yielding at all while constantly looping some rather cpu intensive code. Because of the way the magnitude of vectors is calculated (using square roots), you’re going to run into some efficiency problems with a bunch of such loops running in your game. There are two really good ways of resolving this. 1. Write your own distance calculator. 2. Use larger yield times. The second point is paramount. You don’t need to check every 0.033 seconds to see if a player is in range. This is especially true as the range of the npc increases. I suggest anywhere from 0.5 to 1.5 seconds depending on the size of your range. If immediate detection is extremely necessary, maybe it might be fine to go down to 0.25 or something. However, that is rarely the case. Be wise with your resources.

As for writing your own distance detection, it’s rather simple:

local function get_distance_squared(v_1, v_2)
     return (v_1.X - v_2.X)^2 + (v_1.Y - v_2.Y)^2 + (v_1.Z - v_2.Z)^2
end

This will allow you to calculate the square of the distance without any square roots. This does mean that you’ll have to take this into account when calculating whether the player is in range, but that’s a small and easy pre-run cost. For example, if you want the player to be within 20 studs before the NPC notices, just take the square of that (20^2 = 400), and that’s the distance (distance returned by the function I just provided, that is) you’ll need to be less than in order to be in range.

There are other things I could comment on such as your unnecessary use of non-localized variables and weird comments about GetService, but I thought I’d stick to the primary issues I see with this tutorial. Hope this helps to improve the tutorial!

6 Likes

Thanks for the suggestions. I will take those into considerations. :smiley:

I put the while loop in the check distance function because if I didn’t, it would make an error.

I remember putting tags in the “optional tags”. Strange…