Questions regarding single-script architecture & module loaders

Hi, I’m fairly new to the dev forums, so please bear with me and correct me if I make any mistakes.

I’ve read about this way of setting up your game using a single-script architecture (1 LocalScript for client-side and 1 normal Script for server-side with lots of modules). My question is, how do you properly make a system like this? I’m talking about the scripting part; handling a whole game with 2 scripts while correctly using modules.

My other question is, I’ve seen around these things called “module loaders”, how exactly do they work?

I realize that I might be asking for a lot, so I apologize in advance. If you don’t mind explaining these things to me, it would be greatly appreciated.

Thank you.

11 Likes

Personally, putting everything in 1 Script or LocalScript isn’t favourable; I like to set each script to a purpose eg. ServerShop or BattleHandle. Another reason why I don’t is because if the script errors in 1 part, everything else will be affected. Yet another reason is that working on a Script with many lines gets extremely tough to maintain.

These are used to start something and begin a function or create something ready for an action, a good thing with keeping it in a ModuleScripts is that many other Scripts & LocalScripts can use them.
With ModuleScripts sometimes people program in this type called OOP [Object orientated programming] which can be very resourceful. A better explanation here: Documentation - Roblox Creator Hub

It matters on the context of what you’re loading.
Usually they create new Instances for what you’re going to use.

If my explanation is bad please forgive me.

4 Likes

This is honestly a personal choice, based on your habits, and how you wish to develop things. I for one, do happen to use the module loading type format. It’d be easier to explain certain aspects of it in a Team Create, so if you’d really like to know, add me.

1 Like

Using one script would not be all that maintainable and I would not recommend it at all. You often need to pull parts of the game apart for testing and adding new features. You should opt for a method that allows you to easily maintain your game code.

I have not seen any “loaders” but at a guess they are just implementing lazy initialization instead of attempting to dynamically load module on a function call?

4 Likes

Segmenting each part of the game into modules is what I was assuming he meant.

Kind of like:
https://gyazo.com/150f5f46378bf896baaf620ac83d5933

Being everyone else is implementing code:

function module:LoadLibrary(libName)
	if Libs[libName] then return Libs[libName] end
	local module = script.ResourceModules:FindFirstChild(libName)
	if module then
		Libs[libName] = require(module)
		return Libs[libName]
	else
		warn("Module \""..libName.."\" was not found")
		return nil
	end
end

My parent module uses this code to cache, or initially require, resources modules.

4 Likes

Thank you for your input, it’s greatly appreciated.

Would you mind posting some examples?

Welcome to the devforums!

Anyway, my Single-Script Architecture loadout consists of a custom loader, which changes the behaviors of Folders to act as if they were modules, and they have an “init” module inside of them.

This is a basic module loader that I wrote just on the fly, although you’d want to cater to your exact architecture. You shouldn’t feel like you are obligated to use one-set architecture, do whatever works best for you. Depending on your style and how you work, you may feel like Single Script Architecture wouldn’t work well enough for you, or perhaps you have a better architecture in mind. In the end, if you are creating a game, the players that play the game will not see your code, but you want to have a strong code-base that you can maintain, update, and patch bugs on the fly without any hassle.

local module = {}
function module.require(ModuleFolderToRequire)
    assert(typeof(ModuleFolderToRequire) == "Folder", "ModuleFolderToRequire must be a folder")
    for _,v in pairs (ModuleFolderToRequire:GetChildren()) do
        if v:IsA("ModuleScript") and v.Name == "init" then
            return require(v)
        end
    end
end
return module

I used to have an init method that initializes internal components for the module to be ready to use after require is called, as some may prefer to use the __call metamethod to initialize :init() without actually writing out :init(), however, based off of my personal experience, the first case is far easier to utilize and maintain.

local module = {}
module.__index = module
function module.load()
    return setmetatable({
        __call = function()
            return require(script.Name):init()
        end}, module)
end
return module

The utility would be similar to the code below, to alleviate potential discrepancies you could encounter is to utilize the “shared” or “_G” so you can get the loader without any problems.

local Require = module.require()
local MyModule= Require("MyModule")
local MyOtherModule = Require("MyOtherModule")

MyModule:DoStuff()
MyOtherModule:DoStuff()
5 Likes

Module loader example

-- script
local loader = setmetatable({}, {
	__index = function(table, index)
		print('looking for', index)
		if index == 'ModuleScript2' then
			table.ModuleScript2 = require(script.Parent.ModuleScript2)
			return table.ModuleScript2
		elseif index == 'ModuleScript1' then
			table.ModuleScript1 = require(script.Parent.ModuleScript1)
			return table.ModuleScript1
		else
			print('not found')
		end
	end
})

loader.ModuleScript1.hi()
loader.ModuleScript2.hi()

loader.ModuleScript1.hi()
loader.ModuleScript2.hi()

-- module (print change for module 2)
local module = {}

function module.hi()
	print('hi 1') --
end

return module

output

RobloxStudioBeta_1xjNu965BH

2 Likes

Thank you for welcoming me!

Those examples are great help, I’ll look into them.

Sorry, I’m new to the forums. Add you on Roblox?

1 Like

I have a single script architecture.

I have mine setup with one script in ServerScriptService and one localscript in StarterPlayerScripts which both handle defining common variables for each machine and initializing specific modules that I store in folders that are in ReplicatedStorage and ServerStorage respectively. I also make use of metatables to make my life easier when getting the modules and other things.

If you have any questions on the specifics of my structure you can dm me I’ll be glad to answer.

2 Likes

Im not really sure if I can show you an example as it is mostly something that comes with experience. I can however give you a tip. If a solution seems long winded or overly complex then it probably is and should be changed. There will be in most cases tools or methods to reeduce the complexity to improve the maintainability of the game.

1 Like

Sorry for the late reply, and thank you for the informative post.

Can you please provide code examples so I can better comprehend it?

I’ll make sure to keep that in mind. Thank you for helping me, I really appreciate it.

1 Like

Modules are just code blocks that run one time, once prompted (not auto) and can be accessed by other scripts.

You can use them to order operations if you put each operation in a module and require(run) each module in a specific order on the thread. They allow you to break up your code without race conditions.

One normal and the rest modules isn’t a special format. Most games just have no reason to have 2 starting points(multiple threads).

Specific uses:
Reusable code, especially across network if in replicated storage. (modular function)
Object, defaults to running once and providing a table holding mutable data. (modular state)

You can combine the two if you want to play with some mild OOP.

I’ve actually asked about this kind of structure multiple times and have actually fully adapted it now. @Crazyman32’s Aero Game Framework has been a big help to my learning curve and I’m using his framework in a lot of my projects as of late, with various modifications or self-created systems based around AGF. I’ve become pretty familiar with this architecture.

Essentially, you don’t run any code in the scripts. It’s called single-script architecture but your scripts only act as bootstrappers. Everything happens in ModuleScripts and is fully event-based. The only code that’s ran without being told are said bootstrappers. You can have three: two client, one server.

  • Server bootstrapper goes in ServerScriptService, this loads server-side code and exposes endpoints for client modules (e.g. for networking or creating services)

  • Client bootstrapper goes in StarterPlayerScripts, this loads client-side code (in the case of needing to run code for fresh spawns, you handle that here as well with CharacterAdded)

  • Second one goes in ReplicatedFirst, this one is primarily used to set things up initially (CoreGui setters, preloading, loading screens, so on)

As I said before, I use AGF in my projects a lot (sorry Crazyman, I edit it sometimes to put the ServerStorage components in ServerScriptService.Aero). A typical AGF structure looks somewhat like this with all code accounted for:

ReplicatedFirst
    AeroLoad
        TitleScreenBootstrapper
ReplicatedStorage
    Aero
        Internal
        Shared
ServerScriptService
    Aero
        Internal
        Modules
        Services
StarterPlayer
    StarterPlayerScripts
        Aero
            Controllers
            Internal
            Modules

Init functions are used to set things up before the actual module should be started. For example, if you’re setting up a shop service, then you want to register the events first before starting any code, otherwise you have no events and the code can’t run. Therefore, event registration happens in Init. Init functions are the same as running code in the main scope on require, which is what most closed-source or require-by-Id modules do.

Code in the main thread:

local Module = {}
local initialised = false

print("Started")
initialised = true

function Module:Test()
    print("Test ran, init:", initialised)
end

return Module

local module = require(Module)
module:Test()

Function equivalent:

local Module = {}
local initialised = false

function Module:Test()
    print("Test ran, init:", initialised)
end

function Module:Init()
    initialised = true
    print("Started")
end

return Module

local module = require(Module)
module:Init()
module:Test()

Init functions are typically good when you’re working with a framework or modules that need to set things up for a module to be able to work. Init itself is a function like any other, however its use case is commonly used to determine a set of instructions that must be completed before something else. Typically, Init is complemented by a Start function which runs code. If you think about it logically:

Before I start, I need to initialise. I can’t start if I don’t exist or don’t have what I need.

I know there’s a lot of talk about this whole “if one script fails, everything else fails”. I used to be on that same worry wagon, even just a few days ago. A framework actually prevents that in a lot of cases. Unless a calling environment fails to start a module, a module’s error is actually locked to itself. You can isolate errors very easily with modules and fix them without too much of your game breaking, or any of it at all. It boils down to dependencies and what the error is.

Finally, a module loader is just fancy talk for either a framework or a library loader. Modules can be used in either structure to form something: the components of a framework or the libraries that are loaded by a library loader. A “module loader” itself is broad.

A framework directs flow, while a library loader lets you control the flow. A framework is typically used to solve a problem of flow or help organise your code, while a library loader lets you load modules on demand. Loading modules on demand refers to a practice called lazy-loading. Frameworks have lazy-loading in them as well, but they also combine the counterpart, eager-loading, for specific components. These modules are essentially treated like regular scripts: required, initialised and started ASAP. Modules on the other hand can be but typically they aren’t.

For definitions:

  • The practice of lazy-loading refers to not loading something until its required in code, which is helpful especially in a library loading environment.

  • Lazy-loading’s counterpart, eager-loading, refers to modules that are loaded along with something else. In a framework’s case, eager-loaded modules are loaded as the bootstrapper runs.

  • Libraries are a pretty literal term: they’re modules which contain useful functions, like how an actual library provides useful information (assuming you’re looking at a fact book or so).

If you want a good library loader, you can check out @Quenty’s NevermoreEngine. Loaders often are used to get around pesky issues, such as circular dependencies. This term you can search up, though it boils down to code recursively calling each other since they need one another to work.

This is all the insight I can give you. I’m still fairly new to the ballpark but I absolutely love this structure now that I’ve gotten familiar with it and have put it to practice. I don’t think I’ll see myself going back to multi-script structures unless I get lazy or use it temporarily while I work on a framework of sorts. My complete praise is to single-script structures and I’m glad I took the time to learn it after a long time of asking, trial, error and giving up on the DevForum and on my own time.

33 Likes

Lua is single-threaded. :slightly_smiling_face:

Code in scripts themselves can be counted as multiple starting points once the thread scheduler gets to their first chunks, but two threads aren’t ran. Even with spawn and coroutine, that’s pseudothreading, not starting actual new threads.

Thus no need for more than one Script :stuck_out_tongue:
Doesn’t work in lua anyway.
Thanks again for the enlightenment; are you following me?

Yep. Hurray for module-based structures. I’m sorry, I just learned how to apply single-script architectures in code and run everything from modules and it’s helped my flow so much that I’m excited about it.

No, I read every thread and reply that’s posted in Scripting Support.

I’ve always been a fan of your posts, so I thank you for that informative explanation, it was much needed and I greatly appreciate it.

I’ll make sure to nail it into my head.

1 Like