[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.
- Get a test humanoid. You can get it by finding the Rig Builder in your plugins tab
- Make sure every limb of the humanoid is UNANCHORED because it will be stuck in place if it isn’t.
- 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.
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.
- If you haven’t, convert the functions in the script to bindable functions to make it more easier for this lesson.
- Make a BindableFunction.
- 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
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