How would I use Module scripts to manage NPC behavior?

I have read and watched many tutorials about module scripts, and I mostly understand it. What I don’t understand is how I would use it to manage NPC behavior across NPCs within a workspace based on their names on a server script. If anyone could explain it to me, that would be most helpful.

1 Like

The best approach would be using a class system.

Every NPC would be assigned this class, acting as the “brains” of the character. This will allow you to make changes to the way your NPC’s act, all from a single source.

Here is an in-depth post on using OOP

To sum it up; you will have a generalized .new method that will be called on every new NPC.
You can use CollectionService to listen for added NPC’s, then pass them through this method.

I suggest doing this setup on the server, then passing updated values to the client.
Varying on the setup of your ModuleScript, and your goal, you can have a large table consisting of the game’s NPC’s, which can be used to add more functionality.

For example, if you want them to switch between “wandering” or “attacking” states, you can have a loop which would iterate through either table and applying the relevant methods.

Here’s how I would set it up:
Server-side
Script

local NPC_TAG = "NPC"
-- Limits
local WALK_RADIUS = 1 -- The "radius" from which a random NearbyPoint will be used to calculate
local NPC_TICK = 2 -- every "tick", run new actions for NPCs

local NPCManager = require(script.NPCManager)

local CollectionService = game:GetService("CollectionService")

local randomSeed = Random.new() -- for "true randomness"

local activeNPCs = {}
-- Plugging in the current position of the NPC, get a random point nearby
local function GetNearbyPoint(relative_to: Vector3): Vector3
	return Vector3.new(randomSeed:NextInteger(-WALK_RADIUS, WALK_RADIUS), 0, randomSeed:NextInteger(-WALK_RADIUS, WALK_RADIUS)) + relative_to
end

function NPCRemoved(RemovedNPC: Model)
	local npc = activeNPCs[RemovedNPC] or error(`{RemovedNPC} is not apart of activeNPCs; something went wrong.`)

	npc:CleanupAsync()
end

function NPCAdded(AddedNPC: Model)
	local npc = NPCManager.new(AddedNPC)

	activeNPCs[AddedNPC] = npc
end
-- Since NPCs may have been added in Studio, we will have to get them
for _, AddedNPC in pairs(CollectionService:GetTagged(NPC_TAG)) do
	NPCAdded(AddedNPC)
end

CollectionService:GetInstanceRemovedSignal(NPC_TAG):Connect(NPCRemoved)
CollectionService:GetInstanceAddedSignal(NPC_TAG):Connect(NPCAdded)

-- I do not recommend loops such as these, but again, for demonstration.
while task.wait(NPC_TICK) do
	for _, npc in activeNPCs do
		npc:RandomizeState() -- Set a random state!
		npc:StopMoving() -- If the state is wandering again, this may cause a hiccup. Only meant for demonstration.

		if npc:GetState() == "Wandering" then
			-- This NPC is meant to wander! Have it walk to a random point nearby.
			local random_point = GetNearbyPoint(npc:GetPosition())
			npc:WalkTo(random_point)
		end
	end
end

NPCManager

export type NPC_State = "Wandering" | "Idle" -- Fill this in with your desired states. It wont affect functionality, only autocompletion. Will make your life easier.

local randomSeed = Random.new()

local NPCManager = {}

-- Create a new class. This creates the NPC and all you will need to work with it.
local NPC = {}
NPC.__index = NPC

-- Now we can create our functions.
-- This is just for our use case scenario. Would likely be ineffecient in any other case.
function NPC:RandomizeState()
	local random_number = randomSeed:NextInteger(1, 2) -- Should be based on your # of NPC_State
	local new_state: NPC_State

	if random_number == 1 then
		new_state = "Wandering"
	else
		new_state = "Idle"
	end

	self:ChangeState(new_state)
end
-- Return the current Vector3 of the HumanoidRootPart
function NPC:GetPosition(): Vector3
	return self._Humanoid.RootPart.Position
end
-- Change the internal state of the NPC between "Wandering" and "Idle". Alternatively, Attributes can be used.
function NPC:ChangeState(new_state: NPC_State)
	self._state = new_state
end
-- Return the current self._state
function NPC:GetState(): NPC_State
	return self._state
end
-- Iterate through and disconnect all connections.
function NPC:CleanupAsync() -- You can use a Janitor module to help with cleanup.
	for index: number = 1, #self._connections do
		local connection = self._connections[index]

		connection:Disconnect()
	end
end
-- Reset the WalkToPoint so the Humanoid stops moving.
function NPC:StopMoving()
	self._Humanoid.WalkToPoint = Vector3.zero
end
-- Call :MoveTo on the Humanoid, printing off the result when `point` is reached.
function NPC:WalkTo(point: Vector3)
	self._Humanoid:MoveTo(point)
end

function NPCManager.new<NPC>(NPCModel: Model)
	local newNPC = {}
	setmetatable(newNPC, NPC) -- In layman's terms, we are adding the `NPC` table into this `newNPC` table, making the methods we constructed available to it.

	local function walkToFinished()
		print(`{newNPC._Model.Name} finished walking!`)
	end

	local Humanoid = NPCModel:FindFirstChildOfClass("Humanoid")

	newNPC._Humanoid = Humanoid -- I like to use an underscore for "do not touch" properties
	newNPC._Model = NPCModel
	newNPC._state = "Wandering"

	-- Connections
	local moveToConnection = Humanoid.MoveToFinished:Connect(walkToFinished)
	local destroyedConnection = NPCModel.Destroying:Once(function()
		newNPC:CleanupAsync()
	end)

	newNPC._connections = {
		moveToConnection,
		destroyedConnection
	}

	return newNPC
end

return NPCManager

If youre interested in:
More efficient way of “WalkTo” i.e. PathfindingService
setmetatable
.__index
MoveTo

Now all you have to do is insert some new Rig’s with the “Rig Builder” then add the “NPC_TAG” Tag to them. Refer to CollectionService if you’re interested in how Tag’s work.

This only scratches the surface of what you can do with OOP, even more so with NPC mechanics. The ModuleScript only has them walking around, but you can imagine much more functionality just by adding a few methods then plugging them into your Script’s.

Thank you!

Would this mean I need to rewrite my script? The interactions between the players and each NPC rely on remote events currently and relies on their names for their separate different functions.

After reading for a bit, I still don’t understand.

I apologize if I’m bothering you, but this seems too complicated for what I’m going for.

I want the NPC to wander by default, but upon interaction from the player with a tool (currently my script uses remote events) stops and does a little interaction with the player (with a remote event back to the tool) before they wander again.

No problem at all!

For this behavior you can manage the NPCs from a server-side script that receives the event. You can create a unique identifier for each NPC.

Similar to how I had a table of activeNPCs, you can assign this identifier as an attribute to the NPC—representing its key in activeNPCs.

When the player interacts with the NPC, they can use GetAttribute to get this identifier and pass it to the server. After receiving, the script can stop the NPC from wandering.

If you’d like the assurance that the NPC has stopped, you can use a RemoteFunction to yield until a result is returned from the server.

Does this answer your question?