Is this a good approach for a Roblox module loader?
Basically, each of my services has an Init function that contains its main logic and lifecycle setup, such as connecting RemoteEvent listeners with the actual logic inside.
All dependencies are also required outside of the Init function.
The loader works in three stages:
- Require all modules.
- Call
Init()for every module. - Only then start listening for player events.
Here is the loader:
local ServerScriptService = game:GetService("ServerScriptService")
local Players = game:GetService("Players")
local DataManager = require(game.ServerScriptService.UniverseServer.Data.DataManager)
local folders = {
ServerScriptService.UniverseServer.Services,
ServerScriptService.UniverseServer.Systems,
ServerScriptService.UniverseServer.DeveloperProducts
}
local loadedModules = {}
local playerHandlers = {}
local characterHandlers = {}
local playerRemovingHandlers = {}
local function safeCall(moduleName, fn, ...)
local success, err = pcall(fn, ...)
if not success then
warn(("[Loader] %s error: %s"):format(moduleName, err))
end
end
local rawModules = {}
for _, folder in ipairs(folders) do
for _, module in ipairs(folder:GetDescendants()) do
if module:IsA("ModuleScript") and not loadedModules[module] then
loadedModules[module] = true
local success, moduleScript = pcall(require, module)
if not success then
warn(("[Loader] Failed to require %s: %s"):format(
module:GetFullName(),
moduleScript
))
elseif typeof(moduleScript) == "table" then
table.insert(rawModules, {
Name = module.Name,
Script = moduleScript
})
end
end
end
end
for _, item in ipairs(rawModules) do
local mod = item.Script
if typeof(mod.Init) == "function" then
safeCall(item.Name .. ".Init", function()
mod:Init()
end)
end
if typeof(mod.SetupPlayer) == "function" then
table.insert(playerHandlers, function(player)
mod:SetupPlayer(player)
end)
end
if typeof(mod.SetupCharacter) == "function" then
table.insert(characterHandlers, function(player, character)
mod:SetupCharacter(player, character)
end)
end
if typeof(mod.PlayerRemoving) == "function" then
table.insert(playerRemovingHandlers, function(player)
mod:PlayerRemoving(player)
end)
end
end
local function onPlayerAdded(player)
for _, handler in ipairs(playerHandlers) do
task.spawn(function()
safeCall("SetupPlayer", handler, player)
end)
end
player.CharacterAdded:Connect(function(character)
for _, handler in ipairs(characterHandlers) do
task.spawn(function()
safeCall("SetupCharacter", handler, player, character)
end)
end
end)
if player.Character then
for _, handler in ipairs(characterHandlers) do
task.spawn(function()
safeCall("SetupCharacter", handler, player, player.Character)
end)
end
end
end
DataManager.PlayerLoaded.Event:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
for _, handler in ipairs(playerRemovingHandlers) do
task.spawn(function()
safeCall("PlayerRemoving", handler, player)
end)
end
end)
Is there anything fundamentally wrong with this architecture, or are there issues that could cause problems later as the project grows?