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.