Docs
Docs
Preamble
Hello! Welcome to the BBI programming team! I'm Jaiden and I'm so happy to have you working with us here at BBI on the programming team. We're a diverse family of scripters and programmers delivering only the best to the rest of Bunker Bravo. I truly believe we serve as a backbone to our games, thus we must arm ourselves with only the best tools, and hold ourselves to the upmost standards.
I hope you find my ramblings below useful. Good luck!
Code Standards
Authoring and Copyright
ALL scripts within Bunker Bravo Interactive games MUST contain five lines dedicated to:
- The name of the script
- A breif description of what the script does
- The author of the script
- The date the script was created
- A copyright notice
The following block is an example to base ALL scripts you write for Bunker Bravo Interactive on:
-- @name Server.luau
-- @description Returns utility and core functions the server uses
-- @author imskyyc
-- @date 29 Feb 2024 @ 10:06 PM PST
-- @copyright Bunker Bravo Interactive
Clear & Concise Code
Authoring scripts is alike to painting a picture. For some, you may benefit from planning out your picture beforehand, and having a solid idea of what you want to make from the get-go. For others, you may begin to create your picture and develop it as time goes on. Both methods are completely valid starting points! However, both starting points MUST end in the same place. Your code development process should always end in a well constructed, easy to read, linear script. This will help other developers read, interpret, debug, and fix your code. We always need to think about our scripts as if someone else is reading them!
There's two core concepts to keep in mind as your scripts develop over time, or when you're in your planning phase: Obvious Variables and DRY Scripting.
Obvious Variables imply, well, variables should be OBVIOUS. For example:
local parts = {}
A reasonable person would expect the parts table to be some sort of Dictionary or Array of parts, not some array of booleans or other values.
DRY Scripting DRY is a concept which expands to "Don't repeat yourself." At its core, it means exactly what it says. If you find yourself repeating the same blocks of code over and over and over, you've reached a point where you should make a function that executes that code. This will keep your scripts less repetative and easier to read.
- Non-DRY script example
workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1 workspace.part.Transparency -= 1
Not great right?
- DRY script example
local decrementTransparency = function() workspace.part.Transparency -= 1 end for i = 1, 10 do decrementTransparency() end
Linear Flow
A script which has linear flow reads bottom to top, having your setup at the bottom, then your handler functions, then your DRY functions, then your constants / variables.
Personally, I like to organize my script in to the following sections:
-- services
-- data
-- functions
-- main
-- exports (module-specific)
These simple comments help me, but more importantly help other developers determine what each chunk of the script does. Subdividing like this will help you develop scripts in a more effective, efficient, and clean manner.
Organization & Commenting
Scripts should make use of comments to document their behavior. Commenting can be accomplished in a few ways, but primarily via chapters OR linear flow.
If your script does not move linearly, then you may need to show in your functions where your code jumps to next, to speed up the debugging process, and ensure code stays intuitive and readable.
Chaptering may include sectioning, for example, groups of functions that have similar behavior, like audio functions.
Appropriate Script Placement
Make use of folders! The scripts folder is obviously where all of our scripts are placed. We want to avoid having a "Miscellaneous" folder, as it can be very confusing going forward. Try and find an appropriate & intuitive place for the code you write. It will help other developers.
Code Resources
Typechecking
Basics
LuaU supports eight "primitive" types, of which can be typeguarded via the type() or typeof() functions. Those are: nil: null / empty string: characters / text number: if you need me to explain this im really questioning how you got here boolean: true / false table: see #3 function: see #3 thread: Coroutines or threads spawned via the coroutine or task functions. userdata: Roblox data models (BasePart, Part, etc)
Tables & Functions CANNOT be declared as local t: table = {}, or local f: function = ... Userdata values are represented via concrete types (any Roblox built-in class)
When checking base Lua types, you should only use the type() function, as it's about 50% faster than typeof().
Declaring a type is very simple. They act the same as variables but take slightly different arguments. For example: type SomeType = {}
Typically you would declare a variable like that via the local keyword. But for types, you utilize the type keyword. Types do not take the same arguments as variables, they only take other types. In a module, if you want other scripts that require your script to be able to use the types you declare elsewhere, you must export those types. Exported types can be used the same as any require()'d module index.
For example, a module named types exports an Array type and a Dictionary type. The module can return nil at the end, because exporting types acts as a return. A script utilizing the types module can now access Types.Array when requiring.
Type unions can be used to join two different types together. For example, Roblox classes that extend eachother have their types unioned, so typechecks for one are valid for the other. Example: the type BasePart which includes all rendered Roblox instances that are parts (MeshPart, Part, Sphere, etc) extends Instance, so the types of Instance which include things like the ChildAdded event are valid when you have a BasePart.
You can declare type unions via the & operator. For example: type Type1 = { string } type Type2 = { number } type Type3 = Type1 & Type2 & { boolean }
Type3 can now be an array of strings, numbers, or booleans. You can also use the | (or) operator to indicate a type could be one or the other. type Type3 = (Type1 | Type2) & { boolean }
The ? operator, in my opinion, is one of the most useful type operators in Lua. It can be appended to any type to indicate that the value could be nil. It's equivalent to stating local Variable: string | nil = <something>
Tables & Function types can be declared by the data / arguments they hold.
For example:
type PlayerData = {
Ranks: Dictionary<number, number>,
Badges: Dictionary<number, number>
}
Any type declared via type __ = ... can take strict type arguments via a pair of less-than (<) and greater-than (>) signs. For example: type Dictionary<K, V> = { [K]: V }[K] indicates the type of the indexes / keys, V indicated the type of the values.
This is the same concept if you were to do local Table = { [SomeVariable] = Value }, like the old MultiTween function.
Functions can be declared via a lambda function: local SomeFunction = (Arguments) -> (Returns)
For example: local SomeFunction = (string, number, ...any?) -> nil | (...any?)
When to use typechecking
Typechecking has very little effect on a produciton deployment. The only place where typechecking has a use is for typeguarding. Types should typically be used in order to help other developers understand the datasets you're working with.
For example: Developer A makes a function that adds a part to a table. The type for such a table would be Array<BasePart> However, Developer A is unclear as to what should be contained in the array. Developer B comes along and needs to modify that code, but doesn't know what data is stored in the table.
A properly typed table in this case would read: local Table = {} :: Array<BasePart> This indicates clearly that the table is an Array of BaseParts.
Typechecking should also be used when creating classes or other custom data models. Example:
export type SomeType = {
propertyOne: boolean,
propertyTwo: string,
propertyThree: number
}
Object-Oriented Lua
Object Oriented Lua is a concept that brings the benefits of abstraction and objects in to LuaU. There's a few basic requirements to get OOP working, but I like to add on typechecking to round out the API for other scripts to use.
Basic OOP example:
local Class = {}
Class.__index = Class
-- methods
function Class.someMethod(self)
-- do something
end
-- constructor
function Class.new()
local baseClass = {}
return setmetatable(baseClass, Class)
end
OOP with typechecking:
local Class = {}
Class.__index = Class
-- methods
function Class.someMethod(self: Class)
-- do something
end
-- types
export type BaseClass = {
something: string
}
export type Class = typeof(setmetatable({} :: BaseClass, Class))
-- constructor
function Class.new() : Class
local baseClass: BaseClass = {
something = "Hello, World!"
}
return setmetatable(baseClass, Class)
end
Framework Documentation
Coming soon.