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:
function ServerPlayer.takeDamage(self: ServerPlayer, amount: number): ()
if not self:isAlive() then
return
end
self.humanoid.Health -= amount
end
function Npc.takeDamage(self: Npc, amount: number): ()
if not self:isAlive() then
return
end
self.health -= amount
if self.health <= 0 then
self:kill()
end
end
My Pedantics¶
Variables¶
I swear to God, DO NOT ABREVIATE TO THE POINT YOU WON'T UNDERSTAND SHIT.
Look at this monstorsity. You can not understand jackshit. When you write code, always assume that there will be a poor soul that is gonna read, understand, and refactor your code.
The good way to do is:
local leftArm: BasePart
local rightArm: BasePart
local leftLeg: BasePart
local rightLeg: BasePart
local head: BasePart
local torso: BasePart
local humanoidRootPart: BasePart
Oh now my eyes won't cry anymore. Now I can easily understand what the hell the variables means and do.
Doesn't really mean you should never abbreviate. There are some acceptable forms of abbreviations. Including but not limited to: * id. Identifier. * config. Configuration. * msg. Message. * init. Initialize. * i, k, v, j. Use these in loops when you do not give a single utter fuck on what values they are.
If an abbreviation can mean many things, stop, rethink your life choices, and write the full term. Do not sacrifice sanity for the sake of a few key strokes.
Type Handling¶
Here's an example:
local function add(x: number, y: number): number
assert(type(x) == "number", "Bad x!")
assert(type(y) == "number", "Bad y!")
return x + y
end
Please. The type-checker is already enough for screaming at you when you mess up the types. This is redundant as shit. Unless you're handling very unpredictable inputs, which begs the question: why?
You should only do these sorts of stuff for stuff that the type-checker cannot check. For example the specific state of an object.
local function foo<T>(optional: Optional<T>): ()
if optional:isEmpty() then
error("Provided Optional should not be empty!")
end
-- ...
end
Identations¶
Here's an example:
local normalVector = Vector3.FromNormalId(normalId)
local surfacePosition = (inst.Size * normalVector) / 2
local offset = surfacePosition + (normalVector * frontOffset)
Why. Why. WHY. Sure, it looks pretty, but it's not worth your time. You're gonna modify here and there whenever you want to change the variable. You'll gonna spend more time styling your code than actually writing practical code.
Comments¶
Look. Please do not write comments every three goddamn lines. A code should be self-documenting at best, and you should only write comments when a logic is a bit arbitrary, and explaining the why you chose this specific implementation.
Write descriptive function and variable names. A function should at most be doing ONE thing. If a large logic can be seperated into many other small chunks of logic, turn them into functions. This way, writing so many comments becomes unnecessary because you can look at the code and immediately understand what it does.