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