Best Practices Handbook

9/14/2023

Hey, my name is Jack. I’ve collaborated with several prominent Roblox groups and have accumulated a plethora of knowledge to share with you all. In this handbook, I’ll be covering the best practices for general programming and specific Lua related challenges.

I wish I had this resource when I was learning.

Best Practices Handbook



camelCase

Click To See More

There are numerous different naming conventions. However, camalCase is widely recognized as the generally accepted convention across the programming world.

Here’s a few code snippets of variables and functions when used with camelCase.

firstVariable = 100
sumOfAllNumbers = 200
function findTheAverage()

end



Use Effective Naming

Click To See More

Do you know what this variable stands for?

local rds

Well I certainty wouldn’t know what it meant.
Let’s try this again:

local ragDollService

This is much better. Using ineffective variables names will not only hurt your co-workers but will eventually confuse you.



Guard Clauses

Click To See More

Guard clauses are conditionals that exit a function/loop with a return/break/continue statement.

Here’s an example of nested conditionals that can be replaced with guard clauses.

function checkUsernameStatus(userNameValid : boolean, premiumSubscription : boolean)
	if premiumSubscription then
		if userNameValid then
			return "Username is valid"
		end
	end
end

Replacement with guard clauses:

function checkUsernameStatus(userNameValid : boolean, premiumSubscription : boolean)
	if not userNameValid or not premiumSubscription then return end
	
	return "Username is valid"
end

Notice how the code no longer has nested if statements. In large systems, guard clauses will come in handy when dealing with a multitude of conditionals.



Use of Modules

Click To See More

Module Scripts are arguably the most useful script object in Roblox.

Module scripts allow you to create clean and reusable code. Repetitive code is a thing of the past once you have harnessed the power of module scripts. Some popular frameworks employ the use of module scripts instead of scripts and localscripts (e.g. Knit Framework). Why rewrite code when you can just reference a single function or class?

This is a basic example of module scripts. However when combined with OOP, module scripts go to the next level. I’ll leave a tutorial to OOP below as well.

local myModule = {}

function myModule:returnCoolString(string1)
	local coolNumber = tostring(self:returnCoolNumber())
	local theString = coolNumber .. " " .. string1
	
	return theString
end

function myModule:returnCoolNumber()
	local coolNumber = math.random(1,100)
	
	return coolNumber
end

return myModule
myModule:returnCoolString("Hi")
-- OUPUT: "40 Hi"

https://www.youtube.com/watch?v=2TC-bx0YfGk



Functional Programming

Click To See More

Functional Programming is the practice of using functions without changing any data. This is a useful practice in large scalable systems.

For example:

function returnSum(num1, num2, num3)
	local sumOfAllNumbers = num1 + num2 + num3
	return sumOfAllNumbers
end

Notice how that code did not manipulate any data. It simply returns an output.



Type Checking

Click To See More

Type Checking is extremely useful, even more so when working with bigger projects.
In fact, if you have ever used a roblox service, you have seen typechecking.

For example, let’s take a look at tween service:


In this picture I am creating a new tween, and you can see it showing a typechecking UI when I’m filling out the parameters.

The orange underlines resemble variables while the red underlines resemble the type.
This is useful information because you now know what the method’s parameters are looking for.
You can typecheck variables, parameters, and function returns.


Now let’s make our own method using type checking:

Now, string1 and string2 expect a type of string. And the function expects to return a string.



DRY

Click To See More
  • Don’t
  • Repeat
  • Yourself

Duplicating your code in multiple places will ultimately lead to confusing code and impossible changes.

Here’s a snippet of code to discuss.

function findClosestPlayer(plr)
	local distance = nil
	local nearPlayer = nil
	for i, v in pairs(players:GetPlayers()) do
		local distanceBetween = v:DistanceFromCharacter(plr.Character.HumanoidRootPart.Position)
		if not v.Character or (distance and distanceBetween >= distance) or v.Name == plr.Name then
			continue
		end
		distance = distanceBetween
		nearPlayer = v
	end
end

Now imagine if this was not a function and you duplicated this code 10 times throughout your script.
First of all, this will cause readability to decrease.

Let’s say I need to change part of this code. Looks like we have to change all 10 occurrences of this code.

However, If this code was in a function, you could change the code in the function and you would be all good.



Use Comments

Click To See More

Documenting your code is important when working on projects with a team or just your self.

Imagine you write a complex system. Three months later, you need to make some changes to your code. However, you don’t know how everything works in your system. If your code was documented, it would have saved a couple hours of confusion.

Also, did you know that the word “TODO” is a keyword?

-- TODO: Add mobile combability

In studio, the word “TODO” will be bolded.



Goldilocks Effect

Click To See More

A lot of experience programmers will strive for the most optimized solution.
However, in some cases this will lead to over-complexity.

The most optimal solution is to have performant and readable code.
Find a middle ground when writing code.



Clean up your connections

Click To See More

A lot of newer-intermediate programmers will often make the mistake of making memory leaks by having a misinterpretation of connections and memory.

Let’s look at the following code:

part.Touched:Connect(function()
	-- // Do stuff //
end)

Let’s say you only need this code to run once. After the first time this code runs, it will be useless to you. And, this connection will still be in memory. It’s only until the part get’s deleted will the connection get garbage collected.

Let’s go ahead and fix this problem:

local connection
connection = part.Touched:Connect(function()
	connection:Disconnect()
	-- // Do stuff //
end)

Now, this touched event is only in memory until the first time it’s touched.


For an alternative solution, you can use Event:Once(). This will make sure the event only fires once and is garbage collected:

part.Touched:Once(function()
	-- // Do stuff //
end)


Use proper indentation

Click To See More

This one may be obvious, however it’s worth noting.
Some languages such as Python require proper indentation, however Lua is an exception.
Just because Lua doesn’t require it, doesn’t mean you should stop using indentation.

Did you know that there is a built in tool to format your whole script?
Right click in your script and select Format-- > Format Document



Use whitespace

Click To See More

Whitespace is simply just an empty line in your script
It is one of the key factors in making your code readable.

You can use whitespace effectively in the following places:

  • Before a function/loop
  • After a function/loop
  • After top-script variables
  • After code that has a specific duty


YAGNI

Click To See More

You aint gonna need it…

Don’t write code that you think you will need in the future.
This is coding for imaginary future use cases that you think you will need.
90% of the time, you won’t need that code. And it will sit there.



Scalability

Click To See More

Program with a mindset of scalability.
Don’t hardcode certain aspects of your code. This will lead to long hours of work if you plan your project to be bigger.



Use a framework for large scale projects

Click To See More

Large scale projects can get unorganized and unoptimized quickly.
If possible, using a framework is extremely helpful.

For example, I use the Knit Framework.
This framework prioritizes modular and reusable code. No more Scripts or Local Scripts!
You don’t even have to worry about remote events, as Knit manages all your events for you.



task.wait instead of wait

Click To See More

Instead of doing this:

wait(5)

Do this:

task.wait(5)

wait() is deprecated. It has been moved to the task library.



Instance.new best practices for performance

Click To See More

Instance.new is already performance heavy, so utilize this best practice.
Do not use the second argument of instance.new to set the parent of an object.

The following code is the worst way you can use Instance.new. It uses the second argument of Instance.new. In this case, it takes Roblox ten performance expensive steps to execute the code. This code queues useless replication changes which is heavy on CPU and bandwidth usage.

-- What not to do
local newPart = Instance.new("Part", workspace)
newPart.Position = Vector3.new(1,1,1)
newPart.Size = Vector3.new(1,1,1)

Instead, you should always set the parent property last:

-- What to do
local newPart = Instance.new("Part")
newPart.Position = Vector3.new(1,1,1)
newPart.Size = Vector3.new(1,1,1)
newPart.Parent = workspace

This code only takes Roblox five steps. This is the most optimal solution. This code’s property updates are extremely fast. When the object is inserted into the game, it’s in it’s final state.

If you don’t set the parent argument last, you will not get the desired performance. In fact, most Roblox core scripts and tools use this bad practice…

Set up Instance.new in the following order:
A. Instance.new
B. Assign properties
C. Assign Parent
D. Connect signals

For more information:
PSA: Don't use Instance.new() with parent argument



Cloning instead of instancing

Click To See More

If you are trying to create a new object. Always resort to cloning instead of Instance.new if possible.

Cloning an object is significantly more performant than using Instance.new to create a new object.



GetService instead of referring to services

Click To See More

Instead of doing this:

local tweenService  = game.TweenService

Do this:

local tweenService = game:GetService("TweenService")

GetService yields until it finds a service, if the service is not available, it creates one.
Also, if your services are named different than the original’s, GetService will find the service despite the different names.



62 Likes

I just wanted to elaborate more on the purpose of using :GetService.

Why use :GetService?
It returns a service with the given class name requested. When called with the name of a service, such as the ‘Players’ service, it will return the instance of that service. If the service doesn’t exist yet it will be created and the new service is returned. If you used game.Players instead of game:GetService("Players"), there is a slight chance that the service may not have loaded in yet and result in an error or if you changed the name of the service it would also return an error. In conclusion, it is simply better practice to use :GetService.

12 Likes

Just using getservice isn’t even the best practice. You can also do:

local Services = setmetatable({},{
    __index = function(self, ind)
        if pcall(function() game:GetService(ind) end) then
            return game:GetService(ind)
        else
            return nil
        end
    end
})

Then simply call Services.Workspace

Best used in a module. It’s the same exact thing but you don’t need to call :GetService every single service! (I’m too lazy to do all that)

Good Handbook! I will reccommend it to friends for learning better.

5 Likes

Game:GetService is the better practice of referring to different services.

GetService also has benefits than normal referencing does.
It will yield until a specific service has been found, if not it will create one. However, there’s not much use for that yet.
Along with this, it is possible that developers will name services different names. GetService will get the normal service despite the possibility of services being named different than the original.

5 Likes

So we have to write a 9 line ModuleScript only to call Services.Workspace when I can write one line: game:GetService(“Workspace”).

I do not see the point.

1 Like

I was making a joke since some people actually use this ridiculous metatable method. But I might just use it myself anyways because of how lazy I am.

6 Likes

I know I have already written that…

1 Like

I was laughing when I saw the post. However, I have worked with certain games that have done it.

2 Likes

Oh lol how did you find this module?

1 Like

A meme.
image
It’s not a bad method to use, but it’s surely laughable. (that being said im using it haha)

14 Likes

Can you explain this a little bit more for beginners? The announcement may be hard to understand for some. (edit: all you really need to do is give a mini explanation on why, or people are gonna keep doing it anyway)

1 Like

Yea good point, I’ll update that section in a bit.

1 Like

Wouldnt it be spelled “camelCase”?

Super neat handbook, tysm

1 Like

Good catch, I’ll fix that. I dont want to be using incorrect camal casing in my camal case section :blush:

1 Like

You should also teach about :Once() instead of just Connect and Disconnections.

2 Likes

Oh yea, I remember thinking about that last night. Although I must have fell asleep before writing it. I’ll add it now.

2 Likes

A few notes I’d like to say, this isn’t meant to be negative but just to add information so the community has as much information as we can pool as a collective :slight_smile:


camelCase has been phased out in the in-engine API in favour for PascalCase, Destroy not destroy and so forth, code which has public methods should ideally be using PascalCase for those methods for consistency with the engine itself


It would also be nice to add information about assert as a way to add guard clauses, baring in mind that if the first argument is false or nil, it will throw the second argument as an error

assert(game.Workspace:FindFirstChild("Baseplate"), "Baseplate does not exist.")

I 100% agree with this, perhaps an inclusion of the basics of how the self keyword works, especially since you use it in the returnCoolString function and might confuse a few people haha


Hopefully these few extras might help even just a handful of people

6 Likes

This is definitely useful information, I’ll probably edit this post tomorrow to include some of these ideas.

1 Like

There’s hardly an agreement on camel case being the standard, and either way it’s usually used in conjunction with pascal case and sometimes snake case (especially upper case for constants) depending on the specific thing you’re naming. Like is it protected, private, public? Is it a function? Is it a class? Is it a constant? Is it a file? Etc etc. The standard widely varies by programming language, and then of course projects and libraries within that language too.

The segment on functional programming is a bit far off too. Functional programming is quite a bit more complex and pretty hard to explain, it also varies a lot depending on who you ask.

If were talking about pure functional programming, which is often what people refer to these days when they use the term, it’s mainly about minimizing side effects and making functions deterministic. E.g if you call a function with the same argument, it will return the same thing every time. Usually when we program there’s lots of times this doesn’t happen, for example if we use the value of the clock inside of the function to decide something, or a math.random().

As for other “rules”, there’s a lot of people who are big on what you kinda mentioned, which is not updating data. So instead of changing data, they favor creating a new data structure to contain that data when performing some sort of operation with it. Recursions are also seen as the go to over loops. Which obviously stems from the whole mathematical origin of functions. And then of course functions can be passed to other functions, returned, or assigned to variables, etc.

Now people often think that if you use some programming paradigm, that has to be the only programming paradigm that you use. That obviously makes zero sense, there’s strengths and weaknesses of OOP and functional programming, and nothing that prevents you from using them or other paradigms in the same project or even together in the same bits of code. For example recursion can be pretty sweet sometimes, but it can also be memory intensive as shit and fill up the call stack to the point of crashing ur program.

There’s also no one set of rules for any paradigm, most of them have at least one or several “sub paradigms”.

3 Likes

I’m glad I didn’t get lazy reading this because I just discovered the use of Knit Framework. I can now develop without messing with Remotes and stuff.

2 Likes