![](https://hackmd.io/_uploads/rJyO6Gbih.png) --- ## Author's Notes This guide is not claiming to be the one answer to how to code. It's highly opinionated and you can disagree with the guide's practices. So take from it what you will. Use this as a guideline or completely as is, or don't, it's completely up to you! --- ## Resources The resources listed below are what this guide adheres to unless specified otherwise. It's best you read through them, you'll most likely get something out of it. [Best Practices by: evaera](https://gist.github.com/evaera/fee751d4e228dd262fe1174ba142a719) [Performance Tips by: Roberto Ierusalimschy](https://www.lua.org/gems/sample.pdf) [Lua Style Guide by: Roblox](https://roblox.github.io/lua-style-guide/) [**Dark Mode Version**](https://hackmd.io/@aquaisaquafina/aquas_luau_style_guide_dark_mode) --- ## Aims You should always aim for your code to be more readable than writeable. This guide is meant to do just that while staying consistent. --- ## Comments Comments are something that should be used sparringly. This doesn't mean you should hesitate when it comes to using them. However, you don't want to explain what every little thing does to the point that it's unnecessary. In addition, instead of explaining what each section does say why it's there. **Note: When describing why a function is there, put the comment inside the function as the first line rather than on the outside!** This ensures that when you hide the codeblock in your IDE it also hides the comment. #### EXAMPLE ##### Good ```lua= do local realAccessKey1: string = HttpService:GenerateGUID() local realAccessKey2: string = HttpService:GenerateGUID() local function DoNuke(accessKey1: string, accessKey2: string) -- Needed to stop the humans from overpopulating. if realAccessKey1 ~= accessKey1 then return end if realAccessKey2 ~= accessKey2 then return end -- Needed or else nuke can be activated without the keys. -- Some big bad person could get their hands on it :(. KillAllHumans() end end ``` ##### Bad ```lua= do local realAccessKey1: string = HttpService:GenerateGUID() -- Generates Key local realAccessKey2: string = HttpService:GenerateGUID() -- Generates Key local function DoNuke(accessKey1: string, accessKey2: string) -- Does nuke. if realAccessKey1 ~= accessKey1 then return end if realAccessKey2 ~= accessKey2 then return end -- Checks if the real access keys are equal to the ones inputted. KillAllHumans() end end ``` --- ## Variables For variables you may want to use abbreviations as they are easier to type, however the trade-off is that it makes it just as much harder to read. ### Casing Variables by default should be in PascalCase unless specified below. #### EXCEPTIONS * Setting constants should be fully capitalized. * Variables inside functions should be in snakeCase unless its a service or a client controller. (Services should always be defined at the top of a script, this is an edgecase that involves the use of frameworks, knit being an example.) * Variables that are folder instances should be prefixed with "_". * Private tables should be prefixed with "T_" * Dummy variables should be substituted with "_" --- ## Blocks Each block of code should be spaced out by a line based of relevance. This keeps the code tidy and allows for others to have a much easier time understanding what your code does. ### Do Blocks You can use a do block if limitating the scope proves useful. #### EXAMPLE ###### THIS EXAMPLE IS TAKEN DIRECTLY FROM ROBLOX'S STYLE GUIDE, IT DOES NOT ADHERE TO THIS STYLE GUIDE. ```lua= local getId do local lastId = 0 getId = function() lastId = lastId + 1 return lastId end end ``` ### Use of Parenthesis Use parenthesis in if statements if it improves readability. This is based on the programmers judgement. #### EXAMPLE ##### Good You can easily see the seperation. ```lua= if (x > y and y > z) or (y > x and z > y) then -- Do Stuff end ``` ##### Bad Harder to see the seperation. ```lua= if x > y and y > z or y > x and z > y then -- Do Stuff end ``` ### In-line Statements You should use in-line if statements if they are used for breaking out of a function. #### Example ##### Good ```lua= local badValue = true if badValue then return end -- Escapes if the scary bad value is there :( ``` Condensed into a single line, saving space without harming readability. ##### Bad ```lua= local badValue = true if badValue then return end -- Escapes if the scary bad value is there :( ``` 3 Lines used for something that can easily be condensed without harming readability. ### Conditional Checks You should avoid things like "if value ~= nil" and instead do "if value". This saves spaces and makes the code more readable. You can also use the not operator. #### Example ##### Good ```lua= local badValue = nil if badValue then return end -- Escapes if the scary bad value is there :( ``` ```lua= local badValue = nil if not badValue then return end -- Escapes if the scary bad value is not there :) ``` ##### Bad ```lua= local badValue = nil if badValue ~= nil then return end -- Escapes if the scary bad value is there :( ``` ```lua= local badValue = nil if badValue == nil then return end -- Escapes if the scary bad value is not there :) ``` ### Nesting You should try and reduce nesting code as much as possible. Doing this is as simple as returning out with in-line if statements. #### Example ##### Good ```lua= local badValue = nil if badValue then return end -- Does stuff here. ``` ##### Bad ```lua= local badValue = nil if not badValue then -- Does stuff here. end ``` --- ## Whitespace You should try and keep whitespacing to a minimum whilst not being too shallow. Too much whitespacing makes the code harder to read. However too little makes it condensed and thus, also hard to read. **DO NOT EVER** use extra horizontal whitespace. **ALWAYS** end a script with an extra line. (You can see this automatically being done inside newly created module scripts.) ### General #### EXAMPLE ###### Good ```lua= --[[ Services ]]-- local ReplicatedStorage = game:GetService("ReplicatedStorage") local UserInputService = game:GetService("UserInputService") local GuiService = game:GetService("GuiService") --[[ Private Variables ]]-- local Platform: "Unknown" | "PC" | "Mobile" | "Console" = "Unknown" local _Packages = ReplicatedStorage.Packages local _AvailableInputs = script.AvailableInputs local _Skills = ReplicatedStorage.Skills ``` ###### Bad ```lua= --[[ Services ]]-- local ReplicatedStorage = game:GetService("ReplicatedStorage") local UserInputService = game:GetService("UserInputService") local GuiService = game:GetService("GuiService") --[[ Private Variables ]]-- local Platform: "Unknown" | "PC" | "Mobile" | "Console" = "Unknown" local _Packages = ReplicatedStorage.Packages local _AvailableInputs = script.AvailableInputs local _Skills = ReplicatedStorage.Skills ``` ### Tab Width This guide uses a tab width of 3 spaces, it just looks and reads better. See for yourself! This is mainly preference and isn't a requirement for the guide. **Note: The practices stated in the guide are not shown in the code below, the code was created before this guide came into creation!** ![](https://hackmd.io/_uploads/rJ4hQzbj2.png) --- ## Frameworks There are a lot of different frameworks however Knit is the one primarily used with this style guide. It's not a necessity to use a framework but is recommended as it helps with management along with plenty other benefits. [Official Knit Documentation](https://sleitnick.github.io/Knit/) --- ## Type Checking We use type checking everywhere in this style guide. Make sure to familarize yourself with type checking via the basics linked below before reading on. [Luau Type Checking Basics](https://devforum.roblox.com/t/type-checking-for-beginners/1027242) When you create a function always use type assignment to function variables. Additionally, when a function returns, assign the type that it returns. #### EXAMPLE ##### Good ```lua= local function Add(number1: number, number2: number): number if typeof(number1) ~= "number" or typeof(number2) ~= "number" then return end -- We need this so the function doesn't error if something other than a number is inputed into the arguments. return number1 + number2 end Add(1, 2)--> 3 -- We know it takes in two arguments that are both numbers and returns a number. ``` ##### Bad ```lua= local function Add(number1, number2) if typeof(number1) ~= "number" or typeof(number2) ~= "number" then return end -- We need this so the function doesn't error if something other than a number is inputed into the arguments. return number1 + number2 end Add(?, ?)--> ? -- We don't know what it takes in or returns. ``` Of course we would know what that Add function takes in and what it does, but for more complex functions we might not. Thus it's useful to use type checking so we know exactly what it takes in and returns without having to backtrack and look at the function. --- ## OOP/Classes Classes should never use inheritance and instead favor composition. This makes it similar to an ECS system. This means better code reusability, a smaller hierarchy, and only carrying over the functionality we need. [Luau OOP Composition Guide](https://www.youtube.com/watch?v=KNT-vjqB_qI) --- ## Format This guide uses a template to ensure easier locating of individual parts in a script. Most people will disagree with this and say code is meant to look boring. However, I believe there is nothing wrong with prettying up your code. Well, as long as it doesn't compromise functionality and readability. #### Template ```lua= --[[ Services ]]-- --[[ Private Variables ]]-- --[[ Tables ]]-- --[[ Modules ]]-- --[[ Private Functions ]]-- --[[ Handling ]]-- ``` This allows us to search for sections and know where everything is placed. Below is an example of this template filled out using the Knit framework. ```lua= --[[ Services ]]-- local ReplicatedStorage = game:GetService("ReplicatedStorage") --[[ Private Variables ]]-- local _Packages = ReplicatedStorage.Packages local _Debris = workspace.Debris --[[ Tables ]]-- --[[ Modules ]]-- local Knit = require(_Packages.Knit) local Debris = require(_Packages.Debris) --[[ Private Functions ]]-- --[[ Handling ]]-- local DebrisController = Knit.CreateController { Name = "DebrisController" } function DebrisController:KnitInit() _Debris.ChildAdded:Connect(function(object: Instance) local destroyWait = object:GetAttribute("DestroyWait") or 6 Debris:AddItem(object, destroyWait) end) end function DebrisController:KnitStart() end return DebrisController ``` --- ## Organization Although this isn't related to a code style, organization of a project can affect how the code is used and thus is included here. You should try to keep a maximum of 1 script and 1 local script. However, if your game uses a loading screen/system then it's perfectly acceptable to have another local script for handling that alone. I call this the 1:1 rule, however this is commonly referred to as single script architecture. #### Folders * Packages --> ReplicatedStorage * Modules --> ReplicatedStorage, StarterPlayerScripts, ServerScriptService #### Script Type Locations | Parenting/Ancestors * Scripts --> ServerScriptService * LocalScripts --> StarterPlayerScripts, ReplicatedFirst * ModuleScripts --> ReplicatedStorage, StarterPlayerScripts, ServerScriptService For further clarification, I would think of it like this, you want to hide as much code as possible from the client. This is called abstraction. Based of this, if you need to access a function inside a module from both the client and server, then you would put it under replicated storage. If you only need it on the server, then putting it where the scripts are would be best. The inverse also applies for local scripts. This way of doing things provides 2 main benefits. 1. It allows you to know what each modules access level is; server, client, or both. 2. It follows the abstraction principle, and keeps unnecessary code from the client's view. **NOTE:** The purpose of the **packages** folder is to store modules that can be accessed and used in other projects without outside dependants. You should **refrain** from storing something game specific inside the folder. **NOTE:** It's okay to have nested dependencies under the actual package as long as it's not from the outside. #### Examples ##### Good ![](https://hackmd.io/_uploads/BygtF4Mj3.png) The "Hitbox" module depends on the "SETTINGS" module. The "Knit" module depends on both the "KnitClient" and "KnitServer" modules. This is acceptable because those dependencies are directly nested under the module script that is the main package. ##### Bad ![](https://hackmd.io/_uploads/HyW5FVzj3.png) Lets say the "Knit" module depends on the "Hitbox" module to run and vice versa. This is bad because they won't work on their own. Meaning they are unable to be carried over to other games by themselves alone. --- ## End Well that's it for the style guide. You can leave a heart if you want. Have any suggestions/questions/concerns? Feel free to dm me on discord @aquamizu. I'm always welcome to hear anyone's opinion!