Programming Formalities Guide
Written and Produced by MapleMoose | @Map1eMoose, Head Developer at Dreamlabs Studios.
- Introduction
- Program Design
- The Explorer Tab
- Module Scripts
- Monetization
- Data Stores / Data Management
- Variable and Function Naming
- UI Navigation
- Optimizations
- Debugging Methods
Introduction
This is a full guide to programming style on Roblox. This guide does not focus on how to program but more how you should program. ie. what everything in a working game should look like, accounting for modularity, code readability, multiple programmers and future-proofing. This guide doesnât just cover scripting but also the explorer and how scripts and certain game elements should work together. This guide focuses on a broad or large sense of code organization, if your looking for specific code formatting guides, here are two recommendations: Lua Style Guide, MadLife Code Guidebook.
This is NOT a professional standard guide. It is entirely possible that any information here may not be 100% factually correct. Most of the stuff in this guide was self-learned from developing (game to be released), 4 years of Roblox development experience, university, and working with 7 other programmers (each with a unique background and style). Some of these ideas and concepts are personal opinions and could be approached differently from another perspective. There is not one single correct answer to approaching this topic.
One thing to keep in mind is that not every experience is built the same. Some experiences may require things that others donât. Copying 1 for 1 from this guide may not be completely applicable to your development needs.
Program Design
This section is based from a University programming class on how to solve problems and how to design and implement efficient code.
Decomposition
Decomposition is a design strategy in which it focuses on breaking down a problem into smaller tasks. When approaching a problem, break the problem down into a small number of tasks. Break each of those down into smaller subtasks. Each small subtask and becomes its own function. A good decomposition allows for code-reuse and makes it easier for future changes to the program.
Decomposition lets you focus on âwhat should this program doâ instead of âwhat are the commands to do itâ. It lets you design the whole system at once.
Information Hiding
Identify tasks that might be useful in other contexts, write the function with enough flexibility to make it easier to reuse. Keep these functions as independent as possible from the rest of the system. With proper documentation, this allows other programmers to use those functions in many different contexts without knowing how the function works.
Information hiding is giving a programmer a function with a description, parameters and return value(s). There are already functions like this, for example: math.abs(x)
(the absolute value function). You donât need to understand how this function is doing its calculations, all you need to know is its parameters, return value and a brief description of what it does.
A function represents a task. You can use functions to hide its implementations, which make the program easier to follow and easier to read. Functions contain code thats used more than once, easier and faster to write, modify and fix. On large multi-programmer projects, information hiding is key for allowing programmers to use each others functions without knowing how it works.
Documenting Functions
A functionâs description should tell the user what it needs and what it does. It doesnât need to say how it does its task.
- Pre-conditions
- Describe any and all parameters. Are there any restrictions on those parameters?
- Describe any assumptions or anything for the function to work correctly.
- Post-conditions
- Return values
- Changes the function makes to other objects
- Output or other effects.
- Yield(s)
- Does or can the function yield?
Testing Code
You should try to test functions individually. You should choose inputs where you know the correct output. Choose inputs that are likely to cause errors. You should ensure that every line of code in your function is ran at least once. If a input is tested and it yields an incorrect output, go back and fix the function but retest all the previous inputs as well. This ensures that any changes you made did not break anything else unintentionally.
The Explorer Tab
The following outlines how each category should be organized.
Workspace
Holds any world models and the game play area.
ReplicatedFirst
Should hold your custom loading screen GUI and / or model (if applicable) and nothing else.
ReplicatedStorage
Replicated Storage should hold numerous folders for different systems. An âAssetsâ folder should be used for general assets. An example Replicated Storage could look like:
- Assets : Folder (Stores any miscellaneous assets used by client and server.)
- Dances : Folder (Stores any dances for a dance system)
- Inventory : Folder (Stores any items used in a inventory system)
- PetsStorage: Folder (Contains any pet models or any assets used with pets).
- Modules: Folder (Contains any client/server used modules which are accessed by multiple scripts)
Its important to keep everything sorted so assets can be easily located by the developer for easy modification. Folders such as Inventory should contain subfolders with different types of items such as: [Hats, Armors, Potions, etcâŚ]. Organization is important.
ServerScriptService
ServerScriptService should contain any server-sided scripts. If deemed necessary, organize your server scripts into folders.
ServerStorage
Not really used. It could be used to store assets hidden from players, similar to ReplicatedStorage but if a player has access to it, it might as well stay in ReplicatedStorage. If its a large asset such a map, keep it in ServerStorage, if its a small asset such as a sword or an armor, keep it ReplicatedStorage. ServerStorage could contain BindableEvents for custom events fired from the developer console.
StarterGui
Contains all the UI the player loads in with. No scripts should be direct children of StarterGui. Any local scripts which have StarterGui as its ancestor should have a ScreenGui as its parent.
StarterPlayer.StarterCharacterScripts
Any scripts that are associated with the playerâs character model.
StarterPlayer.StarterPlayerScripts
Any client scripts not associated with UI.
Module Scripts
Roblox developers have create some very helpful open-source modules. Modules like: âBridgeNet2â, âFusionâ, "FastCast, âProfile Serviceâ, âTeam Makerâ, etc, are incredibly helpful with programming as they add extra tools and functionality to make the process more simple and optimized. These modules can be used / accessed by only one or multiple scripts. When adding a module script to your game, there are three main places where it could go:
- With its associated script(s) (if its only accessed by only one system)
- In ServerStorage or ServerScriptService (if the module is only accessed on the server)
- In ReplicatedStorage (where it is accessible by the clients and the server)
When placing these modules, keep them organized and properly named. If multiple scripts access the same module, that module should be in a common accessible place. If only one script uses the module, then it should be stored with that script (usually a child of the script). Try not to have duplicate module scripts throughout your game, only one copy of each module should exist in a game. When importing a module in a script, you should name its variable the same as the moduleâs instance name.
Common Functions Module
Almost every experience should have Common Functions Module. This module would be custom developed and tailored to each individual experience. This module would contain any common functions which are used by multiple scripts, such as GetRank/RoleInGroup
or DeepCopyTable
. The functions in this module should be broad enough where they are applicable in multiple scripts. If you need to store some global constants through your game, this module would also be a good place to do so.
As mentioned in Program Design â Documenting Functions & Information Hiding, each of the functions in this module should be well commented to where other programmers donât need to know how the function works, they just need to know input, output and any other changes.
Client Settings
If your game has custom client settings, you should program it into a module script. This allows any other client scripts to require
that script and access the settings values directly. You could also introduce callback functions where if the player modifies a setting value, the settings module would call the callback-functions associated with that value, this allows any other scripts which operate around that setting value to change on an event.
Notes Module
As simple as it may seem, it is recommended for experiences to have a Notes module. This module wouldnât contain any code, it would be all comments --[[ ]]
. Anything information worthy could be stored in this module such as a: Todo list, Things to change, bugs to be fixed or dev console commands.
Monetization
There are three main components to monetization: prompting client purchases, managing purchases on the server, and the product / gamepass Ids. To start, the product Ids should all be stored in a module in ReplicatedStorage in a similar format:
local MonetizationIds = {}
---------------Gamepasses-------------------
MonetizationIds.VIP = 1234567890
--------------- Dev Products-------------------
MonetizationIds["Miscellaneous"] = {
["Request Song"] = 2345678901
},
MonetizationIds["PetEggs"] = {
["Common"] = 3456789012,
["Rare"] = 5678901234,
["Legendary"] = 6789012345,
["Mythical"] = 7890123456,
}
MonetizationIds["Currency"] = {
["Bucks-250"] = 987654321,
["Bucks-900"] = 654321897,
["Bucks-2800"] = 987321654,
}
return MonetizationIds
This sort of format allows for other scripts to easily access the product ids which they need. If product Ids are stored individually in scripts, it can be very difficult to change the Ids if the files moving to a different place. Client scripts should access these Ids directly:
MarketplaceService:PromptProductPurchase(LocalPlayer,MonetizationIds.Miscellaneous["Request Song"])
On the server, we can only use one MarketplaceService.ProcessReceipt
function which can make it difficult working with different systems or multiple programmers. The server should contain one script responsible for picking up the MarketplaceService.ProcessReceipt
request and divert those requests to a module script associated with that category.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
local MonetizationIds = require(ReplicatedStorage.Modules.MonetizationIds)
local PetModule = require(script.PetEgg) -- PetEgg module which manages purchases related to pet eggs
local CurrencyModule = require(script.Currency) -- Currency module which manages purchases related to currency
--------------------------------------------------------------------------------------------------------------------------
-- function sees if any value in the dictionary matches the needle (find a needle in the haystack)
local function dictFind(dict: dictionary, value: needle)
for i,v in dict do
if v == value then
return i
end
end
end
-- function to determine which system the purchase is related too.
local function ClassifyPurchase(Id)
if dictFind(MonetizationIds.PetEggs, Id) then
return "PetEgg"
elseif dictFind(MonetizationIds.Currency, Id) then
return "Currency"
end
end
--------------------------------------------------------------------------------------------------------------------------
local function processReceipt(receiptInfo) -- Invoked function
local Result = Enum.ProductPurchaseDecision.NotProcessedYet -- create result variable, assume purchase failed
local purchase = ClassifyPurchase(receiptInfo.ProductId) -- see which system the purchase belongs to
pcall(function() -- attempt the purchase
if purchase == "PetEgg" then
Result = PetModule.Process(receiptInfo)
elseif purchase == "Currency" then
Result = CurrencyModule.Process(receiptInfo)
end -- result is whether the purchase was successful or not
end)
return Result -- return the result to the invoked function
end
MarketplaceService.ProcessReceipt = processReceipt -- Initial function Invoke
This scripts takes the MarketplaceService.ProcessReceipt
, finds which category it belongs to, and fires the associated module to deal with that specific purchase.
Data Stores / Data Management
In many games Data Stores are accessed constantly by many different scripts. This guide will focus on using ProfileService with a custom DataManager script to manage players data across different scripts.
Our module uses profile service to load in the data, then gives access to any scripts who need that data. Its up to the individual scripts that when overwriting / updating data, it is of appropriate type and value. An example very simplified DataManager module: This script on its own wont work, but shows the concepts that would go into a DataManager module.
local PlayerService = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ProfileService = require("ProfileService")
local DataManager = {}
DataManager.PlayerDataStores = {} -- Where all the player's data is stored, this dictionary is accessed by the scripts.
local function GetTemplateData()-- Returns the default data table,
return { -- Template data could look something like this
["Inventory"] = {},
["Pets"] = {},
["Stats"] = {
["Level"] = 1,
["EXP"] = 0,
}
["Settings"] = { --General Settings
["TimeOfDay"] = false, -- Day == true
["Rain"] = false,
["HidePlayers"] = false,
["HidePets"] = false,
["MusicVolume"] = 50,
["SFXVolume"] = 50,
["NameTagColor"] = "ffffff",
["RedeemedCodes"] = {},
},
["Gold"] = 50,
["PurchaseLog"] = {},--Roblox Purchases, receiptInfo.PurchaseId
}
end
local function RepairTable(DataTable, defaultTable)end -- Ensures Data table matches the default data // adds any missing indexes/ sub dictionaries
local ProfileStore = ProfileService.GetProfileStore(
"MyGameData_01",
GetTemplateData()
)
local Profiles = {}
-- Player Joined
PlayerService.PlayerAdded:Connect(player)
local UserID = player.UserId
local profile = ProfileStore:LoadProfileAsync(tostring(UserID))
Profiles[UserID] = profile
local Data = DataManager.PlayerDataStores[UserID]
Data = RepairTable(Data, GetTemplateData())
DataManager.PlayerDataStores[UserID] = Profiles[UserID].Data
end
--Player Left
PlayerService.PlayerRemoving:Connect(function(player)
Profiles[player.UserId]:Release()
end)
-- Save Data
DataManager.SaveData = function(UserID)
Profiles[UserID]:Save()
end
return DataManager
If a server script wishes to access this data they should require the module and use the âDataManager.PlayerDataStoresâ dictionary to get what they need. Here is an example of a basic item purchasing from a shop script:
GetRemoteFunction("PurchaseItem").OnServerInvoke = function(player, itemId)
local userId = player.UserId
local playerData = DataManager.PlayerDataStores[userId] -- Get player's data
if not playerData then -- ensure data exists
return
end
local purchaseAccepted = doSomeTransaction(player, itemId) -- function to do currency math and give player the item
--Ensure enough currency
if purchaseAccepted then
table.insert(playerData.Inventory, newItemData)-- add items to inventory
else
warn("Purchase failed: " .. err)
return
end
DataManager.Save(userId) -- save data when done
return true
end
If a client ever needs data, the client should fire a remote to its associated server script (client shop script requests the server shop script) and that server script accesses the data directly.
Variable and Function Naming
Common variables like player or services should be defined the same way across all scripts. Variables and functions should be named based off what its referring to. Variable names should be concise but also long enough to know what they are. Variable names that are 3-4 characters or less can be confusing. Variable names should not be too vague; names like: data
or connection
tells the reader very little of what that variable is actually for. You donât need to follow the below examples on what variables to name in PascalCase or camelCase or snake_case, the main important concept in this section is script readability and consistency throughout your whole project.
Objects / Instances
Any objects should be named in PascalCase. This includes stuff like frames, parts, models, folders and players.
ex. local ShopFrame = script.Parent.ShopFrame
Primitive Values / Functions
All primitive values such as numbers, strings, booleans or any variable inside a script which does not reference any external objects should be camelCase.
All functions/methods inside of scripts or modules should be named with camelCase.
ex. local numberOfPlayer = #PlayerService:GetPlayers()
ex. local userId = Player.UserId
ex.
local function addTwoNumbers(num1: number, num2: number)
return num1 + num2
end
Constants
Lua does not support constants like other languages. We can still attempt to create constant values by defining them as UPPERCASE.
This tells the programmer that the variable is a constant and should only be read from, and not modified during program execution.
ex. local XP_GENERATED_PER_MINUTE = 50
To help emphasize this concept, here are some of the most common variables in scripts and how they should be defined. Consistency between scripts helps readability and modularity.
Services
local PlayerService = game:GetService("Players") -- I call it `PlayerService` instead of `Players` because `Players` is a common variable name which is often used and `PlayerService` is more clear what its referencing.
local Lighting = game:GetService("Lighting")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local StarterGui = game:GetService("StarterGui")
local MarketplaceService = game:GetService("MarketplaceService")
local UserInputService = game:GetService("UserInputService")
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local MemoryStoreService = game:GetService("MemoryStoreService")
Players
local Players = PlayerService:GetPlayers()
RemoteEvent.Event:Connect(function(Player) end)
local LocalPlayer = PlayerService.LocalPlayer
For-Loops
For-loops like for i = 0, 3, 1 do
(iterating through a numeric number) are fine to have single letter variables.
For-loops that iterate though tables (arrays) or dictionaryâs can have single letter variables only if the table is described with a comment before the loop. If no comments are present, the two variables should be properly named. Variables not used in a for-loop should be a underscore: _
.
local Players = PlayerService:GetPlayers()
-- Later in the code...
--WRONG
for i,v in Players do
-- do something with the player
end
--RIGHT
for _, Player in Players do
-- do something with the player
end
--RIGHT
-- Players is a table of all player instances
for _,v in Players do -- v is a Player's Instance
-- do something with the player
end
Organization In Scripts
This section focuses on script formatting, organizing your variables, services functions, and commenting. Organization is important for maintaining code and keeping easy code readability. If all the scripts in your experience are made in the same organizational format, it makes it much easier to read, traverse and find what you are looking for.
Global Script Variables
Any variables which are used through out the script should be defined at the top of the script. It should be layed out in the following order:
-- Services
-- Any modules
-- Instances (UI, Parts, Models)
-- Remaining Variables (constants, any other variables)
----------------------------------------------
--Below is an example of the top of a client shop script:
local PlayerService = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
local MonetizationIds = require("MonetizationIds")
local UIVisibilityManager = require("UIVisibilityManager")
local ItemData = require(ItemStorage:WaitForChild("ItemData"))
local LocalPlayer = PlayerService.LocalPlayer
local ItemStorage = ReplicatedStorage:WaitForChild("Inventory")
local MainFrame = script.Parent:WaitForChild("MainFrame")
Any variables which are only used in a specific function, or section of code can be kept together with that code.
Section Organization
Having large scripts and functions separated by just line breaks can create a hard to read script. You should split up your script into independent sections which are easily identifiable and organized. (This specific type of formatting is uncommon however I personally find this style useful) The scripts I showcase are split up by three large lines of â-
â with the sectionâs name in the middle, like so:
-- ## This next section would be everything related to the currency UI, the titles of each section should be descriptive enough to understand what the code in that section is for.
-----------------------------------------------------------------------------------
---------------------------Top Currency UI-----------------------------------
------------------------------------------------------------------------------------
-- ## First any variables this section uses that are not defined yet
local CurrencyUI = script.Parent.Parent.TopCurrency
local CurrencyFolder = LocalPlayer:WaitForChild("PlayerData"):WaitForChild("Currency")
local TokenValue = CurrencyFolder:WaitForChild("Tokens")
local CashValue = CurrencyFolder:WaitForChild("Bucks")
-- ##Next are any functions
local function openCurrency() -- Opens the currency UI
CurrencyUI.CashUI.Visible = true
CurrencyUI.TokensUI.Visible = true
end
local function closeCurrency() -- Closes the currency UI
CurrencyUI.CashUI.Visible = false
CurrencyUI.TokensUI.Visible = false
end
-- Sets the Currency UI visibility using UIVisibilityManager
UIVisibilityManager.SetDefaultUI("Top","Currency",openCurrency, closeCurrency)
UIVisibilityManager.SetDefaultUIVisibility("Top", true)
--Update Tokens Label when it changes
TokenValue.Changed:Connect(function()
CurrencyUI.TokensUI.Label.Text = tostring(TokenValue.Value)
TopFrame.CurrencyHolder.TokensFrame.Label.Text = tostring(TokenValue.Value)
end)
--Update Cash label when it changes
CashValue.Changed:Connect(function()
CurrencyUI.CashUI:WaitForChild("Label").Text = tostring(CashValue.Value)
TopFrame.CurrencyHolder.BucksFrame.Label.Text = tostring(CashValue.Value)
end)
--## This is what a section could / should loop like. Any following section should repeat the process, big title and so on:
--------------------------------------------------------------------------------
--------------------------Data Management------------------------------
---------------------------------------------------------------------------------
local clientShopData
local clientShopWeights
GetRemoteEvent("RequestShopInformation").OnClientEvent:Connect(function(data, weights)
clientShopData = data
clientShopWeights = newWeights
end)
-- etc
Remember to not make your sections to broad or large. Section breaks are meant to make code more easy to read and understandable, having sections that are 100+ lines of code defeats this purpose. Scripts themselves should not be too long either, most scripts should be under ~1000 lines of code and anything over that might be better split up in a module script. Donât make your scripts too short either. Having a folder of many scripts which are ~50 lines of code each can become difficult to navigate for the reader.
UI Navigation
It can be difficult to create smooth UI navigation. One of the most challenging issues is overlapping UI frames, or ensuring when one frame opens, the other one closes. This is not too difficult to do on a small scale but if your game have many UI elements, its complexity can add up fast. It is recommended to use/create a UIVisibilityModule
. This module should manage the visibility of all the UI inside your experience. It should be this moduleâs job to ensure that two frames are never overlapping or displayed in the same position.
I developed a script which does exactly this. When using the module to display a frame, it has two main arguments: the location of the frame, and the whether if its a temporary or default frame. When you call the module, you say where the frame is located (âTopâ, âLeftâ, âCenterâ, âRightâ, etcâŚ) and whether its a temporary frame or a default frame. Temporary frames would be like a shop or inventory window, and a default frame would be like the playerâs health bar or side menu buttons.
So for example:
-- If i opened a shop frame by doing:
UIVisibilityManager.SetUIVisibility("Center", "Shop",OpenUI, CloseUI,true)
-- The module would call the OpenUI() function and mark "Shop" as being in the "Center" position.
-- In another script... lets say i want to open the settings UI:
UIVisibilityManager.SetUIVisibility("Center","Settings",OpenSettings, CloseSettings, true)
-- The module would see that "Shop" is already opened in the position "Center", so the module would call the CloseUI() function to close the shop, then call OpenSettings() function to open the settings frame
UIVisibilityManager usable functions:
TO OPEN A FRAME
UIVisibilityManager.SetUIVisibility(Position,UIName,ShowUIFunction, CloseUIFunction, UI Visibility)
TO CLOSE A FRAME
UIVisibilityManager.SetUIVisibility(Position,UIName,nil, CloseUIFunction, UI Visibility)
TO CLOSE ALL FRAMES
VisibilityManager.TemporaryCloseAll(UIName) -- Used any time where no UI should be visible
VisibilityManager.UndoTemporaryClose(UIName) -- Undo the VisibilityManager.TemporaryCloseAll()
TO SET A DEFAULT OPEN UI
UIVisibilityManager.SetDefaultUI("Bottom", "MainUI", OpenUI, CloseUI) -- The default UI is the frame that should be open most of the time
TO OPEN / CLOSE THE DEFAULT UI
UIVisibilityManager.SetDefaultUIVisibility("Bottom", true)-- true will open the default ui in that position, false will close the default ui
TEMPORARY OPEN A FRAME -- Closes the frame is that position, once the finished function is called, the frame that is supposed to be open there will open.
UIVisibilityManager.ShowTemporary(Position,UIName,ShowUIFunction,CloseFunction)
UIVisibilityManager.FinishedTemporary(Position,UIName, CloseUIFunction)
---------------------------
Position: string -- where the frame is located on screen ("Top", "Left", "Center", "Right", etc..)
UIName: string -- The name / class of the ui, does not need to be related to any instances / objects; just needs to be a unique from every other UI frame using `UIVisibilityManager`
ShowUIFunction: function -- The function (callback) that is triggered when the frame should open, EXAMPLE:
CloseUIFunction: function -- the function (callback) that is triggered when the frame should close,
UIVisibility: bool -- What the frame visibility should be, (true activates the open function, false activates the close function)
-- EXAMPLE:
local function openUI()
MainFrame.Visible = true
end
local function closeUI()
MainFrame.Visible = false
end
UIVisibilityManager.SetUIVisibility("Center", "Shop", openUI, closeUI, true)
-- (Position: string, UIName: string, openUIFunction: function, closeUIFunction: function, UIVisibility: bool)
Investing time to create a script similar to this will make navigating between UI elements as fluid as possible while still being easy to implement. A simpler version of the module could be used for more complex UI elements such as a shop with many tabs or windows to switch between. This module itself should be under ~200 lines of code.
Example Script:
--[[
Example script which could be used to manage a central single frame position with basic functionality.
Could also be used to switch between multiple frames in a single window.
]]
local VisibilityManager = {}
VisibilityManager.Frame = false
VisibilityManager.FrameCloseFunction = false
VisibilityManager.SetUIVisibility = function( UIName: string, OpenFunction, CloseFunction, Bool) -- Open/CloseFunction:function, Bool:bool, true for set ui to open, false for close UI
if Bool == false then -- Close the Frame
if VisibilityManager.Frame == UIName then
VisibilityManager.Frame = false
VisibilityManager.FrameCloseFunction = false
end
coroutine.wrap(CloseFunction)()
elseif Bool == true then -- Open the Frame
if typeof(VisibilityManager.Frame) == "string" then
coroutine.wrap(VisibilityManager.FrameCloseFunction)()
end
VisibilityManager.FrameCloseFunction = CloseFunction
VisibilityManager.Frame = UIName
coroutine.wrap(OpenFunction)()
end
end
Optimizations
Optimizing your game allows for lower compute power which mean less electricity use, lower bandwidth use and smoother gameplay on older/less capable devices. You canât just flick a switch and make your game run 10x faster. Code optimization is best done before you even started coding by using proper practices.
Events
In game development, events are everywhere. Events are really efficient and allow code to be ran only when its needed.
One example of this is waiting for something to happen. The simplest approach to wait for something is a while task.wait() do
loop, and break the loop when you no longer need to wait. This is a really inefficient method to do this task. This loop is un-necessary and uses pointless compute resources. A better solution to this would be using events. It could be:
RbxScriptConnection:Wait()
which would be much more efficient.
A specific example would be a option for the client to hide all other players. There would be two main ways to solve this: a infinite loop where every iteration would set all player models to invisible or use events such as CharacterAdded
and make characters invisible as they are created. The event method would be much more efficient and is the better option.
Assets
Try to avoid re-uploading assets and using both versions of that asset. For example, if you have a two icons in your UI which are the exact same but different assetIds, that creates unnecessary images which the player needs to load-in. Especially on a large scale, avoiding duplicate assets with different assetIds will help optimize the playerâs streaming usage. This could be avoided by having a list of all imported assets (usually images) and their Ids.
Remotes
One way in which you can optimize your workflow is to use a module for RemoteEvents and RemoteFunctions. There are many modules which you could use such as nevermore . With nevermore, remotes are defined as so:
local Nevermore = require(ReplicatedStorage:WaitForChild("Nevermore"))
local getRemoteFunction = Nevermore("GetRemoteFunction")
local getRemoteEvent = Nevermore("GetRemoteEvent")
local RequestInventory = getRemoteFunction("RequestInventory")
local RequestFavourite = getRemoteEvent("RequestFavourite")
If you use this method for all of your remotes, there should be no (or very few) remote instances in ReplicatedStorage which cleans up the explorer. Another great alternative for networking is âBridgeNet2â.
**Removed Section: Time Analysis**
time analysis is important but is not very applicable here. this section is basically saying donât write inefficient algorithms*
Time Analysis
Time Analysis is measuring how fast a function computes with a given data set. This topic isnât hugely important when it comes to game development as most data sets are relatively small in scale but it is still important to write efficient code to reduce compute power and optimize for lower end devices. Time Analysis focuses on the number of operations (n
) needed to compute as the data set grows ignoring compute power (ignoring slow vs fast computer). This is known as Time-complexity or big-O
notation. Some operations are: +
, -
, *
, /
, (add, subtract, multiple, divide) etc.
-
O(n!)
- Factorial time (slowest) -
O(2^n)
- Exponential time -
O(n^2)
- Quadratic time -
O(n log(n))
- Quasilinear time -
O(n)
- Linear time -
O(log(n))
- Logarithmic time -
O(1)
- Constant time (fastest)
Most function are either linear or constant time. Constant time meaning the function does the same number of operations regardless of input size. Linear mean the function does a certain amount of operations for each item in the data set, so the number of operations its does scales linearly with the size of a data set. The faster/more efficient a function is, the better your code.
For-Loops (Through Tables/Dictionaries)
There are many different ways to iterate through a table. I tested each methodology and seen which one was most efficient / fastest.
I created a table of size 1 000 000 with each element equal to 1
. I iterated through the table 1 000 times using each method and recorded the time it took using os.clock()
. (Data is in seconds)
- ipairs 2.520437800005311
for i,v in ipairs(data) do end
- pairs 2.5394094999937806
for i,v in pairs(data) do end
-
nothing 2.545355299997027
for i,v in data do end
- next 2.5720390000060434
for i,v in next, data do end
- Numeric-Pre 3.7817707999929553
local v for i = 1, #data do v = data[i] end
- Numeric-Post 3.782368200001656
for i = 1,#data do local v = data[i] end
The method you chose to iterate through a table is a small and negligible impact, however a fully built experience will be running these loops constantly and part of optimization and saving compute power is choosing the best method. All these methods work the same (except pairs
is meant with dictionaries and ipairs
is meant with arrays) and can be interchanged for each other. Looking at the data, you should be using pairs
, ipairs
and/or "nothing"
.
Duplicate Scripts
Try to avoid duplicate scripts in your experience. An example would be having custom seats around your map with each seat managed by a script. If you have 50 seats in your map thats 50 scripts for all the seats. That is really inefficient. If you ever need to change the script, your would have to replace all 50 scripts which takes your valuable time. A better alternative would be to store all 50 custom seats in a group and use a single script with a for-loop to iterate through all the seats and run the appropriate functions.
GetUserThumbnail Size
It is very common when developing to need to display an image of the player. To accomplish this, developers use the function PlayerService:GetUserThumbnailAsync(userId, thumbnailType, thumbnailSize)
. The third argument of this function thumbnailSize
is an enum type with many different options for different sizes. The more common size option is 420 x 420 (in pixels). However, for most applications this size is pointless. If youâre making a player leaderboard and all of the player icons are at most 150x150 (in pixels/offset frame size) There is no reason to get the largest icon size. Always getting the largest icon can cause the client to download unnecessarily large assets where a 150x150 would suitable. One good rule is to size your frame to its largest point take that size and move one step up. This should ensure your icons are always full quality but you are not wasting extra resources downloading oversized assets.
Automatic Robux Prices
When selling marketplace items in a client UI shop, the easiest way to list the item prices is by manual. However if you ever decide to change the price of the product, you will have to go back and update your UI, and if you do this for multiple items, it can become a nuisance. The most efficient way (from a long term developing time perspective) to do this is to get the price of the product when the UI starts up. This can done by doing
local price = MarketplaceService:GetProductInfo(assetId, infoType).PriceInRobux
This ensures the price is always updated (when this function is ran).
Connecting if
statements
In lua, and many other programming languages, the and
keyword is a conditional statement which only operates what is after the keyword if and only if what is before the keyword is true. This can be used to condense multiple nested if statements into one long if statement.
if object1 then
if object1.Value then
-- do something
end
end
-- is the same as
if object1 and object1.Value then
-- do something
end
In this example,
if object1 and object1.Value then
, it first checks if object1 is notnil
. The right side:object1.Value
will error ifobject1
is nil. Something like: Attempt to index a nil value. However because of how theand
keyword works, if the left side of the condition isfalse
ornil
, the whole condition will automatically be false so there is no point evaluating the right side, so the right side never runs so it never errors.
Comments
As simple as it may seem, COMMENTS ARE VERY IMPORTANT. More time is spent reading code then actually writing it. Comment your code so when a reader reads your code they can understand what you are doing and what is happening. You donât need cliche comments every line but just enough to get an understanding. Do your future self and everyone else a favor and comment your code. Uncommented code is difficult to read and its often easier to re-write the script than to try and decipher some code.
Debugging Methods
General Practices
- Use
print
statements through out your script, or every other line if you have to. See what is / isnât printing or if the printed values match expectations. - Use
typeof( variable )
to ensure variables is of the correct type - Print values before and after calculations, compare expected results with actual results.
Devforum
The devforum is usually my first go-to when debugging an issue. I search in #Scripting-Support
to see if anyone has had similar issues. It can be difficult to find your issue on the forum by searching alone. Continue to try searching using different keywords. If you still cant find anything close to your problem then make a post. Remember to always search for similar posts before making your own, the devforum is filled with clutter duplicate posts.
Developer Hub
The developer hub is a great resource if you know what your looking for. If you donât know what to search or what documentation to look at, then the devforum is a better place to look for help. The developer hub isnât a great resource at fixing bugs, however it is a good resource for correcting incorrect usage of method/function calls and the use of services.
Chat GPT (AI Chat Bots)
I have no clue why I am recommending Chat GPT. Recent advancements have made the service quite useful for programming. This should not be your first go-to resource for fixing bugs. Chat GPT is still relativity new and can often be inaccurate. Here is a couple steps you should take when using Chat GPT:
-
Write your response with detail and clarity. You are writing to a third party with absolutely no knowledge or context of what your doing. Explain that your are doing Roblox Development, what you are trying to accomplish, and possible solutions, or things you have already tried.
-
Treat each response as it is incorrect. The majority of the time, Chat GPT will get things right, but it is common for it to write nonsense about APIs or how certain elements work. Try its solutions but donât commit hours trying to get what it said to work.
-
Generate multiple responses. Chat GPT can change its answer or its solution on another generated response. If multiple responses are similar, then its solution is more likely correct, if the responses are different, then it is most likely wrong.
Toolbox
The toolbox can be a great resource if it has what you need. You could copy entire scripts from toolbox assets, just take whichever chunk of code you need and apply it to your system. Make an effort to try and understand what code you are using, copy-pasting code which you donât understand is not productive. If you are entirely stuck, find a toolbox asset which is similar to what you want but slowly modify it to what you want.
Remake
Another option is to remake the object, one step at a time. Start from a basic working state, add another element then test to ensure the object still works. Slowly continue this process until you either finish with your desired state or until you hit an addition which broke your object. If your object worked before a small addition but not after, then it should be relatively easy to narrow what your addition may be doing to break the object.
assert
, error
, warn
Three lesser-known keywords are assert
, error
, and warn
. These can be great debugging tools to use throughout your scripts. Its ideal to use them when programming with many function calls. These can be used to ensure that code is flowing correctly and the types and values of parameters and values are valid.
`assert`
assert( condition , "error message") -- Throws an error when the given condition is false
-- Example:
local function doThing(number1: number, number2: number)
assert( typeof(number1) == "number" and typeof(number2) == "number", "Sent parameters are not of the number type")
-- This assert function will stop the script and error "Sent parameters are not of the number type" if either `number1` or `number2` is not a number type
`warn`
warn("Something bad happened") -- Functions the same as a print, just shows as a different color and in bold in the output
`error`
error("Something went wrong") -- Functions similar to print, but output is red bold text and it stops the script at the line it was called.