How to make Roblox NPCs
Roblox NPCs make your game worlds more immersive and engaging. We can use them as teammates or as our enemies. If you build them right, they could possibly even feel life-like.
NPCs have evolved from following simple rules like in Pacman to having a range of behaviors such as characters in Elder Scrolls or Grand Theft Auto. The limits of what you can do are truly bound by your imagination.
Let’s learn what it takes to create NPCs.
What you’ll learn
- Using the finite state machine design pattern
- How to use CollectionService to control behaviors
- Create a chat dialog system
- NPC pathfinding
- Make NPCs surprise players by breaking typical patterns
Get your Roblox NPCs talking
To get your Roblox NPCs talking, we’ll use the Dialog and DialogChoice objects.
For the simplest method, we can use dialog chains directly in Roblox Studio without using Lua. This is great if your dialog is only text and doesn’t change the player’s abilities, attributes, or the world.
We must first create a Dialog object as a child of your NPC. The Dialog object starts off the conversation and prompts the player with choices. Now add a DialogChoice as a child of the dialog object. Finally, keep adding as many choices as you need.
To add your dialog text, update the Dialog.InitialPrompt property with the NPCs starting text.
Your dialog will follow a tree-like structure where the selected choice continues on to the next prompt and its choices.
Within each choice, update the ResponseDialog property to add your chosen text.
The Dialog.ConversationDistance property controls how far you can engage the conversation.
Change the Dialog.Purpose property with an icon to fit the purpose of the conversation. You can use an exclamation (!), question mark (?), or dollar symbol ($) as an icon.
Now change the color of your dialog boxes by changing the Dialog.Tone property.
Scripting your NPC dialog
Now that we have the dialog object structure, we can use the DialogChoiceSelected event to trigger different actions. First, we’ll need to set a name for each dialog choice so we can know which one was selected by the player.
Dialog choice selection script
-- add a local script as a child of the main Dialog object
local dialog = script.Parent
dialog.DialogChoiceSelected:connect(function(player, choice)
if choice.Name == "heal_player" then
player.Character.Humanoid.Health = 100
else if choice.Name == "give_medkit" then
-- update player’s inventory with a med kit item
end
end)
If multiple players have to engage in the conversation with the NPC, we can use the Dialog:GetCurrentPlayers function to get a list of all players currently using this dialog. This function will return a list of players that you can use the ipair function to loop through.
Follow the path
Roblox gives us the PathfindingService to calculate a route between two points. Behind the scenes, this service uses the A* algorithm.
Let’s first go over how this algorithm works to appreciate how much PathfindingService is doing for us.
The A* algorithm
The A* (pronounced A-star) algorithm is a search procedure to find the shortest path between two points. It uses a heuristic where each possible direction has a calculated cost.
Algorithm path grid
This algorithm uses a grid for each possible spot an NPC can move towards. We first define a grid that fits our map.
Each square in our grid has an associated cost to move towards it.
The lower the cost, the shorter the path.
If your world has full-range motion then each point has 8 possibilities:
top left, top, top right
left, right
bottom left, bottom, and bottom left directions.
The cost for moving diagonally gets cut in half since it’s quicker to move to the top right than to go up and then right.
Within our grid, we transform our world to determine the sections that are blocked from movement. Each grid position should correspond to the size of the controllable characters. For example, if your player character is 128 wide with a depth of 128 pixels, then that’s your grid square size.
So in a 256,000 by 256,000 pixel world, you’ll have a 200 by 200 grid to move around.
Now that we have our grid, we can calculate the cost for each potential path.
How to find the shortest path
We’ll first give each unblocked option a cost of 1 for all positions in the direction we’d like to move and the remaining options get a cost of 2.
With each iteration, we accumulate the path cost. Once we hit a roadblock, we’ll pick the next best option in the direction of our target and give it a cost of 1. Again, the remaining options get an additional cost of 2.
This works great if our starting position is in an open position where it’s not surrounded by blocked options.
Imagine your character is in a room where the target position is outside of the room opposite the farthest wall from the exit. Using this algorithm will first try to find a path going directly towards the target since it’s potentially the shortest.
Eventually, it will exhaust these obvious choices and find that the exit leads to the shortest path.
Luckily, PathFindingService does all of this for us!
Using PathfindingService
The PathfindingService creates a dynamic path for us that keeps up with changes as they happen. If the player strays from the path, it will reroute itself like your GPS app.
Once we have a path from our two points, a script can use its waypoints to give our NPC smaller trips to reach their destination. Each step of the way, the script can redirect the character when an upcoming path becomes blocked with an obstacle.
Waypoint example
local pathFindingService = game:GetService("PathfindingService")
local currentPosition = Vector3.new(10, 10, 10)
local targetPosition = Vector3.new(100, 10, 200)
local waypoints
local path = pathFindingService:CreatePath()
local function goToDestination(destination)
local success, error = pcall(function()
path:ComputeAsync(currentPosition, targetPosition)
end)
if success and path.Status == Enum.PathStatus.Success then
waypoints = path:GetWaypoints()
end
end
Using the Path:Blocked event, we can customize how our NPC reacts when they can not go any further.
Blocked example
local blocked
local nextWaypoint = 2 -- next position after start
local function goToDestination(destination)
local success, error = pcall(function()
path:ComputeAsync(currentPosition, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
waypoints = path:GetWaypoints()
end
blockedPath = path.Blocked:Connect(function(blockedWaypoint)
if blockedWaypoint > nextWaypoint then
blockedPath:Disconnect() -- path is blocked ahead, disconnect to stop movement
goToDestination(destination) -- re-compute path
end
end)
end
Notice that we’re using a recursive function (a function that calls itself) to recalculate the path when it becomes blocked.
Like we learned with the A* algorithm, we can define different costs for different types of materials in our world. For example, if characters can’t swim we’ll give water a huge cost while giving normal materials a low cost.
Material cost example
local pathFindingService = game:GetService(“PathfindingService”)
local path = pathFindingService:CreatePath({
Costs = {
Water = math.huge,
Stone = 0,
Slime = 5
}
})
In Lua, math.huge represents infinity.
For each material, we’ll use the name that’s used in our terrain or parts.
We can also modify our path constraints by defining the minimal spacing needed to move horizontally or vertically. You can also control if a path allows a character to jump.
You can also set the path costs for a part without using the material name by using PathfindingModifier. Within a part of your world, add a PathfindingModifier node as a child. Now update the ModifierId with a useful name such as KillZone and update the Costs variable in the path with this name.
Path constraints example
local pathFindingService = game:GetService(“PathfindingService”)
local path = pathFindingService:CreatePath({
AgentRadius = 4, -- minimum horizontal space,
AgentHeight = 10, -- minimum vertical space,
AgentCanJump = false, -- NPC can not jump
})
Immersive Roblox NPCs
NPC AI
Similar to how we can control event sequences, we can use the Finite State Machine design pattern to build our NPCs behaviors. Check this post out to learn more about how Finite State Machines work.
I’d recommend using the Configuration object to store NPC properties instead of within a script. This is a good example of separating object data from object logic.
The Configuration object properties can include the list of NPC states and any shared properties between all your NPCs. That way, if an NPC needs to change we only need to change it within its properties and not in a script.
We’ll use CollectionService to tag objects and NPCs to control which behaviors they can use.
Within each NPC state, we’ll use Roblox CFrames and Region3 type to calculate the distance of the NPC from other characters.
How to find other characters or NPCs to interact with
We’ll use the Workspace:FindPartsInRegion3 function to get a list of parts within a given part of the world. A Region3 object is a data type that defines a volume of 3d space.
local Workspace = game:GetService("Workspace")
-- searches for characters within a box using the character’s position as the center
local function findCharacters(character, searchRadius)
local center = character.Humanoid.Position
local topCorner = center + Vector3.new(searchRadius, searchRadius, searchRadius)
local bottomCorner = center + Vector3.new(-searchRadius, -searchRadius, -searchRadius)
local region = Region3.new(bottomCorner, topCorner)
local partsFound = Workspace:FindPartsInRegion3(region, nil, math.huge)
return partsFound
end
NPC Finite State Machine
We’ll use a Script object as a child of the NPC model to control its behaviors. The NPC will keep its script active by using the RunService:Heartbeat event that runs each frame tick.
Also, as mentioned before we’ll use a Configuration object to manage the NPC’s properties.
These are the different types that you can use in a Configuration object.
Let’s define the different states the NPC will switch between.
local RunService = game:GetService("RunService")
local Configuration = script.Parent:WaitForChild("Configuration")
local NpcStates = {
Idle = 1,
Attacking = 2,
Hiding = 3,
Talking = 4,
Dead = 5
}
Here are Configuration object values we can set directly within Roblox Studio.
local SEARCH_RADIUS = configuration:FindFirstChild("SearchRadius")
local ATTACK_DAMAGE = configuration:FindFirstChild("AttackDamage")
local IDLE_DURATION = configuration:FindFirstChild("IdleDuration")
local ATTACK_DURATION = configuration:FindFirstChild("AttackDuration")
local HIDE_DURATION = configuration:FindFirstChild("HideDuration")
local DESTROY_DELAY = configuration:FindFirstChild("DestroyDelay")
Next, define the NPC object class.
local Npc = {}
function Npc:new()
local o = {}
o.Humanoid = script.Parent.Humanoid
o.State = NpcStates.Idle
o.Moving = false
o.HidingDuration = HIDE_DURATION
self.__index = self
return setmetatable(o, self)
end
function Npc:isAlive()
return self.Humanoid.Health > 0
end
These are the NPC state functions that run when they’re in that state.
function Npc:IdleState()
if self:isAlive() then
local charactersFound = findCharacters(script.Parent, SEARCH_RADIUS)
if #charactersFound > 0 then
self.State = NpcStates.Hiding
end
end
end
function Npc:Attack(character)
character.Humanoid.Health = character.Humanoid.Health - ATTACK_DAMAGE
end
function Npc:AttackingState()
if self:isAlive() then
-- attack first nearest character
local characters = findCharacters(script.Parent, SEARCH_RADIUS)
if #characters > 0 then
self:Attack(characters[0])
end
wait(ATTACK_DURATION)
self.State = NpcStates.Idle
end
end
function Npc:HidingState()
if self:isAlive() then
If not self.moving then
self.Moving = true
-- pick a random location and move character there
local randomPoint = script.Parent.Humanoid.Position + Vector3.new(math.random(10, 50), 0, math.random(10,50))
script.Parent.Humanoid:MoveTo(randomPoint)
wait(HIDE_DURATION)
self.Moving = false
self.State = NpcStates.Attacking
end
end
end
function Npc:TalkingState()
if self:isAlive() then
local dialog = self.Parent.Dialog
if dialog.InUse then
self.State = NpcStates.Idle
end
end
end
function Npc:DeadState()
wait(DESTROY_DELAY)
script.Parent:Destroy()
end
Finally, the Tick function invokes the NPC’s various state functions with each Heartbeat event from RunService.
function Npc:Tick()
if self.State == NpcStates.Idle then
self:IdleState()
else if self.State == NpcStates.Attacking then
self:AttackingState()
else if self.State == NpcStates.Hiding then
self:HidingState()
else if self.State == NpcStates.Talking then
self:TalkingState()
else if self.State == NpcStates.Dead then
self:DeadState()
end
end
local someNpc = Npc:new()
RunService.Heartbeat:Connect(function()
someNpc:Tick()
end)
As you can see this NPC starts off in an idle state but when a character gets within its search region it will switch to its hiding state. After a brief delay, the NPC will then attack the nearest character before returning to its idle state again.
Manage NPC behaviors easily with CollectionService
Within our NPCs finite-state functions we can use CollectionService to filter the characters found. For example, this is useful when we have multiple NPC types that interact with other NPC types for different reasons.
local CollectionService = game:GetService("CollectionService")
local function findTaggedCharacters(tag)
local taggedCharacters = {}
local characters = findCharacters(script.Parent, SEARCH_RADIUS)
for i, character in ipairs(characters)
if CollectionService:HasTag(character, tag) then
table.insert(taggedCharacters, character)
end
end
return taggedCharacters
end
local function Npc:AttackState()
local attackableCharacters = findTaggedCharacters(“Attackable”)
if #attackableCharacters > 0 then
self:Attack(attackableCharacters[0])
end
end
Make characters feel real and lifelike
To make our NPCs more real, we’ll use a mood system. The mood system can use a list of variables that change for different reasons and at various rates.
This system is just an approximation of course but introduces unexpected cycles in character behavior.
For example, an NPC can have moods such as angry, sad, hungry, or excited. Depending on the cycles in your game, i.e. 4 real hours is one game day, these moods will fluctuate over time.
Other in-game factors could keep an NPC in a certain mood for longer or shorter periods of time.
While these cycles may repeat, each character will express their moods differently.
Think of each mood as if they were separate waves in a pool. At one part of the pool, the waves appear very smooth, other parts are wavier, and a few spots are chaotic and thrashing.
Only one of these moods will dominate as the primary mood at the moment. If one mood goes up then one or more other moods go down.
Some moods could also slightly contribute to other moods too. Imagine each mood pattern as if it were some sinusoidal wave. The character’s personality comes through with how these different moods interact with one another.
One character may frequently get more hungry when it reaches a certain level of excitement. A different character may become sadder if they eat too much.
Mood component functions
For our mood cycles, we’ll keep our input values between 0 to 1.
We can use a square wave function as our base. This is a function where from x = 0 to x=0.5, the output is 1 and from x = 0.5 to x = 1 the output is zero. Since the value is flat for half the cycle, the mood level stays the same. For the next half of the cycle, the mood has no impact.
To modify this, we can add additional sinusoidal functions such as sine or cosine to include gradual dips and valleys in the cycle.
External elements in the game will temporarily affect the different coefficients or multipliers used in these composite cycle functions.
Other implementation ideas
These are just some ideas that you could explore to modify this mood system concept.
What if instead of using a composition of wave functions, we combine cycles using Fourier transforms and feedback systems as controls?
How can we use cycles found in fractals?
What about using audio input to create these cycles?
What if Roblox NPCs have behaviors that are modeled from real player behaviors and evolve over time?
What’s next
You should now be able to create some fun Roblox NPCs in your game. Roblox is paving the way for the Metaverse and the future of gaming. The future needs characters that players can relate to and empathize with by making them feel more human.
Please consider joining my email newsletter for more updates or joining the conversation on Twitter!
Thank you for reading and stay curious!