Nirleka's Guide to Scripting¶
Do note that you don't ALWAYS need to follow these guides. If you're doing quick and dirty prototyping to prove your theories, then you don't really need to follow all of these. This guide is mostly for production ready code.
Naming Conventions¶
- Classes, server, client, and module scripts: UpperCamelCase.
- Variables, functions, fields: lowerCamelCase.
- Constants: UPPER_SNAKE_CASE.
Use Luau's Static Typechecker¶
Use --!strict at the top of a file to enable Luau's strictest type checking. This prevents you from giving a function the wrong parameter types, mispelling variable names, etc, through autocomplete.
Declaration of Variables¶
Roblox Services¶
Services used in a script should all be declared on the top of the script, preferably in alphabetical order.
Avoid directly accessing services using the dot ., for example game.ReplicatedStorage, instead use game:GetService(...), like game:GetService("ReplicatedStorage").
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
Types¶
If you declare a variable but you do not set a value to it immediately, or it's initially nil, specify the type of the variable.
And of course, if it could be nil, use ?
Avoid specifying the type if you DO set it immediately
local initialStartTime: number = 10 -- Avoid this, it's redundant
local initialStartTime = 10 -- This is acceptable
An exception to this is for tables and other types that uses generics or contains something.
local closestPlayers = {} -- Avoid this
local closestPlayers: { [Player]: true } = {} -- Do this instead
Another example for other types:
Classes¶
Avoid writing all systems and logic into a single monolith script. Seperate the systems into module scripts. A typical module script will have this as the canvas:
--!strict
--[=[
@class Class
A class. Mindblowing I know.
]=]
local Class = {}
Class.__index = Class
export type Class = typeof(setmetatable({} :: {
field: string
}, Class))
function Class.new(): Class
return setmetatable({
field = "A field"
}, Class)
end
function Class.getField(self: Class): string
return self.field
end
return Class
Functions¶
Declaration¶
Avoid using the colon : when declaring functions where it is part of a class.
Avoid:
Instead do:
This lets the typechecker know what self is. Elsewhere, you can still call the function normally with a colon:
Declaring a Function's Parameters and Returns¶
If your function takes something and returns something, you should state their types.
Multiple returns:
function ServerPlayer.getNameAndSurname(self: ServerPlayer): (string, string)
return self.name, self.surname
end
And if it doesn't return anything, explictly state it by using ()
Using Functions¶
Avoid using direct access to a class's fields. Use getter and setter methods instead. For example:
Avoid:
Instead do:
This lets you change on how a position is retrieved without having to modify every single script that wanted to retrieve the position of an entity.
function Entity.getPosition(self: Entity): Vector?
if not self:isAlive() then
return nil
end
return self:getCharacter().HumanoidRootPart.Position
end
And this lets you change how a walkspeed is set for each entities. If you want an entity that doesn't use a humanoid, you can change how it's implemented without other scripts needing to handle each entity types.
function Entity.setWalkSpeed(self: Entity, walkSpeed: number): ()
if not self:isAlive() or not self.humanoid then
return
end
self.humanoid.WalkSpeed = walkSpeed
end
Of course, this is for external uses. This becomes less important if you're using them inside the class functions itself.
function Entity.update(self: Entity, deltaTime: number): ()
if not self:isAlive() then
return
end
if self.isGoingInsane then
self.sanity -= deltaTime
end
if self.sanity <= INSANITY_THRESHOLD then
self:ascend()
end
end
Constants¶
Constants are variables where its value never change during runtime. Avoid using raw, magic numbers, strings, or other datas in your logic. This makes it easier for other people reading your code to know what those values are for. And also makes it easier for you to change them if there are multiple logics using the same constant.
You should declare constants at the upper part of the file.
local MIN_WAIT_TIME = 0.1
local MAX_WAIT_TIME = 1.5
local waitTime = Random.new():NextNumber(MIN_WAIT_TIME, MAX_WAIT_TIME)
task.wait(waitTime)
Enums¶
Luau doesn't have enums but creating custom enum tables is a good practice for fixed sets of data.
Reasons for using them¶
- Autocomplete & Safety: Eliminates "magic strings" or numbers, preventing bugs caused by typos (e.g.,
"Happpy"instead of"Happy"). - Readability:
FACE_TYPES.ANGRYis much more descriptive than the arbitrary number1. - Centralization: Change a value in one place, and it updates across your entire codebase.
Implementation¶
For basic use, a basic table works.
However, if by some demonic reasons that your enum table might get modified, use table.freeze to make them read-only. Any attempts to modify them throws an error.
Design Patterns¶
Interfaces¶
Interfaces are type definitions that describes what a class should look like without knowing its internal logic. This allows you to make a function that accepts any object, as long as that object has the required methods the specified interface have.
export type Damageable = {
takeDamage: (self: any, amount: number) -> (),
getHealth: (self: any) -> number,
}
function Server.hurtExplosion(self: Server, damageable: Damageable): ()
-- Some logic or something...
local calculatedDamage = 10
-- Then:
-- Here, we don't care on how the class actually *handles* damage.
damageable:takeDamage(calculatedDamage)
end
Then we can have other classes implement that interface and supply it into the function: