Module loaders are a very powerful resource that most newer developers are unaware of. Here is a simple one I made using lemonSignal. You can either use rojo with wally to install, or just get the lemonSignal module from the data-orient-house github here and redirect the require to the right place. Anyway, here is a module loader that works well for most projects:
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local ServerScriptService = game:GetService("ServerScriptService")
-- [[IMPORTANT]] If you arent using rojo/wally, you will need to install this manually
local lemonsignal = require(ReplicatedStorage.Packages.lemonsignal)
local _isServer = RunService:IsServer()
local _inst
-- [[IMPORTANT]] Make sure this directory points to your client and server modules
local _root = _isServer and Players.LocalPlayer.PlayerScripts.Controllers or ServerScriptService.Services
local modules: {[string]: any}
local finishedLoading = false
local finishedSignal = lemonsignal.new()
local Private = {}
local ModuleLoader = {}
ModuleLoader.__index = ModuleLoader
function Get()
if not _inst then
_inst = setmetatable({}, ModuleLoader)
Private._RequireModules()
end
return _inst
end
function ModuleLoader.Import(moduleIndex: string)
if modules[moduleIndex] and finishedLoading then
return modules[moduleIndex]
else
local running = coroutine.running()
local _timeoutThread
local _conn
_timeoutThread = task.delay(10, function()
coroutine.resume(running)
end)
_conn = finishedSignal:Connect(function()
coroutine.resume(running)
end)
coroutine.yield(running)
coroutine.close(_timeoutThread)
_conn:Disconnect()
return modules[moduleIndex]
end
end
function Private._SetupModules(bulk: {[string]: any})
for _index: string, contents: any in bulk do
if contents.Init then
contents:Init()
end
end
for _index: string, contents: any in bulk do
if contents.Start then
task.defer(contents.Start, contents)
end
end
modules = bulk
task.wait()
finishedLoading = true
finishedSignal:Fire()
end
function Private._RequireModules()
local unloadedModules = _root:GetChildren()
local toSetup = {}
for _, module: ModuleScript in unloadedModules do
if module.ClassName ~= "ModuleScript" then
continue
end
toSetup[module.Name] = require(module)
end
Private._SetupModules(toSetup)
end
return Get
To use this, just put the module in replicated storage and have a local and server script require it from PlayerScripts or ServerScriptService. Alternatively, if your not on rojo, you can put those scripts under the module in replicated storage and set their run context to client and server. If you end up doing that instead, the runcontext property only exists on server scripts but you can change the client on to run on the client
For using them in script, theres 2 phases you need to understand in order to use this module loader effectively: the initializing phase, and the start phase. The initializing phase focusing on seting up the module and making sure everything other scripts would use is available. For example, if you wanted to have a signal in your module that fires when a player gets a point, you could do something like this:
PointService:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local lemonsignal = require(path.to.lemonSignal)
local Private = {}
local PointService = {}
PointService.__index = PointService
function PointService:Init()
PointService.PointsAwared = lemonsignal.new()
end
function PointService:AwardPoints(player: Player, points: number)
PointService.PointsAwared:Fire(player, points)
print(`Player: {player} got {points} points!`)
end
return setmetatable({}, PointService)
AnotherService:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ModuleLoader = require(path.to.ModuleLoader)
local PointService = ModuleLoader.Import("PointService")
local Private = {}
local AnotherService = {}
AnotherService .__index = AnotherService
function AnotherService:Init()
-- Dont connect here, as it creates a race condition.
-- Connecting in the Start function is ideal as it will be called after the
-- init function, where the connection is made, eliminating race conditions entirely.
end
function AnotherService:Start()
PointService.PointsAwarded:Connect(function(plr: Player, points: number)
print(`AnotherService knows that player: {plr} was awarded: {points} points!`)
end)
end
return setmetatable({}, AnotherService)
If you have any concerns or questions, reply here or talk to me on discord: Sjdinsd.