how to make roblox npcs

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.

roblox npc dialog

To add your dialog text, update the Dialog.InitialPrompt property with the NPCs starting text.

roblox dialog properties

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.

npc dialog in action

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.

roblox pathfinding

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. 

A* path algorithm grid

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.

roblox configuration value types

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
lifelike npcs

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!

Leave a Reply