Help with Modularization

I’ve recently started looking into modularization - from all the articles and posts I’ve read, this appears to be an ideal approach to organising a project, especially with a large team.

I have worked with modules before, but never organised a whole game around them.

To begin, I’ve set up a ‘Main’ script in ServerScriptService, which then contains the modules. The Main script will essentially handle all the modules:

My first question: what’s the best way to store variables? I’d ideally like to store all the variables in the Main script, which can then be referenced from all the other modules. For example, I don’t want to repeatedly define services, like rs = game:GetService("ReplicatedStorage")

Also, is there any other things I should be aware of with modularization? For example, what bad practises are you aware of, how can I optimise efficiency, etc?

I’d also be interested to hear tips and advice from people who are experienced with modularization.

Thanks :slightly_smiling_face:

21 Likes

Didn’t mean to make this a reply, sorry Wingz

Create a modulescript that will function as the ‘main’ modulescript. Ideally, all global variables get stored here.
In that modulescript, have a function, ideally called Initialize. Inside of this function, have the main modulescript require all of the other modules and store them as variables attached to the main module. All modulescripts should require the main one back.

The localscript that requires the main modulescript should run the Initialize() function. This way, when the other modules require the main one, it doesn’t end up trying to require everything again, creating a loop.

Here’s how my game does it:
Main Module Script

function framework.Initialize()
    --[[ GLOBAL VARIABLES ]]--
    -- SERVICES
    framework.players = game:GetService("Players")
    framework.runservice = game:GetService("RunService")
    framework.userinputservice = game:GetService("UserInputService")
    framework.repstore = game:GetService("ReplicatedStorage")

    -- PLAYER VARIABLES
    framework.player = framework.players.LocalPlayer
    framework.character = framework.player.Character
    framework.currentcamera = workspace.CurrentCamera

    --[[ OTHER MODULES ]]--
    -- HANDLERS
    framework.chat = require(script.Modules["Chat Handler"])
    framework.camerahandler = require(script.Modules["Camera Handler"])
    framework.controlhandler = require(script.Modules["Control Handler"])

    game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.All, false)
end

Other Modules

local main = require(script.Parent.Parent) -- This should route to the main modulescript

main.player -- How you access the player, for example

Localscript

local mainframework = require(script:WaitForChild("Main Framework")) -- This should be the main modulescript
mainframework.Initialize()

From then on, all scripting should be done in the modules and nothing else needs to be in the localscript.

7 Likes

Having a separate namespace for services is redundant; I’ve never seen anybody do that. You already have access to them through the DataModel. As for modules, I just type everything out instead of making things complicated, but that’s just me.

@OP: I’d say that your current design is a bit confusing. One of the goals of modularization is to minimize complex interdependency, because that makes your code unpredictable and hard to maintain or debug. However, you defined some private variables in the “main” script and are now trying to share those with the modules, which in turn are being utilized by the main script. This is the kind of interdependency you want to avoid in modular programming.

4 Likes

Please document what you’re doing extremely well and stay clean. I attempted to edit a game and the guy had 470 billion module scripts placed everywhere with no documentation. Needless to say I didn’t continue that project.

That’s the biggest thing that turns me off when it comes to this sort of thing.

6 Likes

@Wingz_P @lArekan @suremark @Intended_Pun @BuildIntoGames

Thanks for the feedback, much appreciated!

Have a main module that contains references to all modules you plan to use in a dictionary format. This:

  • Provides a shared resource for all code that doesn’t need to be changed individually
  • Allows you to yield all code (ie. loading screen)
  • Keeps code clean and consistent
  • Keeps your code from getting clogged up with 50 different modules with common variable names (ie. Stats, Settings)

Always avoid writing duplicate code. Always attempt to simplify anything as much as possible.

11 Likes

I wonder if you would provide a sample of how you do this?

I really only use modules when I write out complex APIs, OOP, and stuff that clutter the main framework. Typically the only thing in my main framework is gameplay mechanics and I have some major gameplay functions in a separate module (or in the script itself if they are on topic and dont clutter). You can also have a handy module loader that when called already loads the renderstepped and other necessary commonly used values into a table that when required is set up then all modules can use it.

Using the advice from @BuildIntoGames and @lArekan , I’ve made a system which works as follows:

For the server scripts, I have one Script named ‘ServerMain’ containing all the server modules:
image

For the client, I have a LocalScript located in StarterPlayerScripts with all the client modules located in a folder in ReplicatedStorage:



Both the ServerMain and LocalScript require a module called ‘MainFramework’ located in Universal in Modules:

-- << RETRIEVE FRAMEWORK >>
local main = require(game:GetService("ReplicatedStorage").Modules.Universal.MainFramework)
main.Initialize()
local modules = main.modules

MainFramework contains all the services and widely used variables for both server and client:

local main = {}

function main.Initialize()
	
	-- << SERVICES >>
	main.rs = game:GetService("ReplicatedStorage")
	main.ss = game:GetService("ServerStorage")
	main.sss = game:GetService("ServerScriptService")
    main.players = game:GetService("Players")
	
	-- << MAIN VARIABLES >>
	main.revents = main.rs.RemoteEvents
	main.moduleGroup = nil
	if game.Players.LocalPlayer ~= nil then
		main.moduleGroup = main.rs.Modules
		main.player = game.Players.LocalPlayer
	else
		main.moduleGroup = main.sss.ServerMain
	end
	
	-- << SETUP MODULES >>
	main.modules = {}
	for a,b in pairs(main.moduleGroup:GetDescendants()) do
		if b.ClassName == "ModuleScript" then
			main.modules[b.Name] = require(b)
		end
	end
	for a,b in pairs(main.rs.Modules.Universal:GetDescendants()) do
		if b.ClassName == "ModuleScript" and b.Name ~= script.Name then
			main.modules[b.Name] = require(b)
		end
	end
	
end

return main

Note: I’ve cut out a lot of the services and variables I use to make it easy to read

Finally, in every module I retrieve the ‘framework’:

local module = {}


-- << RETRIEVE FRAMEWORK >>
local main = require(game:GetService("ReplicatedStorage").Modules.Universal.MainFramework)
local modules = main.modules


-- Module stuff here


return module

This enables all modules to require any module they need (without the risk of cyclic calls) and allows them to reference any variable defined in MainFramework (e.g. print(main.player.Name) would print the local player’s name for Client modules).

Hope this helps any future readers looking to modularise their code :+1:

22 Likes

I can’t provide a proper response or anything yet since I’m in school, but if you’re only using the Initialize function once (hence it’s name), you can shorten that to have the module return a table containing your globals or whatever instead of using a function to index constants into a table to be returned.

ModuleScript:

return {
    yes = "hi"
}

Other script:

local main = require(module)
print(main.yes) --> "hi"

I tried that initially - without the dictionary of modules it works fine, but with the dictionary it causes the modules to repeat require() each other in a cycle.

1 Like

Why would we need to do function main.Initialize?
Can’t we just do

local main  = {}

main.Players = game:GetService("Players")

return main

Or something like that

Also, for other modules under the main module file, what should I do to retreive those in my script as well. Eg, I made a main module script that lists all main services, and under those I put specific stuff like Players or Http stuff.

What would I do to retrieve those from the script?
This topic is from 2 years ago, hopefully someone can reply.

1 Like

The initialization function handles the loading and referencing of all descendant services and easy-loaded modules. Without it, a cyclical reference could occur when one of these descendant services/modules attempts to reference an item within the main framework, such as another module.

My frameworks changed slightly over the years. You can find the newer one here: https://github.com/1ForeverHD/HDAdmin/blob/master/Core/MainModule/SharedModules/MainFramework.lua

Then to retrieve the main framework with a descendant module, you simply do:

local main = require(game.HDAdmin)

or (which is the same as):

local main = require(game:GetService("ReplicatedStorage").HDAdmin.Core.SharedModules.MainFramework)

It’s heavily inspired from AGF so I highly recommend checking out that as it has some incredible tutorials and documentation: AeroGameFramework

5 Likes