Aller au contenu

Lua Style Guide

Overview

This document aims to provide a general overview of what we at Manticore see as sensible defaults for writing code in Lua.

File Structure

  • Start with any require calls, such as:
local myImport = require("assetID")

...

myImport.Foo()

General

  • No whitespace at end of lines
  • No vertical alignment
  • Spell out words fully. Abbreviations generally make code easier to write, but harder to read.

Casing

Element Styling
Classes PascalCase
Functions PascalCase
Enums PascalCase
Properties camelCase
Variables camelCase
Constants LOUD_SNAKE_CASE

Casing Calls

Example Casing Dot or Colon
Enum.ENUM_ENTRY PascalCase -> LOUD_SNAKE_CASE Dot
Class.StaticFunction() PascalCase -> PascalCase Dot
instance.property camelCase -> camelCase Dot
instance:MemberFunction() camelCase -> PascalCase Colon

Examples

Generic Example:

--[[
    Files start with a descriptive multi-line comment
]]--

-- Imports go next
local DogClass = require('DogClass')

-- Functions are PascalCase
local function GiveDog(player)
    -- Local variables use camelCase, classes use PascalCase
    local doggo = DogClass.New()

    -- Properties are camelCase, constants are UPPER_CASE
    doggo.color = Colors.BROWN

    -- Member functions are called with a ':'
    -- While static functions, see above, are called with a '.'
    doggo:AttachToPlayer(player, PlayerSockets.RIGHT_ANKLE)
end

-- Event subscriptions are located at the end of the file
Game.playerJoinedEvent:Connect(GiveDog)

Real example:

--[[
    When a player collides with a coin,
    give them the coin as a resource and remove the coin from the world
]]

-- Handle picking up a coin
local function HandleOverlap(trigger, player)
    -- Check that the object colliding with the trigger is a player
    if player ~= nil and player:IsA("Player") then
        -- If so, increment the 'Manticoin' resource count for that player
        player:AddResource("Manticoin", 1)
        -- Destroy the object in the scene so nobody else can pick it up
        trigger.parent:Destroy()
    end
end

-- Whenever an object collides with the coin's trigger, run this function
trigger.beginOverlapEvent:Connect(HandleOverlap)
-- Spawn player 30 units higher than normal, and print out new position
local function HandlePlayerJoined(player)
    local x, y, z = player:GetPosition()

    player:SetPosition(Vector3.New(x, y + 30, z))
    UI.PrintToScreen("New Position: " .. x .. y .. z)
end

World.playerJoinedEvent:Connect(HandlePlayerJoined)
-- Handle picking up a coin
local function HandleOverlap(trigger, object)
    if object ~= nil and object:IsA("Player") then
        object:AddResource("Manticoin", 1)
        trigger.parent:Destroy()
    end
end

script.parent.beginOverlapEvent:Connect(HandleOverlap)
-- Cat helper script
local DEBUG_PRINT = false

local function IncreaseAge(currentAge)
    currentAge = currentAge + 1

    if DEBUG_PRINT then
        print("Current age updated to" .. currentAge)
    end

    return currentAge
end

local function Main()
    local cat = Cat.New()

    for i = 1, 30 do
        local furColors = cat:GetColors()
        -- Note: table.contains is native Lua so it doesn't follow Core's conventions
        if table.contains(furColors, "grey") then
            currentAge = IncreaseAge(cat.age)
        end

        if DEBUG_PRINT then
            local meowText = cat:Meow()
            print(meowText)
        end
    end
end

Dot vs Colon

The colon is only used for methods that pass self as the first parameter, everything else is a dot.

This means that x:Bar(1, 2) is the same as x.Bar(x, 1, 2)

For more details, here is how it breaks down:

  • Static (dot)
    • Functions
    • Constructor
    • Constants
  • Instance
    • Methods (colon)
    • Properties (dot)
    • Events

For example:

Color.New() -- static functions + constructors
Colors.RED -- constants
player.name -- properties
player:ApplyDamage() -- member functions
-- Casing + operations

uppercase -> Enum.ENUM_ENTRY
          -> Class.StaticFunction()
lowercase -> instance:MemberFunction()
          -> instance.property

-- You should be able to determine the type of operation by the casing alone

-- PascalCase|PascalCase
Class.StaticFunction()

-- camelCase|camelCase
instance.property

-- camelCase|PascalCase
instance:Method()

When making your own methods:

-- Good

-- Called as myClassInstance:Speak(extra)
local function MyClass:Speak(extra)
    return self.speech .. extra
end

-- Bad

-- Called as MyClass.Speak(myClassInstance, extra)
local function MyClass.Speak(self, extra)
    return self.speech .. extra
end

Both are perfectly valid, but following convention allows for the usage call to consistently use colons for clarity.

  • Where possible, use getters and setters
    • Unless otherwise noted, mutating return types will affect the game object (pass by reference)
    • Properties use getter/setter methods unless you can both get and set the value in which case you can directly access them.

Styling

  • Use one statement per line, stay away from massive one-liners.
  • Prefer to put function bodies on new lines.
-- Good
table.sort(stuff, function(a, b)
    return GetValue(a) > GetValue(b)
end)

-- Bad
table.sort(stuff, function(a, b) return GetValue(a) > GetValue(b) end)
  • Put a space before and after operators, except when clarifying precedence.
-- Good
print(5 + 5 * 6^2)

-- Bad
print(5+5* 6 ^2)
  • Put a space after each comma in tables and function calls.
-- Good
local familyNames = {"bill", "amy", "joel"}

-- Bad
local familyNames = {"bill","amy" ,"joel"}
  • When creating blocks, inline any opening syntax elements.
-- Good
local foo = {
    bar = 2,
}

if foo then
    -- do something
end

-- Bad
local foo =
{
    bar = 2,
}

if foo
then
    -- do something
end
  • Only put parenthesis around complicated conditionals to keep your sanity, otherwise they aren't necessary in Lua.
  • Use double quotes for string literals (for example local myMessage = "Here's a message")

Comments

Use block comments for documenting larger elements:

  • Use a block comment at the top of files to describe their purpose.
  • Use a block comment before functions or objects to describe their intent.
--[[
    Pauses time so our protagonist has ample opportunity to train.

    Should only be used when there is a legitimate need to save the world,
    or the effectiveness will degenerate.
]]
local function SaveTheWorld()
    ...
end
  • Use single line comments for inline notes.
  • Comments should generally focus on why code is written a certain way instead of what the code is doing.
  • Obviously there are exceptions to this rule, the more obfuscated your execution the more true this is.
-- Good

-- Without this condition, the player's state would mismatch
if PlayerIsAirborne() then
    EnableFlying()
end

-- Bad

-- Check if the player is in the air
if PlayerIsAirborne() then
    -- Set them to flying
    EnableFlying()
end
  • Each line of a block comment starts with -- and a single space.
  • Inline comments should be separated by at least two spaces from the statement. They should start with -- and a single space.
-- One space for block/single-line comments
local myNum = 2  -- Two spaces after the code, then one space for inline comments

Best Practices

In general, you should always try to use local functions and variables, the only exception should be when you overwrite global functions like Tick(). Make sure to always declare your variables and functions in the order they are using in. Lua parses the file top to bottom, if you try to use a function before it has been declared, you will error.

Miscellaneous

_G vs require

require() explicitly makes a script execute if it hasn't already, and only executes a given script once.

If you need multiple instances of the same script dynamically spawned, require() should not be used.

Using External Data

You can use require() and a script that returns a long string to encapsulate JSON data in its own script. Afterwards use require() again with a JSON library. To make a script that returns a JSON string when you require it, start with this:

return [===[
    -- JSON here, make sure it does not contain "]===]" though!
]===]

Dernière mise à jour: 3 février 2022