Disclaimer: This is not an industry-standard-level guide and even some people behind the practices I personally endorse might not endorse my own practices. I’ll try to highlight my personal preferences that may go against convention. If you wish to suggest changes PLEASE CONSIDER sending me a personal DM so we could avoid trivial public discussion.
:IsA() →
This is the Mad Studio coding style and workflow resource for modular code. It’s a work in progress and may change over time. It’s targeted to benefit smaller development groups or an individual coder. Code longevity and easy maintenance after long periods are the core values of this approach.
You will notice my favoritism of the Roblox API variable casing (I believe PascalCase class members make it easier to seamlessly integrate Lua code with Roblox API variables and methods) and preference towards more “contrast” between global and local variable scopes.
You may use this resource as a guidebook in your personal programming collaboration, get to understand Mad Studio code better or even help make your own version of the guidebook to suit your needs.
I expect you to google up terms you might not be familiar with so I could keep my material short.
3 →
The three sacred resources:
- Best Practices for Clean Code by @evaera
- Lua Performance Tips, Chapter 2 by Roberto Ierusalimschy
- Roblox Lua Style Guide by Roblox
If I don’t cover something in this document then the resources above are to follow. A lot of Mad Studio style and decision making is derived from these three, but some aspects will differ and these differences will be explicitly stated here.
Terminology 101 →
A few special terms you must understand:
Term | Definition |
---|---|
Member | A value within a module table or class instance table |
Function | A literal Lua function (which may also be a Member - in this case it’s a public module function and will be declared to a table with a preceding dot (.)) |
Method | A Function declared to a table with a preceding colon (:) and a Member of a class |
Local | (Scope - Wikipedia) In Roblox Lua, the typed keyword local is a prefix for every variable declaration - THIS IS NOT WHAT THIS TERM IS REFFERING TO - Within the boundaries of a script, any variable declared inside a function, do block or a loop is going to be referred to as a local variable
|
Global | The opposite of a local variable - Global variables are declared outside of functions and above said functions thus they will be globally accessible throughout most of the script. |
Private | Context of a variable, function or class that is not supposed to be accessed by other scripts (modules) |
Public | Context of a variable, function or class that can be accessed by other scripts (modules) |
Class | (Class - Wikipedia) Could refer to either a Roblox API class or a user-defined Lua class; A class can be instantiated (creating an object belonging to that class) |
Instance / Object | A data structure belonging to a class; Instances usually imply a Roblox API class while objects imply a Lua class |
Component | An object declared as a member of another object and specifically created to extend the functionality of that particular object |
Forward declaration | (Forward declaration - Wikipedia) Forward declaration is used when a conventionally structured code has internal dependencies between structure blocks (top of the script depending on the bottom of the script). A forward declared variable is usually set to nil at the start of the script and defined much lower in the script, before functions referencing it are called. |
Script structure →
Mad Studio script structure is different from Roblox proposed standard. The Mad Studio needs a different structure standard because we’re allowing modules to group several public and private classes under a single script - a less than conventional decision to avoid splitting into more modules when the classes are not big or are unlikely to be useful stand-alone.
Individual structure blocks or their components can be removed if the Script / ModuleScript does not have the relevant features.
Structure block | What does it contain |
---|---|
0. Global namespace (Optional) | If working with a framework or a module loader, the first line of the script is going to be a reference to the framework’s namespace or the module loader’s function. |
1. Script header | Defines the name of the project this module belongs to; Defines module’s-own name (It’s good for screenshots); Lists all publicly accessible variables (members) and functions / methods along with their expected inputs and outputs, usage warnings, notices on exclusive behavior and recommendations. Individual input / output parameters must be defined along with their value types with occasional exceptions of a few variable names that have implicit type: - Suffix _name implies string type - Prefix is_ implies bool type - Suffixes _count or _amount imply number type, although it might be useful to be explicit via comments on whether it’s an integer - Variable name same as a defined class implies instance / object type The developer actively maintains this section with changes to the public members / functions / methods in the script. |
2. Constants (Settings) | All static values are defined here, under a global table variable SETTINGS . When working with intense procedural code, constants should be referenced again by private variables (declared later in the script) to prevent table indexing overhead |
3. Module table | Declare a table which is returned at the end of a ModuleScript; The module table must have all non-function member keys defined even if they’re nil; In this structure block, module table members should be defined using basic value types (not class instances) otherwise they’re set to nil (with comments describing the expected value type / class name) and defined in the Initialize structure block to relief the upper part of the script from large pieces of prefabs / “hard-code” and give more focus to other structure blocks |
4. Module dependencies | Global variables of references to required ModuleScripts; Framework service-level modules should be higher, game source modules should be lower, utility modules (e.g. spring math, maid, signal) should be lowest - As a general rule of thumb, move less common features (among the rest of the codebase) higher |
5. Private variables |
Order from top to bottom: 1. References to Roblox services ( game:GetService() )2. One-line-per-RemoteEvent definitions (Prefix with rev_) (If your codebase supports it) - Instantiating RemoteEvents belonging to this module, one line of code each. 3. Forward declaration for class tables for dealing with internal circular dependencies or instantiating public classes from private functions 4. Performance references (Table reference avoiding at runtime); Only use for intense procedural code or tables that are members of a module table within the same script; References to SETTINGS members (Prefix with “sett_ ”), references to values from other modules 5. “Basic” class instantiation (If exceeding 40 lines, consider declaring a global variable with nil and defining it in the Initialize structure block) 6. Internal state variables (e.g. incrementing indexes, reference to the current map) |
6. Private functions |
Order from top to bottom: 1. Private classes 2. Global functions used in Public functions or Public class methods |
7. Public |
Order from top to bottom: 1. Public classes 2. Module functions |
8. Initialize | Config of other ModuleScripts; Defining Module table members with classes from Module dependencies; Creating prefabs (usually in do blocks) |
9. Connections | RemoteEvent connections; .PlayerAdded / .PlayerRemoving connections; Game step (e.g. Heartbeat) connections; Other kinds of singleton listeners |
Mad Quirks →
Rules exclusive to the Mad™ coding style; Anything not mentioned here (or above) has to follow the Roblox Lua Style Guide.
Naming:
- SETTINGS is the only UPPER_CASE variable in the entire codebase; Performance global variables referencing SETTINGS constants are prefixed with sett_ (e.g. sett_WavelengthPeriod); Prefix with deriv_ when a constant is altered (e.g. constant is inverted via 1 / x)
- Local scope variables are written in snake_case (including function arguments)
- Dictionary keys are PascalCase unless they’re private class members in which case they’re declared in _snake_case with an underscore prefix.
- Class method calls and module function calls are :PascalCase() and .PascalCase() respectively.
- “Private” class methods are written as :_PascalCase()
- RemoteEvents follow the same naming rules, but are prefixed with rev_ (e.g. rev_ShootGun)
- NEVER shorten variable names (rare exceptions for heavy math or long terminologies with no synonyms)
Structure:
- We reuse classes through “objects as components” instead of class inheritance.
- “Truthful” checks in if statements are only limited to ternary expressions; Otherwise be explicit with nil or boolean checks by suffixing “== nil”, “== false” or “== true”
- Metatables are only used for providing class methods (__index) and rarely for mock “Roblox instances” (e.g. A Lua class with
Animator
Roblox class members, but it repeats value changes for several real RobloxAnimator
instances allowing the developer to use the same animator player code for multiple animation rigs) - Code is not allowed to be halting (yielding) with the exception of halting methods or functions with a Async prefix (e.g. :LoadProfileAsync()). Callbacks and event listeners are always preferred over yielding.
Class declaration:
The Mad Studio coding standard promotes multiple class packing into a single ModuleScript / Script, so some aspects differ from the Roblox style guide.
- Class metatables are always declared separately from the module table (Module table is the table returned by a ModuleScript on require) - A module table cannot be a class metatable.
- Class metatables do not have a .new() function and are instead instantiated manually (Create instance table and set class metatable) inside private functions (with forward declaration) or public module functions
- Public module functions which return a class instance have to follow the naming standard .NewClassName() (e.g. .NewScriptSignal()) - ModuleScripts do not expose class metatables themselves.
These rules split class method declaration and class instantiation between two or or more structure blocks which will look pretty sketchy, but Mad Studio standard modules can be both a singleton class and provide instances of classes - internally declared classes are encouraged to be entangled with other global variables / functions within the script.
Mandatory commenting beyond script header:
- Structure blocks are separated by generic comment lines (like bowling bumper rails lol)
- All functions with value returns must be followed with a “comment arrow” (-->), returned variable names in snake_case (optional if an instance is returned) and their [ValueTypes] (e.g. [bool], [ScriptSignal], [string], [BasePart], [number], [integer]) (Primitive value types should use the letter case returned by typeof())
- Class metatables, at declaration, must contain square bracket multi-line comments describing every private member belonging to that class (_snake_case table keys)
Workflow →
A structure helps me personally know where to start when creating a new module; This is purely a workflow suggestion:
-
Design names and types for all members, define function / method arguments (input / output) for the module table and internal classes in the script header before writing any code. It’s super useful to have this made first since you can discuss code operation with other people early.
-
Declare empty functions / methods / class metatables according to the script header (don’t forget mandatory comments on function returns and private class members); Fill in module table members in the module table.
-
Begin filling in module dependencies, private variables, constants (SETTINGS).
-
Begin writing the public script layer (if present) starting from functions that instantiate classes.
-
Start filling in empty functions from top to bottom of the script.
-
Complete the initialize and connections structure blocks.
Code snippets →
Figure 1: Structure blocks
local Madwork = _G.Madwork
--[[
{Project}
-[ModuleName]---------------------------------------
Module description
Members:
Functions:
Members [ClassName]:
Methods [ClassName]:
--]]
local SETTINGS = {
}
----- Module Table -----
local Module = {
}
----- Loaded Modules -----
----- Private Variables -----
----- Private functions -----
----- Public -----
----- Initialize -----
----- Connections -----
return Module
Figure 2: Script header
"BackpackClient"
--[[
{Madwork}
-[BackpackClient]---------------------------------------
Core tool behaviour handling client-side;
Functions:
BackpackClient.NewBackpack(character) --> [Backpack]
BackpackClient.SetToolSetupFunction(func) -- A function that's called for every created tool before .ToolAdded is triggered
func [function] (tool)
BackpackClient.GetToolById(tool_id) --> [Tool] or nil
Members [Backpack]:
Backpack.ToolAdded [ScriptSignal](tool)
Backpack.SlotsChanged [ScriptSignal] -- Fired when the client should redraw the backpack UI
Methods [Backpack]:
Backpack:GetTools() --> {tool, ...}
Backpack:GetEquippedTool() --> [Tool] or nil
Backpack:GetToolInSlot(slot) --> [Tool] or nil
Members [Tool]: -- (Everything is read-only)
Tool.Id [number]
Tool.Character [Character]
Tool.Backpack [Backpack]
Tool.IsLocal [bool]
Tool.Equipped [bool]
Tool.Setup [table]
Tool.State [table]
Tool.Slot [number]
Tool.Usage [ToolUsage] or nil -- Set for tool owners only
Tool.StateChanged [ScriptSignal] (state_name, value)
Tool.OnEquipped [ScriptSignal]
Tool.OnUnequipped [ScriptSignal]
Tool.Signal [ScriptSignal] (...) -- Used to replicate custom tool behaviour events to all players
Tool.Functions nil or [table]: {FunctionName = func, ...} -- func(tool, ...) --> [...] -- Indexed by Tool:Function()
Tool.Store [table] -- Custom reference table for storing objects / settings relevant to this tool
Methods [Tool]:
Tool:IsActive() --> [bool] -- Returns false if the tool is destroyed
Tool:Equip()
Tool:Unequip()
Tool:Function(function_name, ...) --> [...] -- Used to bridge game code to tool behaviour code
Tool:ClientAction(...)
-- Cleanup:
Tool:AddCleanupTask(task)
Tool:RemoveCleanupTask(task)
Members [ToolUsage]:
ToolUsage.IsSet [bool] -- false, if UsageSettings was not defined during tool creation
ToolUsage.Settings [table] -- Usage settings that were set during tool creation
ToolUsage.Clip [number]
ToolUsage.Ammo [number]
ToolUsage.IsReloading [bool]
ToolUsage.ClipChanged [ScriptSignal] (clip, ammo) -- Also triggered when ammo changes
ToolUsage.ReloadingChanged [ScriptSignal] (is_reloading)
ToolUsage.OnClipAmmoSet [ScriptSignal] () -- When this signal is triggered, the client-side
-- tool state machine has to reset; Equipped tools will not have to be reequipped
Methods [ToolUsage]:
ToolUsage:ReloadStart()
ToolUsage:ReloadComplete()
ToolUsage:ReloadCancel()
ToolUsage:UseClip()
--]]
"RaycastUtil"
--[[
{Madwork}
-[RaycastUtil]---------------------------------------
Raycast utilities
Functions:
RaycastUtil.Raycast(raycast_params) --> [RaycastResult] or nil -- Simplified Workspace:Raycast() function
raycast_params [table]: {
Origin = vector3,
Direction = vector3, -- (unit_vector * distance)
-- Optional params:
CollisionGroup = "Default",
Blacklist = {ignore_part, ...},
Filter = function(base_part), -- Return true if part has to be blacklisted
}
Members [RaycastResult]:
RaycastResult.Instance [BasePart] -- [Terrain] is also a [BasePart]
RaycastResult.Position [Vector3]
RaycastResult.Material [Enum.Material]
RaycastResult.Normal [Vector3]
--]]
Figure 3: Constants (SETTINGS)
Snippet from “MadworkCaster” - MadworkCaster performs intense raycasting math, so indexing SETTINGS to read constants at runtime would pose a significant amount of overhead. Event-based scripts should index SETTINGS at runtime (e.g. Humanoid.Health -= SETTINGS.CactusDamage
)
local SETTINGS = {
PhysicsLogicalFramerate = 60,
HitscanStepLength = 10,
CasterWorkerStepHeap = 50,
CasterWorkerDefaultBudget = 1, -- milliseconds
RobloxMaxRaycastLenght = 5000,
}
----- Module Table -----
local MadworkCaster = {
}
----- Private Variables -----
local Workspace = game:GetService("Workspace")
local RaycastParamsObject
-- Performance references:
local sett_PhysicsLogicalFramerate = SETTINGS.PhysicsLogicalFramerate
local deriv_PhysicsLogicalFrameTime = 1 / sett_PhysicsLogicalFramerate
local sett_HitscanStepLength = SETTINGS.HitscanStepLength
local sett_CasterWorkerStepHeap = SETTINGS.CasterWorkerStepHeap
local sett_CasterWorkerDefaultBudget = SETTINGS.CasterWorkerDefaultBudget
local sett_RobloxMaxRaycastLenght = SETTINGS.RobloxMaxRaycastLenght
Figure 4: Module dependencies
----- Loaded Services & Modules -----
local ContentService = Madwork.GetService("ContentService")
local CharacterService = Madwork.GetService("CharacterService")
local ReplicaService = Madwork.GetService("ReplicaService")
local MadworkServerSettings = require(Madwork.GetModule("Madwork", "MadworkServerSettings"))
local RateLimiter = require(Madwork.GetModule("Madwork", "RateLimiter"))
local GameAction = require(Madwork.GetShared("Madwork", "GameAction"))
local MadworkDebris = require(Madwork.GetShared("Madwork", "MadworkDebris"))
local RaycastUtil = require(Madwork.GetShared("Madwork", "RaycastUtil"))
Figure 5: Private variables
"ReplicaService"
----- Private Variables -----
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rev_ReplicaRequestData = Madwork.SetupRemoteEvent("Replica_ReplicaRequestData") -- Fired client-side when the client loads for the first time
local rev_ReplicaSetValue = Madwork.SetupRemoteEvent("Replica_ReplicaSetValue") -- (replica_id, {path}, value)
local rev_ReplicaSetValues = Madwork.SetupRemoteEvent("Replica_ReplicaSetValues") -- (replica_id, {path}, {values})
local rev_ReplicaArrayInsert = Madwork.SetupRemoteEvent("Replica_ReplicaArrayInsert") -- (replica_id, {path}, value)
local rev_ReplicaArraySet = Madwork.SetupRemoteEvent("Replica_ReplicaArraySet") -- (replica_id, {path}, index, value)
local rev_ReplicaArrayRemove = Madwork.SetupRemoteEvent("Replica_ReplicaArrayRemove") -- (replica_id, {path}, index)
local rev_ReplicaWrite = Madwork.SetupRemoteEvent("Replica_ReplicaWrite") -- (replica_id, func_id, params...)
local rev_ReplicaSignal = Madwork.SetupRemoteEvent("Replica_ReplicaSignal") -- (replica_id, params...)
local rev_ReplicaSetParent = Madwork.SetupRemoteEvent("Replica_ReplicaSetParent") -- (replica_id, parent_replica_id)
local rev_ReplicaCreate = Madwork.SetupRemoteEvent("Replica_ReplicaCreate") -- (replica_id, {replica_data}) OR (top_replica_id, {creation_data}) or ({replica_package})
local rev_ReplicaDestroy = Madwork.SetupRemoteEvent("Replica_ReplicaDestroy") -- (replica_id)
local DefaultRateLimiter = RateLimiter.Default
local ActivePlayers = ReplicaService.ActivePlayers
local Replicas = ReplicaService._replicas
local TopLevelReplicas = ReplicaService._top_level_replicas
local ReplicaIndex = 0
local LoadedWriteLibs = {} -- {[ModuleScript] = {["function_name"] = {func_id, function}, ...}, ...}
local WriteFunctionFlag = false
local CreatedClassTokens = {} -- [class_name] = true
Figure 6: Class declaration
Snippet from “RateLimiter” - See the whole script
----- Public -----
-- RateLimiter object:
local RateLimiterObject = {
--[[
_sources = {},
_rate_period = 0,
--]]
}
RateLimiterObject.__index = RateLimiterObject
function RateLimiterObject:CheckRate(source) --> is_to_be_processed [bool] -- Whether event should be processed
local sources = self._sources
local os_clock = os.clock()
local rate_time = sources[source]
if rate_time ~= nil then
rate_time = math.max(os_clock, rate_time + self._rate_period)
if rate_time - os_clock < 1 then
sources[source] = rate_time
return true
else
return false
end
else
-- Preventing from remembering players that already left:
if typeof(source) == "Instance" and source:IsA("Player")
and PlayerReference[source] == nil then
return false
end
sources[source] = os_clock + self._rate_period
return true
end
end
function RateLimiterObject:CleanSource(source) -- Forgets about the source - must be called for any object that
self._sources[source] = nil
end
function RateLimiterObject:Cleanup() -- Forgets all sources
self._sources = {}
end
function RateLimiterObject:Destroy() -- Make the RateLimiter module forget about this RateLimiter object
RateLimiters[self] = nil
end
-- Module functions:
function RateLimiter.NewRateLimiter(rate) --> [RateLimiter]
if rate <= 0 then
error("[RateLimiter]: Invalid rate")
end
local rate_limiter = {
_sources = {},
_rate_period = 1 / rate,
}
setmetatable(rate_limiter, RateLimiterObject)
RateLimiters[rate_limiter] = true
return rate_limiter
end
You can understand the Mad Studio code structure better through Mad Studio open-sourced projects:
- ProfileService
- ReplicaService
- MadworkZero (Not actively maintained as of writing)
Please note that this coding standard thread was written AFTER the code referenced above was written, so certain aspects may be a little off (like lack of function argument comments or disordered declarations)
A few tradeoffs →
I’m not looking for people to help me fill this list, but this should be relevant to you if you’re only interested in this article for your own business - I know that the Mad Studio coding standard has a few trade-offs:
- It’s not that good for unit testing - People interested in having their codebase support unit tests should probably limit themselves to one class per one ModuleScript and make private functions part of class metatables. I’m more of a feature testing gang guy.
- Heavy use of underscores (snake_case) might be a nuisance to programmers with foreign keyboard layouts. Should such people just buy new keyboards? Jokes aside, I seriously don’t know - let me know if you’re one of the people who are having serious problems with using underscores (due to keyboard layout / health condition?).
- The SETTINGS table can become a lengthy nuisance for complex modules, but that happens rarely.
- The top Roblox game studios might be following the stock Roblox style guide and this might just mess with your brain a little if you’re going to work for them, lol.
This guide is currently a “prototype” - changes can be applied over time.
By loleris, lead developer of Mad Studio