How to Write Professional Code

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, :grin:


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!

32 Likes

and another tip for writing professional code is to use tools like rojo to organize all your different pieces of code

2 Likes

I HATE when scripting tutorials abbreviate services like ReplicatedStorage to ‘RS’

13 Likes

Comments should ONLY be used for documentation generation and at most extremely niche notes and reminders. If you need comments to explain or describe your code, you are writing bad code…

Another thing you should do is use asserts as much as possible. Asserts are used to check invariants in your code (conditions that have to always hold true otherwise there is a bug somewhere). For example if you have a function that takes health as input, you can assert that it is within 0 to 100 (or any other correct range). This helps you catch bugs much quicker.

3 Likes

I think the section markers are redundant. It should be clear what each section is for, from the code within it. I also think you don’t need the comment for CurrentPlayers. Maybe it could be called PlayersInServer if it’s ambiguous, as well as AddPlayer being renamed to AddPlayerToServer or HandlePlayerAdded.

4 Likes

I’ll give my 2 cents here

Same!

I agree with this. For me, unclean code demotivates me to work on it just by looking at it… Working with clean code is so much easier.

“for each new line on your script, make sure to press tab to indent it”? Confusing wording, I’m not sure what you mean. And by one line if statements, I think you are referring to one-line guard statements, which is an important clarification.

Um, no? What did you mean here?

I guess I agree with placing comments when something is complicated, but @sangolemango pointed this out as “bad code”, and I do feel like this has more of a “band-aid fix” vibe than actually clean code. I agree with comments explaining what a function does, but don’t you think that, if some piece of code is complicated and you need a comment for it, you can just turn it into a function and explain that with a comment?

Yes.

Unclear wording again. What is “you can put comments to tell the user what comments are”?

Agree with these parts.

Oh boy… A lot I’ve got to say about this one. First…

Doesn’t this fall under “not a descriptive comment”? We can already tell that the function is called when a player chats because it’s named _OnPlayerChat. It’s not adding any new information. A better fit would be something like -- Sends the message to every other user and fires any command present within it. And by the way, doesn’t the inside of this function ALSO contain undescriptive comments?

I thinks this falls under what you said about “You do not need to comment a line that moves a player’s root part to a position. We can figure that out.”, honestly. It should be self-explanatory.

This is literally the prime example of what you told us to not do, and you put it into the “example script”. Shouldn’t an example script at the end of your tutorial try to follow the standards you laid out in it as closely as possible? This feels like a blunder.

Unnecessary comments also are in play here. But doesn’t your tutorial also say

And this piece of code has a whole 5 nests. Granted in this case you can easily just use guard statements to get back to 3. But it’s still weird that what you laid out in your tutorial is followed very, very loosely in the script that’s supposed to be “here’s how it should be done”.

Point already made about the comments

Skipping some bits where I think there’s undescriptive comments and extra nesting…

A comment for signifying that you’re gonna return the module on the next line? It’s a style choice, can’t really complain I guess…

Overall, good job on creating this tutorial. Even with the harsh criticism, I wouldn’t call it a bad tutorial. To be blunt it’s more just undercooked. There’s the debatable use of comments, some blatant misfollowing of your own guidelines. But what I also think is missing is sections about, say using deprecated/outdated features, or that there’s no one perfect styling choice. In any case, I think this is a step in the right direction to help newbies write better code.


Writing and organizing seem to me like different things.

I agree. Though I do think that section markers are useful in large scripts, to help you navigate it easier, but they’re definitely redundant in a script this small. Your point about unnecessary comments is also something I agree with.

3 Likes

snake_case works perfectly, bro. Is there a problem with it?

1 Like

is it just me or does this not work

or is it supposed to look like this?

2 Likes

Please, do not use --!native everywhere if you’re not doing a ton of table accesses, math or using buffers.

3 Likes

There is no official docstring format for Lua AFAIK. So whatever comments you add above a function it will appear in the autofill. It’s not like JavaDoc which has strict syntax and format

2 Likes

Then you’d need to press 2 additional keys in-between the words of your variable names. It’s just unnecessary noise when you can separate words by using capitalization.

3 Likes

-- Check through all the players has no reason to be there.

-- 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

And there’s also no need to add this

-- 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.

Just rename the function to :GetPlayerByName

Believe me when I say you’re not going to be seeing comments like those in “professional code.” If anything, you’d often see functions with no comments; if you do, they exist to explain the reasoning of the said function. Internal wikis are preferred for more detailed explanations. Now, I’m not saying there’ll be no comments; I’m saying that comments are used sparingly.

My honest opinion of “clean code” is that code will never be clean. Clean code only exists for those that wrote it. But still do strive to make understandable code.

2 Likes

Thanks for your criticism. For my example at the end, since it was getting late I just took one of my past projects and slapped it in there.

As for the unnecessary comments in it, that was because it needed to be documented, as that was like a public script. Thanks for your opinions.

3 Likes

The example had to be documented, because the script was a public script meant to be read by others. That is why it had so many comments, making it easier to use.

For a script in your game, of course I’m not going to add those documentation comments above a function, neither for getting all the players.

But if other people will see it, then I will.

2 Likes

I like it overall… If you don’t mind though I’ll comment on a few things:

1. Write your code such that you don’t need comments.

Name things crystal clear and keep your logic (as) simple (as possible). With the sole exception of comments for function/API documentation, never write a comment explaining what a piece of code does — if you need that comment, your code isn’t clear enough. Comments are a brilliant tool for explaining why you’re doing something, though. Let your code explain the what and your comments explain the why.

(Yes, document your functions with comments!!! (I sometimes don’t lol))
(Also section header comments are fine, but subjective as to whether to use them)

2. Naming conventions

Over the more-than-half-a-century that people have been developing software, here is the total amount of universal naming conventions and standards that all software engineers have been able to agree on: 0.

Naming things is subjective. There are only three rules:

  1. Stay consistent.
  2. Stay consistent.
  3. Stay consistent.

For new projects, I use camelCase for local variables and basically anything that doesn’t leave the script. But for table members, exported fn names, etc. I use PascalCase to be consistent with the way the Roblox Engine names things. If i require a service or a module, the variable name for it is exactly the name of said service or module. Then, if I’m onboarded to a project that doesn’t do the same, I’m prepared to throw all of these conventions out the window to stay consistent with the project. Consistency is key — if you can’t do anything else, at least be consistent.

3. Type checking is a tool for you to use, not a god for you to serve

Paraphrasing the wise grugbrain.dev, 90% of the benefit of type checking is “I can type a ‘.’ and see in a little menu all the things I can do with this object.” Type correctness is cool and all, but Luau isn’t statically typed, and is built on a language that was never meant to be statically typed. As such, if you blindly decree that “All types shall be correct, thus sayeth the Lord,” you’re going to build all kinds of tables with metatables and functions and type unions as is possible in lua, all the while fighting the awkward type system in a futile effort to please some type checking god of your imagination — all the while never actually changing any of the functionality of your code with the type annotations you add.

I’m not saying that type checking is bad, I’m just saying that I prefer to use it for what it’s useful for, and nothing else. You will never catch me typing --!strict at the top of my editor (just as before, I am prepared to throw this convention out the window if it’s required to be consistent with a project).

An important note about --!native
While a script marked as native won’t permit you to pass in arguments of improper types, that’s not what native code gen is for. Native code generation takes your script, which would usually be interpreted, and causes it to be compiled to native machine code so it’ll run without the slowdowns of the interpreter. This is very cool, but has some drawbacks (which you can read about here) meaning you don’t want to enable this on every script in your game. So I do not recommend using --!native to handle type issues. Let the type checker handle type issues.

4 Likes

I love this feedback, this could be a whole page in of itself…

2 Likes

I personally recommend following the official Luau style guide from Roblox:

https://roblox.github.io/lua-style-guide/#naming


Naming

  • Spell out words fully! Abbreviations generally make code easier to write, but harder to read.
  • Use PascalCase names for class and enum-like objects.
  • Use PascalCase for all Roblox APIs. camelCase APIs are mostly deprecated, but still work for now.
  • Use camelCase names for local variables, member values, and functions.
  • For acronyms within names, don’t capitalize the whole thing. For example, aJsonVariable or MakeHttpCall.
  • The exception to this is when the abbreviation represents a set. For example, in anRGBValue or GetXYZ. In these cases, RGB should be treated as an abbreviation of RedGreenBlue and not as an acronym.
  • Use LOUD_SNAKE_CASE names for local constants.
  • Prefix private members with an underscore, like _camelCase.
    • Lua does not have visibility rules, but using a character like an underscore helps make private access stand out.

Most code on the platform at least loosely follows the official style guide.

4 Likes

WHAT THE (how did this get through editing lol)

quite the opposite for capitalization, normally it would be isDayTime

anyway, you should refer more to the roblox lua style guide

3 Likes

RS is a very commonly used abbreviation for ReplicatedStorage. Why do u hate it?

Any reduction reduces the self-documentation of the code. It is better to use full variable names, especially if they are used repeatedly in the code. This improves readability. Using RS as an example, such a name does not accurately indicate that it is a ReplicatedStorage. What about that handwritten service?.. Well, let’s call it “RemoteService”. What should I name his variable? Well, probably RS2​:sob::skull_and_crossbones:. Logically, if a variable can be called “RS”, then RS2 is also a good name.(no). I think you get my point.

1 Like