You are using ModuleScripts
kind of the right way. Separating functions into individual ModuleScripts
is best for adhering to the DRY (Don’t Repeat Yourself) rule, yes, but having a single LocalScript
handling ALL of your ModuleScripts
can get pretty messy. Especially later down into development where you have a bunch of ModuleScripts
but your main LocalScript
where you handle everything is like thousands of lines long. Not really ideal.
What you should start doing is to have another ModuleScript
inside ReplicatedStorage
that will essentially “load” your ModuleScripts
when passed as an argument by calling a function. Personally I use start
to load my ModuleScripts
, you can use something like Initialize
or what not.
Here, I have already written you a ModuleScript
that will do just that.
type LoaderType = {
LoadedModules: {},
Initialized: boolean,
InitializedEvent: BindableEvent,
Load: ({ModuleScript}) -> (),
Get: (string) -> (any),
isInitialized: () -> (boolean)
}
local Loader: LoaderType = {} :: LoaderType
Loader.LoadedModules = {}
Loader.Initialized = false
Loader.InitializedEvent = Instance.new("BindableEvent")
function Loader.Load(
modules: {ModuleScript}
)
for _, obj in modules do
if not obj:IsA("ModuleScript") then continue end
if Loader.LoadedModules[obj.Name] then continue end
local module = require(obj)
if module.start then
task.spawn(module.start)
Loader.LoadedModules[obj.Name] = module
end
end
Loader.Initialized = true
Loader.InitializedEvent:Fire()
end
function Loader.Get(
moduleName: string
)
if not Loader.isInitialized() then
Loader.InitializedEvent.Event:Wait()
end
local module = Loader.LoadedModules[moduleName]
return module
end
function Loader.isInitialized(): boolean
return Loader.Initialized
end
return Loader
Afterwards, you’re going to restructure your ModuleScripts
to have a ‘start’ function just like so
--!strict
export type ClassType = {
start: () -> (),
foo: () -> ()
}
local ModuleA: ClassType = {} :: ClassType
function ModuleA.start(): ()
print("'ModuleA' started!")
end
function ModuleA.foo(): ()
print("bar")
end
return ModuleA
And then inside your LocalScript
you’re going to load your ModuleScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Loader = require(ReplicatedStorage.Loader)
local dependencies = script.parent.dependencies
Loader.Load(
dependencies:GetChildren()
)
You can also grab other ModuleScripts
through the Loader module and run their functions as well upon startup
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Loader = require(ReplicatedStorage.Loader)
export type ClassType = {
start: () -> ()
}
local ModuleB: ClassType = {} :: ClassType
function ModuleB.start()
print("'ModuleB' started!")
local moduleA = Loader.Get("ModuleA")
moduleA.foo() -- Prints "bar" in output
end
return ModuleB
What I like about this approach is that it keeps everything tidy and I personally believe this should be standard practice. Pretty much every single game you see on the front page utilizes this method and I can see why. You can read more about it here