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.
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.