Skip to content

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.

local destroyingConnection: RBXScriptConnection

And of course, if it could be nil, use ?

local destroyingConnection: RBXScriptConnection?
local currentTarget: Player? = nil

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:

local currentWalkTarget: WalkTarget<Player>? = nil

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:

function Class:getField(): string
    return self.field
end

Instead do:

function Class.getField(self: Class): string
    return self.field
end

This lets the typechecker know what self is. Elsewhere, you can still call the function normally with a colon:

local fieldString = classInstance:getField()

Declaring a Function's Parameters and Returns

If your function takes something and returns something, you should state their types.

local function add(x: number, y: number): number
    return x + y
end

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

local function doSomething(): ()
    -- Does something.
end

Using Functions

Avoid using direct access to a class's fields. Use getter and setter methods instead. For example:

Avoid:

local entityPos = entity.position
entity.walkSpeed = 16

Instead do:

local entityPos = entity:getPosition()
entity:setWalkSpeed(16)

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.ANGRY is much more descriptive than the arbitrary number 1.
  • Centralization: Change a value in one place, and it updates across your entire codebase.

Implementation

For basic use, a basic table works.

local FACE_TYPES = {
    HAPPY = 0,
    ANGRY = 1,
    SAD = 2
}

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.

local FACE_TYPES = table.freeze({
    HAPPY = 0,
    ANGRY = 1,
    SAD = 2
})
local faceControl = agent:getFaceControl()
faceControl:setFace(FACE_TYPES.ANGRY)

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.

local larm
local rarm
local lleg
local rleg
local head
local torso
local hrp

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.