NPCs breathe life into any game world:
- villagers with schedules
- merchants with stock
- guards who patrol
- hostile factions
- relationships
- dialogue that reacts to story flags
- simulation (hunger, work, needs)
- towns that evolve over time
Lua is perfect for simulation layers:
- lightweight logic
- hot reload
- easy-to-edit behavior packages
- clean data-driven content
- integrates with quests, story, and world triggers
This chapter builds:
- NPC definitions
- Behavior packages (wander, talk, patrol, work)
- Daily schedules (day/night cycles)
- Merchant system
- Factions & reputation
- Town simulation
- Social interactions
1. NPC Definitions (Data-Driven)
npcs/
villagers.lua
merchants.lua
guards.lua
1.1 Example NPC Definition
return {
elder = {
name = "Village Elder",
sprite = "npc_elder",
faction = "village",
behaviors = {"talk", "idle"},
dialogue = "elder_intro",
schedule = {
{time=0, state="sleep"},
{time=6, state="idle"},
{time=12, state="walk_square"},
{time=18, state="home"},
{time=22, state="sleep"},
}
},
}
NPC = data + behaviors + dialogue + schedule.
2. NPC Object & Behavior System
NPC behaviors are modular, like animation states.
2.1 NPC Object Class
local NPC = {}
NPC.__index = NPC
function NPC.new(data)
return setmetatable({
data = data,
x = 0, y = 0,
state = "idle",
schedule_index = 1,
}, NPC)
end
2.2 Behavior Packages
Stored in behaviors/.
wander.lua
return {
update = function(npc, dt)
npc.x = npc.x + math.random(-20,20) * dt
npc.y = npc.y + math.random(-20,20) * dt
end
}
talk.lua
return {
interact = function(npc, player)
DialogueSystem:start(npc.data.dialogue)
end
}
patrol.lua
return {
update = function(npc, dt)
-- follow waypoints
local wp = npc.data.waypoints[npc.waypoint_index]
npc:move_to(wp, dt)
end
}
All behaviors share a standard interface.
2.3 NPC Update
function NPC:update(dt)
local behaviors = self.data.behaviors
for _, b in ipairs(behaviors) do
local pkg = require("behaviors."..b)
if pkg.update then
pkg.update(self, dt)
end
end
end
3. Daily Schedules (Day/Night Simulation)
NPCs follow time-based routines.
3.1 Update Schedule Based on Time
function NPC:apply_schedule(hour)
for i = #self.data.schedule,1,-1 do
if hour >= self.data.schedule[i].time then
self.state = self.data.schedule[i].state
return
end
end
end
3.2 Day/Night Integration
Called every in-game minute:
for _, npc in pairs(NPC_LIST) do
npc:apply_schedule(WorldHour)
end
Common routines:
- villagers sleep at night
- guards patrol at night
- merchants open shops during day
- farmers work during morning
4. Merchant System (Economy Basics)
Defined with:
shops/
potions.lua
weapons.lua
4.1 Example Merchant Data
return {
items = {
{id="potion_small", price=20},
{id="potion_large", price=50},
},
restock_rate = 24, -- in-game hours
}
4.2 Merchant NPC Behavior
return {
interact = function(npc, player)
ShopUI:open(npc.data.shop_id)
end
}
4.3 Restocking
function Shop:restock(shop_data)
for _, item in ipairs(shop_data.items) do
item.stock = math.random(1,5)
end
end
Triggered by:
if WorldHour % shop.restock_rate == 0 then
Shop:restock(shop_data)
end
5. Factions & Reputation
Faction examples:
- village
- bandits
- merchants
- kingdom
- demons
- wilderness creatures
5.1 Faction Data
factions = {
village = {name="Village", hostility=0},
bandits = {name="Bandits", hostility=1},
}
5.2 Reputation System
local Reputation = {}
Reputation.__index = Reputation
function Reputation.new()
return setmetatable({rep={}}, Reputation)
end
function Reputation:add(faction, amount)
self.rep[faction] = (self.rep[faction] or 0) + amount
end
return Reputation
5.3 NPC Reaction Based on Reputation
function NPC:reaction(player)
local rep = player.reputation.rep[self.data.faction] or 0
if rep < -10 then
return "hostile"
elseif rep < 0 then
return "unfriendly"
else
return "neutral"
end
end
6. Guard & Aggro Behavior (AI Combat)
If faction is hostile, NPC attacks.
if npc:reaction(player) == "hostile" then
npc.state = "attack"
end
Attack behavior:
return {
update=function(npc, dt)
local dx = player.x - npc.x
local dy = player.y - npc.y
local d = math.sqrt(dx*dx + dy*dy)
if d < 50 then
player:take_damage(5)
else
npc.x = npc.x + dx/d * dt * 50
npc.y = npc.y + dy/d * dt * 50
end
end
}
7. Town Simulation (Lightweight Economy)
Simple simulation:
- villagers produce goods
- merchants consume goods
- food supply changes with population
- shops restock based on supply
7.1 Town Resource Data
Town = {
food = 100,
goods = 20,
population = 12,
}
7.2 Daily Update
function Town:update_daily()
self.food = self.food - self.population * 2
self.goods = self.goods + math.random(0,2)
if self.food < 0 then
event:emit("town_starving")
end
end
Called every 24 in-game hours.
8. NPC Social Interactions
NPCs interact with each other:
- greetings
- gossip
- sharing rumors
- giving quest hints
- reacting to player reputation
This is lightweight but adds life.
8.1 NPC Gossip System
function NPC:gossip(other)
if math.random() < 0.1 then
-- trade rumor
local rumor = "weather_rain" -- example
npc:learn_rumor(rumor)
end
end
8.2 NPC-to-NPC Interaction Update
for _, a in ipairs(NPC_LIST) do
for _, b in ipairs(NPC_LIST) do
if a ~= b and distance(a, b) < 40 then
a:gossip(b)
end
end
end
9. Story Integration
NPCs react to story flags.
9.1 Dialogue Changes
if quest_manager.flags["main_01"] then
npc.dialogue = "elder_after_quest"
end
9.2 World State Changes
if Town.food < 10 then
spawn("starving_villager", 200, 200)
end
10. Putting It All Together โ Example Village Simulation
-- daily town tick
Town:update_daily()
-- update NPC behaviors
for _, npc in pairs(NPC_LIST) do
npc:apply_schedule(WorldHour)
npc:update(dt)
end
-- social interactions
NPCSocial:update(dt)
-- reactions to player
for _, npc in pairs(NPC_LIST) do
if npc:reaction(player) == "hostile" then
npc.state = "attack"
end
end
This simulates a living village.
11. Summary of Chapter 13
You now understand:
- NPC definition & modular behavior system
- Daily routines and schedule-driven AI
- Merchant system & restocking
- Town economy simulation
- Faction and reputation logic
- Hostile/neutral/friendly reactions
- NPC-to-NPC interactions
- Story integration
This system supports:
- RPG villages
- Survival colony sims
- Open-world games
- Town building games
- Strategy games with civilian life
Lua handles these simulation layers elegantly.
Keep Reading
Follow the engineering thread
Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.