This is my view and others view of writing professional code. If you disagree something feel free to tell me! Please note that this is my opinion, and you are allowed to have your own,
Introduction
Hello! I’ve been writing code for around 4 - 5 years, and I’ve been on Roblox for around 6. During my years here, I’ve learnt a lot about programming and would like to share it with you! This guide will teach you to write better code and could lead to you getting more commissions!
Why write clean code?
When you write clean code, it will benefit you and others in many ways. The first part is if you come back to look at the script a month or even a week from when you made it. Unless you have super good memory (I don’t), chances are you will not remember what some of it does, or what one variable means.
Also, when you work with others, it will help then understand your code and be able to read it. This is really good when you’re doing commissions, as people like professional code.
How to write clean code?
Let’s get into the nitty-gritty of writing clean code. Each segment will have a good script example and a bad script example.
Structure
The first part of writing a clean script is structure . This means where you place your variables and functions is important. All of your core variables should go at the top of your script, then your functions, and then your connections, and finally the return
if it’s a module script.
Also, for each new line on your script, make sure to press tab
to indent it. You should never have to nest your code more than 3 times, otherwise it can be put into a function, or you can make one line if statements
.
Good Example
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local CurrentPlayers = {}
local function AddPlayer(player : Player)
table.insert(CurrentPlayers, player)
end
Players.PlayerAdded:Connect(AddPlayer)
Bad Example
local CurrentPlayers = {}
local function AddPlayer(player : Player)
table.insert(CurrentPlayers, player)
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
game:GetService("Players").PlayerAdded:Connect(AddPlayer)
local Players = game:GetService("Players")
As you can see, structure is very important.
Comments
Comments help you and others understand your code better. I like to combine structure and comments together to make the best script. After all, the better looking the script is the better it will run.
I like to comment out each section of my script, and place the respective items under it. Only comment lines that need explanation, such as if they are very complicated.
There are two types of comments, single line and multi-line.
Single Line: -- This is a single line comment!
Multiline:
--[[
I can do multiple
lines on here!
]]
You should also be descriptive with your comments.
local currentPlayers = {} -- this is a table
That is not a descriptive comment.
local currentPlayers = {} -- This will hold all current players in the server.
That is a descriptive comment.
You do not need to comment a line that moves a player’s root part to a position. We can figure that out. But if it rotates it by 90 degrees and moves it randomly based on 2 vectors, then that would be a good place to put a comment.
Also, above functions you can put comments to tell the user what comments are. This is only for documentation but can help.
Here is an example of a function, can you guess what it does from the comments?
-- Returns if a player is an admin or not.
-- @param player - The player object to check for.
-- @return bool - If the user is an admin or not.
Good Example
-- [ SERVICES ]
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- [ LOGIC ]
local CurrentPlayers = {} -- Stores all players that have joined this server.
-- [ FUNCTIONS ]
-- Adds a player to the current players list.
-- @param player - The player object to add.
local function AddPlayer(player : Player)
table.insert(CurrentPlayers, player)
end
-- [ CONNECTIONS ]
Players.PlayerAdded:Connect(AddPlayer)
Bad Example
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- this gets the replicated storage service
local Players = game:GetService("Players") -- this gets the players service
local CurrentPlayers = {} -- this is a table
local function AddPlayer(player : Player) -- this is a function
table.insert(CurrentPlayers, player) -- this adds something to a table
end
Players.PlayerAdded:Connect(AddPlayer) -- this is connection
Naming Conventions
You should name your functions and variables descriptively. Lets say you have a variable that is a boolean whether it’s daytime out. Instead of saying this, local dytime
, you should do local IsDaytime
.
All core variables, the ones at the top of your script, should be in PascalCase, which is each new word starts with a uppercase letter. local CurrentPlayers = {}
All core functions should also use PascalCase.
Any functions that are private in modules, ones that the user should not access should start with an underscore. _
Any variables inside of functions should be camelCase, which is the first word is lowercase, then the following words start with an uppercase. local newPlayersTable = {}
Type Checking
This will help your code look more professional. This basically means telling studio what the type of your variable is.
Let’s say I have a variable called daytime
. Is this a boolean? Is it a number? What is it???
See, if we don’t know the type then studio cannot find errors better. To type check a variable, all you have to do is put a colon after it, then the type. You can find a really detailed guide here: Type Checking For Beginners
Examples
local MyNumber : number = 5
local MyBoolean : boolean = true
local MyTable : {} = {}
local MyInstance : Instance = workspace.Baseplate
local MyString : string = "Hello!"
local function MyFunction(AnotherNumber : number, AnotherString : string)
end
You can put these two lines at the top of any script, and it will tell you to type check variables that aren’t type checked.
--!strict
--!native
Spacing
Instead of doing y==1
, do y == 1
.
Basically just don’t cramp together lines, and always use spaces when necessary.
Conclusion
Here is an example of one my script, that follows (hopefully) the guidelines I’ve listed here.
-- [ SERVICES ]
local Players = game:GetService("Players")
-- [ LOGIC ]
local Signal = require(script:FindFirstChild("Signal"))
-- [ DATA ]
local ProAdmin = {}
ProAdmin.__index = ProAdmin
-- [ MODULE FUNCTIONS ]
-- Called when a player chats.
function ProAdmin:_OnPlayerChat(message : string, player : Player)
if not player or not message then return end
message = string.lower(message)
-- Fire to users.
local isAdmin = self:IsUserAdmin(player)
self.OnPlayerChat:Fire(player, message, isAdmin)
-- Split message.
local splittedMessage = string.split(message, " ")
-- Loop through commands fire right one.
for _, command : {} in pairs(self.Commands) do
if self.Prefix.. string.lower(command.Name) == splittedMessage[1] then
if command.AdminOnly and isAdmin or not command.AdminOnly then
if command.Function then
-- Fire and run command.
task.spawn(command.Function, player, splittedMessage)
end
end
end
end
end
-- Called when a player is added to the game.
function ProAdmin:_OnPlayerAdded(player : Player)
player.Chatted:Connect(function(message : string)
self:_OnPlayerChat(message, player)
end)
end
-- [ FUNCTIONS ]
-- Creates a new ProAdmin system in your game.
function ProAdmin.New(prefix : string, admins : {string})
local self = {
Prefix = prefix or "!",
Admins = admins or {},
Commands = {
{
["Name"] = "Teleport",
["Function"] = function(player : Player, args)
print("hey there! ")
print(player, args)
end,
["AdminOnly"] = true,
}
},
_OnPlayerChat = ProAdmin:_OnPlayerChat(),
OnPlayerChat = Signal.new(),
}
setmetatable(self, ProAdmin)
-- Add current and future players.
for _, player : Player in pairs(Players:GetPlayers()) do
self:_OnPlayerAdded(player)
end
Players.PlayerAdded:Connect(function(player)
self:_OnPlayerAdded(player)
end)
return self
end
-- Runs a command
-- @param {} - The arguments to send.
-- @param player - The player to run it as.
function ProAdmin:RunCommand(name : string, args : {}, player : Player)
if not name or not args then warn("ProAdmin(): RunCommand nil parameter") return end
for _, command in pairs(self.Commands) do
if string.lower(command.Name) == string.lower(name) then
if command.Function then
task.spawn(command.Function, player, args)
end
end
end
end
-- Adds a player to the admin list.
-- @param number - The userID of the player to add.
function ProAdmin:AddAdmin(userID : number)
if not userID then warn("ProAdmin(): AddAdmin nil parameter") return end
for _, admin in pairs(self.Admins) do
if admin == userID then
return
end
end
table.insert(self.Admins, userID)
end
-- Adds a player to the admin list.
-- @param number - The userID of the player to add.
-- @return bool - User was sucessfully removed.
function ProAdmin:RemoveAdmin(userID : number)
if not userID then warn("ProAdmin(): RemoveAdmin nil parameter") return false end
for _, admin in pairs(self.Admins) do
if admin == userID then
table.remove(self.Admins, _)
return true
end
end
return false
end
-- Returns if a player is an admin or not.
-- @param player - The player object to check for.
-- @return bool - If the user is an admin or not.
function ProAdmin:IsUserAdmin(player : Player)
if not player then warn("ProAdmin(): IsUserAdmin nil parameter") return end
for _, admin in pairs(self.Admins) do
if admin == player.UserId then
return true
end
end
return false
end
-- Returns if a player is an admin or not.
-- @param name - The player name to check for.
-- @return player - The user closet to the name.
function ProAdmin:PlayerFromName(name : string)
if not name then warn("ProAdmin(): PlayerFromName nil parameter") return end
name = string.lower(name)
-- Check through all players
for _, player : Player in pairs(Players:GetPlayers()) do
local displayName = string.lower(player.DisplayName)
local username = string.lower(player.Name)
if string.find(displayName, name, 1, true) or string.find(username, name, 1, true) then
return player
end
end
return nil
end
-- Creates a new command.
-- @param name - The name of the command to be typed in.
-- @param commandFunction - The function called when the command is ran.
-- @param adminOnly - If only admins can run this.
-- @param override - ONLY use if you are ok with two commands with the same name being generated, recommended OFF.
-- @return bool - If the command was created successfully.
function ProAdmin:AddCommand(name : string, commandFunction, adminOnly : boolean, override : boolean)
-- Error checking
if not name or not commandFunction or adminOnly == nil then
error("ProAdmin(): On AddCommand, no parameters can be nil!")
end
if override == nil then
override = false
end
-- Check if command exists
if not override then
for _, command in pairs(self.Commands) do
if string.lower(command.Name) == string.lower(name) then
warn("ProAdmin(): "..name.. " is already being used as a command name! Set the override boolean to true in the parameters if you are ok with this.")
return false
end
end
end
-- Insert into commands
table.insert(self.Commands, {
["Name"] = string.lower(name),
["Function"] = commandFunction,
["AdminOnly"] = adminOnly,
})
return true
end
-- [ RETURNING ]
return ProAdmin
Thanks for reading my guidelines to writing professional code!