Can I use a modulscript as a central storage for variables?

I have a few tables and variables that need to be used across many localscripts. Can I use a ModuleScript as a central storage for them, such as:

Module:

local module = {}

module.MyTable = {...}

return module

One of the localscripts:

local myModule = require(script.Parent.ModuleScript)

...

table.insert(myModule.MyTable, someVariable)

I know it’s possible, but is it a good idea? Thanks!

2 Likes

looks like a good idea to me

especially since the only other alternative would be datastores or _G

1 Like

Alright, does everyone agree? Do I mark the post above as the solution?

It isn’t a bad idea, but _G is considered bad practice as it basically makes your variables kind of hidden and unorganized, so you’d want to avoid replicating that

I would suggest making your “storage” module organized, perhaps having multiple modules for different “groups” of data. If possible, put default values inside of it, that will provide autocomplete (could achieve that with type checking as well) hinting at what the module contains. Having methods is always nice, so if they make sense, add some

It is hard to give concrete suggestions though as we do not know what use case you’d have for it

if “methods” are functions that don’t return any thing, I have tons of them :D

I’m making a game where you can build your own things, for example, a car.
And one of my goals for it, was to be easy on 20 year old laptops and to be able to run smoothly on awful ping, so my game currently is one big - 3k lines of code - script, and I desperately need to simplify it, because it’s unreadable for me currently, let alone for someone else.

So I was wondering how to break it down into lots of smaller scripts.

Thanks!

Generally the advice is to have each piece of code do very specific tasks.

This usually means each script can mostly just use its own variables. So sharing then in that way is not recommended. I mean ultimately you need to share some information and that’s a good method, but it should be quite strictly controlled so one script can’t break another. So in a sense I would say it’s better to use bindables to pass information unless the module script is considered the owner of it and is the only thing allowed to directly change the variable (the other scripts can call functions that change the variable but try to limit who all can access it)

About methods, I just meant functions. They may or may not return anything

Oh man, yeah. I try to stay under 1 thousand line, that’s where scripts start becoming hard to manage

image

Breaking stuff down into smaller scripts is not something that (I think) can be taught like um, scripting. It’s kind of something you have to experiment with and develop over time? Well, I wouldn’t be able to tell you how to do it

Modularity is (from my perspective) really important. Instead of having 1 giant chunk of code, you break it down into many many modules, and instead of having 1 big system, you have multiple smaller systems that allow you to abstract away some of the code to keep the main code more concise. It’s a very similar idea to object oriented programming, but without objects per say

This approach has helped me create big projects, while avoiding bugs and keeping the codebase mostly organized. I will also add that it doesn’t really impact performance, unless you do some dubious design decisions

If it helps, here are some examples of how some modules are structured

Drop down to avoid having a very much too lengthy reply

Module for handling player stuff:

local RunService = game:GetService("RunService")
local DebrisService = game:GetService("Debris")

local ReplicatedStorage = game.ReplicatedStorage
local StarterPlayer = game.StarterPlayer
local Players = game.Players
local Characters = ReplicatedStorage.Characters
local Customization = ReplicatedStorage.Customization

local PlayerDatastore = require(ReplicatedStorage.Modules.DatastoreModule.PlayerDatastore)

local ModelTypes = {}
local RemoteCooldown = 2

local RESPAWN_DELAY = 5

local function LoadCharacter(Player : Player, Model : Model, Outfit : {any})

	-- // Unsure if this is needed, but this would prevent :LoadCharacter from using the wrong model if two players respawn at the same time
	local ExisitingCharacter = StarterPlayer:FindFirstChild("StarterCharacter")
	while ExisitingCharacter do
		ExisitingCharacter:GetPropertyChangedSignal("Parent"):Wait()
		ExisitingCharacter = StarterPlayer:FindFirstChild("StarterCharacter")
	end

	local Character = Model:Clone()
	Character.Name = "StarterCharacter"

	local CharacterModule = require(Character.CharacterModule)
	CharacterModule:LoadOutfit(Outfit)

	ModelTypes[Player] = Model

	Character.Parent = StarterPlayer

	Player:LoadCharacter()
	Character.Parent = nil -- Will get gc. Destroying it causes an error
end

Players.PlayerAdded:Connect(function(Player)

	Player.CharacterAdded:Connect(function(PlayerModel)
		local Humanoid : Humanoid = PlayerModel:WaitForChild("Humanoid")

		Humanoid.Died:Connect(function()
			task.wait(RESPAWN_DELAY)

			if not Player.Parent then return end

			local Outfit = PlayerDatastore:GetOutfit(Player.UserId, ModelTypes[Player].Name)
			LoadCharacter(Player, ModelTypes[Player], Outfit)
		end)
	end)
end)

Players.PlayerRemoving:Connect(function(Player)
	ModelTypes[Player] = nil
end)

local PlayerModule = {}

function PlayerModule:LoadCharacter(Player : Player, CharacterType : string, Outfit : {any})
	if ModelTypes[Player] then error("Player "..Player.UserId.." is already loaded") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?

	local Character = Characters:FindFirstChild(CharacterType)
	if not Character then error("Player "..Player.UserId.." tried to load with invalid Character type of "..CharacterType) end -- TODO -- Exploit logging

	local CharacterModule = require(Character:FindFirstChild("CharacterModule") or error("Character "..CharacterType.." does not have a CharacterModule"))
	local Outfit = CharacterModule:ValidateOutfit(Outfit)
	
	PlayerModule:SetReplicationFocus(Player, nil)

	LoadCharacter(Player, Character, Outfit)
	
	Player.ReplicationFocus = Player.Character
	
	return
end

local ValidTeams = {
	game.Teams.Danuwae,
	game.Teams.Versteckt,
	game.Teams["Black Creed"],
	game.Teams["Azure Dynasty"],
}

type TeamsType = 
	|"Danuwae"
	|"Versteckt"
	|"Black Creed"
	|"Azure Dynasty"

function PlayerModule:SelectTeam(Player : Player, TeamName : TeamsType) 
	if ModelTypes[Player] then error("Player "..Player.UserId.." tried to change teams while loaded in") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?
	
	local Team = game.Teams:FindFirstChild(TeamName)
	if not Team then error("Player "..Player.UserId.." tried to load with unknown Team of "..TeamName) end -- TODO -- Exploit logging
	if table.find(ValidTeams, Team) then error("Player "..Player.UserId.." tried to load with invalid Team of "..TeamName) end -- TODO -- Exploit logging

	Player.Team = Team
end

local Parts = {}

function PlayerModule:SetReplicationFocus(Player : Player, Position : Vector3?)
	if ModelTypes[Player] and Position ~= nil then error("Player "..Player.UserId.." tried to change replication focus while loaded in") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?
	if Position ~= nil and typeof(Position) ~= "Vector3" then error("Player "..Player.UserId.." sent a position for replication focus of type "..typeof(Position)) end  -- TODO -- Exploit logging
	
	if Parts[Player.UserId] then Parts[Player.UserId]:Destroy() end

	if not Position then Player.ReplicationFocus = nil return end

	local Part = Instance.new("Part")
	Part.Size = Vector3.one
	Part.CFrame = CFrame.new(Position)
	Part.Anchored = true
	Part.Transparency = 1
	Part.CanCollide = false
	Part.CanTouch = false
	Part.CanQuery = true
	Part.Name = Player.UserId.." ReplicationFocus Part"
	Part.Parent = workspace

	Parts[Player.UserId] = Part

	Player.ReplicationFocus = Part
end

function PlayerModule:Stun(Player : Player)
	local Character = Player.Character
	if not Character then warn("Cannot stun player "..Player.Name..". Player doesn't have a character") return end
	
	if Character:FindFirstChild("StunnedEffect") then
		Character:FindFirstChild("StunnedEffect"):Destroy()
	end
	
	local StunEffect = script.StunnedEffect:Clone()
	StunEffect.Parent = Character
	
	DebrisService:AddItem(StunEffect, 2)
end

Players.PlayerRemoving:Connect(function(Player : Player) 
	if not Parts[Player.UserId] then return end

	Parts[Player.UserId]:Destroy()
	Parts[Player.UserId] = nil
end)

--

local Bridge = script.Bridge -- RemoteFunction
local Remote = script.Remote -- RemoteEvent

local RemoteFunctions = { -- For functions that do not return anything
	"SelectTeam",
	"SetReplicationFocus",
	-- "Stun",
}

local BridgeFunctions = { -- For functions that return stuff & yield
	"LoadCharacter",
}

if RunService:IsServer() then 
	Remote.OnServerEvent:Connect(function(Player : Player, Index, ...)
		if not table.find(RemoteFunctions, Index) and not table.find(BridgeFunctions, Index) then error("Player "..Player.UserId.." is trying to access restricted methods "..Index) end -- TODO -- Report user as exploiter
		PlayerModule[Index](PlayerModule, Player, ...)
	end)
	
	Bridge.OnServerInvoke = function(Player : Player, Index, ...) 
		if not table.find(RemoteFunctions, Index) and not table.find(BridgeFunctions, Index) then error("Player "..Player.UserId.." is trying to access restricted methods "..Index) end -- TODO -- Report user as exploiter
		return PlayerModule[Index](PlayerModule, Player, ...)
	end
end

if RunService:IsClient() then
	for i, v in pairs(PlayerModule) do
		
		if table.find(RemoteFunctions, i) then 
			PlayerModule[i] = function(self, Player, ...)
				Remote:FireServer(i, ...)
			end
		end

		if table.find(BridgeFunctions, i) then 
			PlayerModule[i] = function(self, Player, ...)
				return Bridge:InvokeServer(i, ...)
			end
		end
		-- else, do not change
	end
end

return PlayerModule

I’ve done some unconventional stuff here at the end to make it so when some methods are called from the client, it actually sends a remote to the server and runs stuff on the server. The goal was to reduce the friction caused by remote events/functions, and I’d say, so far I like this design

Simple chaching module, for accessing the same property of an object multiple times within a frame:

-- // This module can be useful to improve performance when accessing the same property of an object multiple times within a frame

local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")

local Cache = {}

local function GetProperty(Object, Property)
	local IsEmpty = true
	for i, v in pairs(Cache) do IsEmpty = false break end

	Cache[Property] = Object[Property]

	if not IsEmpty then
		task.spawn(function() 
			RunService.Heartbeat:Wait()
			table.clear(Cache)
		end)
	end

	return Cache[Property]
end

local Methods = {}

function Methods:GetGuiSize(Object : GuiBase) : Vector2
	return GetProperty(Object, "AbsoluteSize")
end

function Methods:GetGuiPosition(Object : GuiBase) : Vector2
	return GetProperty(Object, "AbsolutePosition") + Vector2.new(0,GuiService.TopbarInset.Height)
end

function Methods:GetProperty(Object : Instance, Property) : Vector2
	return GetProperty(Object, Property)
end

return Methods

Module scripts don’t have to be complicated or complex

BindToClose wrapper for some quality of life:

local RunService = game:GetService("RunService")

local RunningTasks = {}
local Functions = {}

local BindToClose = {}

local IsClosing = false
function BindToClose:IsClosing()
	return IsClosing
end

function BindToClose:BindFunction(Function)
	table.insert(Functions,Function)
end

function BindToClose:BindTask(TaskName : string)
	if table.find(RunningTasks,TaskName) then error("Cannot bind a task more than once") end
	table.insert(RunningTasks,TaskName)
end

function BindToClose:UnbindTask(TaskName : string)
	local Index = table.find(RunningTasks,TaskName)
	if not Index then error("Cannot unbind a task that isn't bound") end
	table.remove(RunningTasks,Index)
end

if RunService:IsServer() then
	game:BindToClose(function() 
		IsClosing = true

		local BoundTasks = 0
		for i, v in ipairs(Functions) do 
			BoundTasks += 1
			task.spawn(function() 
				v()
				BoundTasks -= 1
			end)
		end

		while BoundTasks > 0 do task.wait(1) end

		while #RunningTasks > 0 do task.wait(1) end

		return
	end)
end

return BindToClose

Storing assets:

local AssetIds = {}

AssetIds.LeftArrow = 15705347309
AssetIds.RightArrow = 15705324962
AssetIds.UpArrow = 18883779034
AssetIds.DownArrow = 18883779221

return AssetIds

This is an idea I’m trying out, centralizing where the assets are into a module script. I think I like it, but I haven’t had the chance to really put it to the test

a GameState module, for managing the states of a round based game

local HttpService = game:GetService("HttpService")

local Utils = require(game.ReplicatedStorage.Modules.Utils)
local GAME_SETTINGS = require(game.ReplicatedStorage.GAME_SETTINGS)
local SignalModule = require(game.ReplicatedStorage.Modules.SignalModule)

local GameState = {}

local ActivePlayersFolder = game.ReplicatedStorage.ActivePlayers

local CurrentGamemode : string? = nil
local RoundId = nil
local ActivePlayers : {Player} = {}

local StartTime = 0

local Connections : {RBXScriptConnection} = {}

-- This is called before GameState:StartRound(), but it is garanteed that GameState:StartRound() will be called after
function GameState:LoadPlayer(Player)

	if not Player or not Player.Parent then return false, nil end
	Player:LoadCharacter()
	
	local Character = Player.Character
	if not Character then return false, nil end

	local Humanoid = Character:FindFirstChild("Humanoid")
	if not Humanoid then return false, nil end

	local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
	if not HumanoidRootPart then return false, nil end

	Player:SetAttribute("Playing", true)

	local PlayingTag = Instance.new("ObjectValue", ActivePlayersFolder)
	PlayingTag.Value = Player
	PlayingTag.Name = Player.Name
	
	local CharacterTable = {
		Character = Character,
		HumanoidRootPart = HumanoidRootPart,
		Humanoid = Humanoid,
	}
	
	GameState.PlayerLoadedSignal:Fire(Player, CharacterTable)

	return true, CharacterTable
end

function GameState:StartRound(Gamemode : string, Players : {Player})

	StartTime = os.clock()
	RoundId = HttpService:GenerateGUID(false)
	
	CurrentGamemode = Gamemode
	
	table.move(Players, 1, #Players, #ActivePlayers + 1, ActivePlayers)

	for _, Player in ipairs(Players) do
		table.insert(Connections, Player.CharacterRemoving:Connect(function()
			
			local Index = table.find(ActivePlayers, Player)
			if not Index then return end

			table.remove(ActivePlayers, Index)

			if Player:FindFirstChild("Creator") and Player:FindFirstChild(Player.Creator.Value) and Player.Team == Player:FindFirstChild(Player.Creator.Value).Team and Player.Team.Name ~= "Lobby" and Player.Team.Name ~= "Playing"  then
				local Cause = Player:FindFirstChild(Player.Creator.Value)
				game.ServerScriptService.Dialogue.Choose:Fire("Betrayal", Player, Cause)
			else
				game.ServerScriptService.Dialogue.Choose:Fire("Defeat", Player)
			end
			
			Player:SetAttribute("Playing", false)
			Player.Team = game.Teams.Lobby
			
			GameState.PlayerDiedSignal:Fire(Player)

			for i, Tile in pairs(workspace.Tiles:GetChildren()) do
				local PlayerNames = string.split(Tile.Owners.Value,".")

				local Index = table.find(PlayerNames,Player.Name)
				if not Index then continue end

				table.remove(PlayerNames,Index)

				local Owners = ""
				for i, v in ipairs(PlayerNames) do 
					local Prefix = i == 1 and "" or "."
					Owners = Owners..Prefix..v
				end

				if Owners == "" then
					Tile:SetAttribute("Active", false)
				end
				Tile.Owners.Value = Owners
			end
		end))
	end
	
	GameState.GameStartedSignal:Fire()
end

function GameState:EndRound()
	
	GameState.GameEndingSignal:Fire()
	
	StartTime = 0
	RoundId = nil
	
	CurrentGamemode = nil
	
	for _, Player in ipairs(ActivePlayers) do 
		Player:SetAttribute("Playing", false)
		Player.Team = game.Teams.Lobby
		Player:LoadCharacter()
	end
	
	table.clear(ActivePlayers)
	
	for _, v in ipairs(ActivePlayersFolder:GetChildren()) do
		v:Destroy()
	end

	for _, c in ipairs(Connections) do 
		c:Disconnect()
	end
end

function GameState:IsRoundActive()
	return RoundId ~= nil
end

function GameState:GetRoundId()
	return RoundId
end

function GameState:GetActivePlayers()
	return ActivePlayers
end

function GameState:SanitizeActivePlayersTable(ActivePlayers : {Player})
	local InvalidPlayers = {} -- Player no longer in game
	for i, Player in Utils.r_ipairs(ActivePlayers) do 
		if Player and Player.Parent then continue end
		table.insert(InvalidPlayers, Player)
		table.remove(ActivePlayers, i)
	end
	return ActivePlayers, InvalidPlayers
end

function GameState:IsPlayerActive(Player : Player)
	return table.find(ActivePlayers, Player) ~= nil
end

GameState.PlayerLoadedSignal = SignalModule.new()
GameState.PlayerDiedSignal = SignalModule.new()
GameState.GameStartedSignal = SignalModule.new()
GameState.GameEndingSignal = SignalModule.new()

function GameState:GetCurrentGamemode() : string?
	return CurrentGamemode
end

function GameState:GetRemainingTime()
	return math.clamp(GAME_SETTINGS.GAME_TIME_LIMIT - (os.clock() - StartTime), 0, GAME_SETTINGS.GAME_TIME_LIMIT)
end

function GameState:GetGameProgressionAlpha()
	return math.clamp(os.clock() - StartTime,0, GAME_SETTINGS.GAME_TIME_LIMIT)/GAME_SETTINGS.GAME_TIME_LIMIT
end

return GameState

Hope this helps

1 Like

oh no, the other scripts are solely there to change the variables as they update the world (hopefully that made sense and wasn’t too vague)

That makes sense and can be very useful. Though you may want to use a bindable or attributes instead (assuming I do understand). It would work though, just could itself end up being a bit much.

1 Like

Woah! How’d you get the old icons back?

Thank you!

And may I ask, why do you use “:” in your modules instead of “.”?
Thanks!

I’ll look out for it, thanks! :smiley:


Personal preference, I like how it looks. There is really nothing else to it. They don’t do the same thing, but I don’t use self either way, so, yeah. I wish both notations did the same thing to avoid confusion…


Also, to write :D without a codeblock or having it turning into a smiley face, you can use an html comment tag in the middle: :<!-- -->D

1 Like

Thank you again!

:p
:/
:D

Thank you again (lol) for the tip!

1 Like