Why wont my NPC system move?

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

2 Likes

Have you made sure the NPC model isn’t anchored?

1 Like

Yep, its not anchored. Do you think the meshparts could possibly have something to do with it?

If the MeshParts on the NPC are anchored, try unanchoring them and welding them instead. (Or you can use Constraints)

1 Like

They are welded and unanchored. Also the map is mainly built out of meshparts. The strangest thing is that this works on an empty baseplate with the same configuration happening.

You should make sure the output isn’t logging any errors while the script is running, also, is the HumanoidRootPart anchored? Make sure it’s not.

Nothing anchored, no errors. Even when logging is on it show calculations for correct paths however the npc doesn’t move to those paths.


These are all valid paths its outputting it just wont move.

1 Like

So this module used to work before. What have you changed to make it not work?

It never worked in this map. It always work in a stand alone baseplate, I have not touched the code only removed some comments. Here is the baseplate it was working in: NPC Testing - Roblox

File:
NPC Testing.rbxl (57.0 KB)

do some debugging and put some prints where the code seems to stop. It will help you know where in this script or module the error is.

So there might be an issue with the map itself or other scripts in your map, maybe it’s clipping into something?


This is where I am trying to get the NPC’s to go, there are no invisible walls. Its not clipping anywhere.

Its not a map bug, because I copy and pasted everything over and it runs fine on my baseplate:
image

check legs or parts of Npcs and see if you will find: ''Weld" something that appears when you move
a part and place it anywhere ( terrain or another part )

this messed up some things that i was doing before

No, that changed nothing! I dont know why but this works in plain baseplates but not in this games map? Its confusing I am about to rewrite everything.

Probably some pathfinding bugs. Maybe try moving your npc elsewhere, or make the map more easier for the npc to move around. Try to check if YT here is anything is wrong with the walk speed.