Questions regarding single-script architecture & module loaders

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

Yeah @colbert2677 covered it pretty well. The short of it is that initialization sequences are utilized to “set up” whatever the module needs.

Analogy: Think of a firefighter. When a fire occurs, the firefighter “initializes” by putting on all of his/her gear and getting into the truck. Only at that step is the firefighter ready to fight fires. If the firefighter tries to fight fires before fully getting ready, there’s gonna be some trouble.

So in a similar light, modules that talk with each other need to make sure that they’re ready to talk with each other.

In my framework, there’s a separate lifecycle step called “Start” which is used after everything has guaranteed to have been initialized.

6 Likes

Looking down this thread I think everyone else has shown it very well & much better than I would.

Thank you for that explanation, it cleared up things.

Your input was also helpful, it’s good to see different opinions and implementations. Posts in this thread including yours taught me a lot and helped me better visualize things, so I thank everyone who replied.

1 Like