Сonvenient Module Loader for game scripts organization

(16.09.2024) Now it respects in which order modules was required

Hello everyone. Recently I’ve made my own Module Loader which I using for organization of my games, and now decided to share it.

If you don’t know what is that “Module Loader”, it’s module which helps you to organize you game, because you can write all code in different module scripts and only one server/client script gonna run them all. This makes your game scripts very modular, so you can remove or add them easily, without changing most of code.

local Loader = {}
local required_modules = {}
local get_cache = {}

function Require(modules)
	local local_required_modules = {}
	
	for _,module_script in modules do
		if not module_script:IsA("ModuleScript") then continue end
		if not module_script.Parent:IsA("Folder") then continue end

		local module = require(module_script)
		local_required_modules[module_script.Name] = module
	end
	
	return local_required_modules
end

function Loader.UpdateGetCache() -- Just to optimize get function
	get_cache = {}

	for _,required_modules_list in ipairs(required_modules) do
		for required_module_name, required_module in required_modules_list do
			get_cache[required_module_name] = required_module
		end
	end
end

function Loader.Get(name)
	return get_cache[name]
end

function Loader.Require(modules)
	if typeof(modules) == "Instance" then
		table.insert(required_modules, Require(modules:GetDescendants())) -- if this folder or any other instance
	elseif typeof(modules) == "table" then
		table.insert(required_modules, Require(modules)) -- if this table with modules
	end
	
	Loader.UpdateGetCache()
end

function Loader.Start()	-- loading and initing everything from "required_modules" variable
	for _,required_modules_list in ipairs(required_modules) do
		for required_module_name, required_module in required_modules_list do
			if required_module.__load then
				print("[LOADING MODULE]", required_module_name)
				required_module.__load() -- synchronically one by one loads modules
			end
		end
	end
	
	for _,required_modules_list in ipairs(required_modules) do
		for required_module_name, required_module in required_modules_list do
			if required_module.__init then
				print("[INITIALIZING MODULE]", required_module_name)
				task.spawn(required_module.__init) -- asynchronically initializes each module
			end
		end
	end
end

_G.Get = Loader.Get -- global function to get link to required module
return Loader

I tried to keep code clean and understandable as possible, so it can lack some safety checks. But I guess that’s not big problem, because it’s used for advanced scripters, which know what they’re doing.

It’s would be hard to explain how it works in details here, so I just gonna show some examples of code. How I used it.

--Single server script in my currently developed project

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Loader = require(ReplicatedStorage.Loader)

Loader.Require(ReplicatedStorage.Modules)
Loader.Require(script.Modules)
Loader.Start()

Basically what we doing. We getting our Loader module, and requiring in it two folders. One is shared between client and server in ReplicatedStorage, and other one is parented to our script, so all modules in it can be used only by server. Loader.Start() loads and inits all modules which we required upper.

How organization of server scripts actually look in my project (it’s pretty small for now but gonna grow)
image

I made it to require childrens only of folders, and skip anything except module scripts.
So it gonna skip any garbarage in your folder, you can even put assets folder or something like that in your folder and it still gonna work normally.

If you gonna put module scripts in module scripts Loader gonna skip them, so you can have some local module scripts to some of your modules if you want. By the way you can easily change it, just a little modyfying Loader.

Same works with client

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Loader = require(ReplicatedStorage:WaitForChild("Loader"))

Loader.Require(ReplicatedStorage:WaitForChild("Modules"))
Loader.Require(script.Modules)
Loader.Start()

I highly recommend you to put Loader in ReplicatedStorage so it can be shared between client and server.

What is that “__init” and “__load” functions?
It’s additional functions of Loader, which is can be very useful. Basically it runs both of them for each module when you calling Module.Start()

Which is difference between “__init” and “__load”?
“__load” is called one by one synchronically for each module before “__init” is called. When “__init” is asynchronous function which called in different threads for each module at same time.

How it can be used?
At least for me, “__load” useful to get links of required modules, and getting them into global variables of my certain module, so I can use them later in it.

function Player:__load()
	Health = _G.Get("Health")
	Character = _G.Get("Character")
	Janitor = _G.Get("Janitor")
	TableUtility = _G.Get("TableUtility")
end

One example how I using it in one of my modules. I can use Health/Character/Janitor modules in any piece of my code here now. So let’s say in “new” function of my Player class I can do something like this:

self.character = Character:new(self)
self.character:load()

That’s it. Thanks for reading post. Tell me your throughts under, maybe some things to improve, or questions. I hope someone will find this useful.

P.S: Sorry for some troubles with grammar in this post. English not my first language, but I still hope you got everything I tried to say.

3 Likes

Honestly, I would use this if it had autocomplete for each module. Sadly, Roblox type inferrence doesn’t allow that yet, hence why I still use the good ol require().

1 Like

Module auto complete? Hmm, can you explain what you mean exactly? Have few suggestions, but still no idea honestly.

Using require() gives you all returned functions and member variables of the module, and suggest you it while you type (as expected), however using your module to require a module doesnt have this feature, which is unfortunate because the autocomplete is extremely handy.

Then again you might be able to fix this with the new type solver?

Actually as I know, it can be fixed with some advanced typechecking stuff, I recommend you to watch this video “https://www.youtube.com/watch?v=F5B2zr-60bI”, here everything explained. Timecode 12:06, but I recommend to watch entire video.