Hello devforum! I am currently using an open source NPC system for my game which can be found by clicking here however good this system is, the npc’s it creates just dont move (see Image below:)
My question is why is this happening, please see the code below:
local pathfindingService = game:GetService("PathfindingService")
local insertService = game:GetService("InsertService")
local physicsService = game:GetService("PhysicsService")
local RbxScriptSignal = require(script.RbxScriptSignal)
local config = require(script.Configuration)
local fastWait = require(script.FastWait)
local pathfindableAreas = collectionService:GetTagged("pathfindable")
local showMarkers = game:GetService("RunService"):IsStudio()
local function waitUntilTimeout(event, timeout)
local signal = RbxScriptSignal.CreateSignal()
local conn = nil
conn = event:Connect(function(...)
conn:Disconnect()
signal:fire(...)
end)
delay(timeout, function()
if (conn ~= nil) then
conn:Disconnect()
conn = nil
signal:fire(nil)
end
end)
return signal:wait()
end
local function getRandom(tbl)
return tbl[math.random(1, #tbl)]
end
-- crude table concatenation
local function concatTables(t1, t2)
local tmp = {}
for _, v in pairs(t1) do
table.insert(tmp, v)
end
for _, v in pairs(t2) do
table.insert(tmp, v)
end
return tmp
end
local function getRandomInPart(part)
local random = Random.new()
local randomCFrame = part.CFrame * CFrame.new(random:NextNumber(-part.Size.X/2,part.Size.X/2), random:NextNumber(-part.Size.Y/2,part.Size.Y/2), random:NextNumber(-part.Size.Z/2,part.Size.Z/2))
return randomCFrame
end
local usedBrickColors = {}
local function controlNpc(npc)
local markerColor
repeat
markerColor = BrickColor.random()
until usedBrickColors[markerColor] == nil
usedBrickColors[markerColor] = true
local path = pathfindingService:CreatePath({ AgentCanJump = false, AgentRadius = 5 })
local function log(logType, message)
if config.logging then logType("[NPC{" .. npc.Name .. "}] " .. message) end
end
return function()
log(print, "NPC controller activated")
while fastWait(math.random(0, config.maxDawdlingTime)) do
local nextPathfindableArea = getRandom(pathfindableAreas)
local nextTarget = getRandomInPart(nextPathfindableArea)
local pos = nextTarget.X .. "," .. nextTarget.Y .. "," .. nextTarget.Z
local markers = {}
log(print, "New target -> " .. pos)
path:ComputeAsync(npc.PrimaryPart.Position, nextTarget.Position)
log(print, "Calculated new path")
local waypoints = path:GetWaypoints()
--if showMarkers then
for _, waypoint in pairs(waypoints) do
local marker = Instance.new("Part")
marker.Shape = "Ball"
marker.Material = "Neon"
marker.BrickColor = markerColor
marker.Size = Vector3.new(0.6, 0.6, 0.6)
marker.Position = waypoint.Position
marker.Anchored = true
marker.CanCollide = false
marker.Parent = game.Workspace
table.insert(markers, marker)
end
--end
for i, waypoint in pairs(waypoints) do
npc.Humanoid:MoveTo(waypoint.Position)
if waitUntilTimeout(npc.Humanoid.MoveToFinished, config.movementTimeout) == nil then
log(warn, "Timed out trying to reach path, stopping")
npc.Humanoid:MoveTo(npc.PrimaryPart.Position) -- cancel any pending movement
if showMarkers then
for _, marker in pairs(markers) do
marker:Destroy()
end
end
break
else
if showMarkers then
markers[i]:Destroy()
end
end
end
end
end
end
physicsService:CreateCollisionGroup("players")
physicsService:CreateCollisionGroup("npcs")
if not config.canCollideWithPlayers then
physicsService:CollisionGroupSetCollidable("players", "npcs", false)
end
if not config.canCollideWithNPCs then
physicsService:CollisionGroupSetCollidable("npcs", "npcs", false)
end
game.Players.PlayerAdded:Connect(function(player)
player.CharacterAppearanceLoaded:Connect(function(char)
for _, v in pairs(char:GetChildren()) do
if v:IsA("BasePart") then
physicsService:SetPartCollisionGroup(v, "players")
end
end
end)
end)
local npcAssets = require(script.Assets) -- this takes time to prep assets
for i = 1, config.count, 1 do
local npc = script.Template:Clone()
npc.Name = config.nameGenerator(i)
npc.Parent = game.Workspace
for _, v in pairs(npc:GetChildren()) do
if v:IsA("BasePart") then
physicsService:SetPartCollisionGroup(v, "npcs")
end
end
local discriminator = getRandom(config.discriminators)
local face = getRandom(concatTables(npcAssets.faces.all, npcAssets.faces[discriminator]))
npc.Head.face.Texture = face
local clothes = getRandom(concatTables(npcAssets.clothes.all, npcAssets.clothes[discriminator]))
npc.Shirt.ShirtTemplate = clothes[1]
npc.Pants.PantsTemplate = clothes[2]
local hair = getRandom(concatTables(npcAssets.hair.all, npcAssets.hair[discriminator]))
if hair ~= -1 then
npc.Humanoid:AddAccessory(hair:Clone())
end
local accessory = getRandom(concatTables(npcAssets.accessories.all, npcAssets.accessories[discriminator]))
if accessory ~= -1 then
npc.Humanoid:AddAccessory(accessory:Clone())
end
npc.PrimaryPart:SetNetworkOwner(nil)
local skinTone = getRandom(concatTables(npcAssets.skinTone.all, npcAssets.skinTone[discriminator]))
npc["Body Colors"].HeadColor = skinTone
npc["Body Colors"].LeftArmColor = skinTone
npc["Body Colors"].LeftLegColor = skinTone
npc["Body Colors"].RightArmColor = skinTone
npc["Body Colors"].RightLegColor = skinTone
npc["Body Colors"].TorsoColor = skinTone
local animScript = script.Animation:Clone()
animScript.Parent = npc
animScript.Disabled = false
npc:SetPrimaryPartCFrame(config.origin)
coroutine.resume(
coroutine.create(
controlNpc(npc)
)
)
fastWait() -- yield, otherwise it may crash the game or exhaust script execution time
end
Other Code:
Settings Modules
-- Justice Configuration --
---------------------------
return {
count = 40, -- The amount of NPCs you want to generate
movementTimeout = 3, -- How long the NPCs can be stuck for before they give up and go to another target
maxDawdlingTime = 5, -- How long the NPCs will dawdle in an area before they select a new target
canCollideWithNPCs = false, -- If NPCs can collide with each other
canCollideWithPlayers = false, -- If NPCs can collide with players
discriminators = { "male", "female" }, -- How NPCs will be unique (e.g. gender, race, etc.)
origin = CFrame.new(161.4, 108.968, 127.3), -- The spawnpoint of the NPCs
logging = true, -- If logging should be enabled.
nameGenerator = function(i) -- The function used to generate names for the NPCs.
return "NPC " .. i
end,
}
Assets Module
-- Assets for NPC randomization --
----------------------------------
local insertService = game:GetService("InsertService")
-- assets for generating npcs
-- use catalog ids
local assets = {
faces = {
all = {
20418658, -- Eer...
20722130, -- Shiny Teeth
26424808, -- Know-It-All Grin
226217449, -- Laughing Fun
244160766, -- Just Trouble
31117267, -- Skeptic
20337343, -- Disbelief
23932048, -- Awkward....
209994929, -- Suspicious
209995366, -- Joyful Smile
141728790, -- Tired Face
236399287, -- Happy Wink
},
male = {
141728899, -- Drill Sergeant
255827175, -- Serious Scar Face
209994783, -- Raig Face
277950647, -- Furious George
398675764, -- Nouveau George
},
female = {
209994875, -- Smiling Girl
334656210, -- Miss Scarlet
416846300, -- Anime Surprise
280988698, -- Super Happy Joy
}
},
clothes = {
-- { shirt, pants }
all = {
{ 2726208440, 2726208973 }, -- Wick
{ 2966670306, 2966672022 }, -- Bryce
{ 292025047, 292632277 }, -- White Tuxedo
{ 268437111, 268437154 }, -- The Businessman
{ 289792371, 289792420 }, -- The Private Contractor
{ 703050484, 703050785 }, -- The Peacoat
{ 6254472431, 6264952708 }, -- Williams Suit
{ 6254471921, 6264952194 }, -- Wayne Suit
{ 6254471499, 6264951522 }, -- Victor Madrazzo Suit
{ 6254469455, 6264946838 }, -- Thomas Suit
},
male = {},
female = {
{ 6322087764, 6322092635 } -- Secretary (it's Pandemonica from Helltaker)
}
},
hair = {
all = {},
male = {
32278814, -- Trecky Hair
13477818, -- Normal Boy Hair
80922374, -- Chestnut Spikes
26658141, -- Messy Hair
62743701, -- Stylish Brown Hair
4875445470, -- Black Short Parted Hair
5644883846, -- Cool Boy Hair
5921587347, -- Brown Curly Hair For Amazing People
6310032618, -- Black Messy Side Part
6128248269, -- Black Mullet
5461545832, -- Blonde Messy Wavy Hair
6026462825, -- Cool Boy Brown Hair
323476364, -- Brown Scene Hair
4735347390, -- Brown Floof Hair
6187500468, -- Brown Mullet
},
female = {
5890690147, -- Popstar Hair
5897464879, -- Blonde Popstar Hair
5945436918, -- Light Brown Ethereal Hairstyle
5945433814, -- Blonde Ethereal Hairstyle
6066575453, -- Curly iconic hair for iconic people in blonde
6309005259, -- HollywoodLocks in Pink Ombre
6188729655, -- Blonde Adorable Braided Hair
}
},
accessories = {
all = {
-1, -- special one so that some NPCs just don't get accessories for variety
4507911797, -- Sleek Tactical Shades
11884330, -- Nerd Glasses
22070802, -- Secret Kid Wizard Glasses
74970669, -- Eyepatch
20642008, -- Bandit
5728016218, -- White Sponge Mask
4143016822, -- Vintage Glasses
5891250919, -- Rosey Gold Vintage Glasses
4545294588, -- Sleek Vintage Glasses
4258680288, -- Black Aesthetical Glasses
4965516845, -- Transfer Student Glasses
},
male = {
158066137, -- Andrew's Beard
987022351, -- Master of Disguise Mustache
4940496302, -- Full Brown Stubble
4995497755, -- Stubble Beard
4315331611, -- Imperial Beard
5700473228, -- Sponge Mask - Male Star
},
female = {
4300266038, -- Earring Hoops
6054184925, -- Elegant Low Hair Bow White
5318235356, -- White Aesthetic Headband
6305581728, -- Pink Heart Lollipop
}
},
skinTone = {
all = {
BrickColor.new("Light orange"),
BrickColor.new("Dark orange"),
BrickColor.new("Burnt Sienna"),
BrickColor.new("Pastel brown"),
BrickColor.new("Reddish brown"),
BrickColor.new("Dirt brown"),
BrickColor.new("Pastel yellow"),
BrickColor.new("Bright orange"),
},
male = {},
female = {}
}
}
-- prep assets and return proper ids
-- this will load in the assets with InsertService, get the appropriate IDs/objs needed,
-- and destroy the inserted objects for performance reasons (if possible)
local start = tick()
print("Prepping NPC assets")
local tmp = {
faces = {},
clothes = {},
hair = {},
accessories = {},
skinTone = assets.skinTone
}
local assetCount = 0
for discriminator, faces in pairs(assets.faces) do
tmp.faces[discriminator] = {}
for _, face in pairs(faces) do
local obj = insertService:LoadAsset(face)
table.insert(tmp.faces[discriminator], obj.face.Texture)
obj:Destroy()
assetCount = assetCount + 1
end
end
print("Faces prepped (at " .. tick() - start .. "s)")
for discriminator, clothesPairs in pairs(assets.clothes) do
tmp.clothes[discriminator] = {}
for _, clothesPair in pairs(clothesPairs) do
local obj1 = insertService:LoadAsset(clothesPair[1])
local obj2 = insertService:LoadAsset(clothesPair[2])
local clothes = {}
clothes[1] = obj1.Shirt.ShirtTemplate
clothes[2] = obj2.Pants.PantsTemplate
table.insert(tmp.clothes[discriminator], clothes)
obj1:Destroy()
obj2:Destroy()
assetCount = assetCount + 2
end
end
print("Clothes prepped (at " .. tick() - start .. "s)")
for discriminator, hairs in pairs(assets.hair) do
tmp.hair[discriminator] = {}
for _, hair in pairs(hairs) do
if hair == -1 then
table.insert(tmp.hair[discriminator], -1)
continue
end
table.insert(tmp.hair[discriminator], insertService:LoadAsset(hair):GetChildren()[1])
assetCount = assetCount + 1
end
end
print("Hair prepped (at " .. tick() - start .. "s)")
for discriminator, accessories in pairs(assets.accessories) do
tmp.accessories[discriminator] = {}
for _, accessory in pairs(accessories) do
if accessory == -1 then
table.insert(tmp.accessories[discriminator], -1)
continue
end
table.insert(tmp.accessories[discriminator], insertService:LoadAsset(accessory):GetChildren()[1])
assetCount = assetCount + 1
end
end
print("Accessories prepped (at " .. tick() - start .. "s)")
print("Done (prepped " .. assetCount .. " assets, took " .. tick() - start .. "s)")
return tmp```
Fast Wait Code
-- CloneTrooper1019 / MaximumADHD
local RunService = game:GetService("RunService")
local threads = {}
RunService.Stepped:Connect(function ()
local now = tick()
local resumePool
for thread, resumeTime in pairs(threads) do
-- Resume if we're reasonably close enough.
local diff = (resumeTime - now)
if diff < 0.005 then
if not resumePool then
resumePool = {}
end
table.insert(resumePool, thread)
end
end
if resumePool then
for _,thread in pairs(resumePool) do
threads[thread] = nil
coroutine.resume(thread, now)
end
end
end)
local function fastWait(t)
local t = tonumber(t) or 1 / 30
local start = tick()
local thread = coroutine.running()
threads[thread] = start + t
-- Wait for the thread to resume.
local now = coroutine.yield()
return now - start, elapsedTime()
end
return fastWait
RbxScriptSignal
local t = {}
function t.CreateSignal()
local this = {}
local mBindableEvent = Instance.new('BindableEvent')
local mAllCns = {} --all connection objects returned by mBindableEvent::connect
--main functions
function this:connect(func)
if self ~= this then error("connect must be called with `:`, not `.`", 2) end
if type(func) ~= 'function' then
error("Argument #1 of connect must be a function, got a "..type(func), 2)
end
local cn = mBindableEvent.Event:Connect(func)
mAllCns[cn] = true
local pubCn = {}
function pubCn:disconnect()
cn:Disconnect()
mAllCns[cn] = nil
end
pubCn.Disconnect = pubCn.disconnect
return pubCn
end
function this:disconnect()
if self ~= this then error("disconnect must be called with `:`, not `.`", 2) end
for cn, _ in pairs(mAllCns) do
cn:Disconnect()
mAllCns[cn] = nil
end
end
function this:wait()
if self ~= this then error("wait must be called with `:`, not `.`", 2) end
return mBindableEvent.Event:Wait()
end
function this:fire(...)
if self ~= this then error("fire must be called with `:`, not `.`", 2) end
mBindableEvent:Fire(...)
end
this.Connect = this.connect
this.Disconnect = this.disconnect
this.Wait = this.wait
this.Fire = this.fire
return this
end
return t
If anyone has any ideas to why it might be breaking please let me know! The game I am using it in is primarily made of mesh parts so if that might have an effect?
Thanks @apoaddda