Skip to main content

Code Guidelines

When working with others, it's important to keep things as consistent as possible in order to allow easy and fast iteration throughout the different projects, by different people. Here are our currently established coding guidelines which helps us achieve this goal. Feel free to reach out for help or clarification on anything you see here.

Packages


Below is a list of common packages that you should be familiar with in order to efficiently work with our codebases.

PackageDescription
RoamGame and Service bootstrapper
ModulesOnRailsPathless module fetcher
NetWireAlternative networking solution
BaseObjectBase class for objects
PromiseClass for handling asynchronus code
SignalClass to create custom events
JanitorCleanup helper object
InputClasses to provide easier input handling
FusionState management library. Commonly used for UI
TableManagerClass to make and listen to changes on tables
TableReplicatorEnables replication of tables
PlayerProfileManagerOrion PlayerData solution
ComponentClass to create objects associated with a tagged instance
BaseComponentComponent extension to provide common useful functionality
RemoteComponentComponent extension to provide networking functionality

General


Rules of thumb

  • No magic numbers, make it a constant! The same logic applies to constant strings repeated throughout your code.
  • Variables, fuction parameters, and function return values should be properly type defined.
  • Any Roblox Service used within a file should be declared at the top in the Services section using game:GetService(). (workspace is an exception to this rule)
  • Prefer using the ModulesOnRails Import function to fetch modules instead of using require, you should avoid having direct paths.
  • Every file must have a unique name, otherwise it will cause issues when using Import.
  • UI should be setup as its own component file and should have an associated Hoarcekat .story file.
  • All yielding code should be wrapped in a Promise or a Coroutine.
  • Your code should be well documented. Code should be ledgible enough that you dont need many comments to explain what it is doing. Use comments to instead explain why you are doing something.
  • Avoid deffering variable declaration where possible.

Syntax for naming

  • Constant variables should be formatted in UPPER_SNAKE_CASE.
  • Properties, ClassNames, Enums, and non-static method names are PascalCase.
  • Static class functions should be called with the dot operator . and formatted in camelCase.
  • Object methods should be called with the colon operator : and formatted in PascalCase.
  • Private properties or functions of a class should be prefaced with an underscore _ to denote it is not intended for external use.

Function Structure Syntax

Functions and Methods should be prefaced with a Method/Function header which contains a brief description about what the function does, how it works, and clarification on the parameters if not clear at a glance. If you are describing param and return values then use the moonwave documentation format.

Private Functions have behavior which is only supposed to be used in the file it's defined in.

local function add(num1: number, num2: number): number
return num1 + num2
end

Private Methods are an object's function which has behavior that is only supposed to be used by the object itself. They should have _ before its name to help indicate that they are private.

function MyClass:_Increment(amount: number?)
self._Money += amount or 1
end

Public Methods are an object's function which have behavior that can be used externally.

function MyClass:GetMoney(): number
return self._Money
end

Static Functions are functions of a class meant to be accessed without an object of said class.

function MyClass.getFromInstance(obj: Instance): MyClass
assert(Storage[obj], `{obj:GetFullName()} was not found in storage.`)
return Storage[obj]
end

File Structure


Header / Preamble

All files should start with a preamble containing the author(s), creation date, and file description. This can be autogenerated with the preamble snippet.

-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class MyFile

This file is an example template for something and does XYZ
]=]

Your file should be split up and organized into logical sections for easy searching. You can use the sectionheader snippet to generate a generic barrier.

--------------------------------------------------------------------------------
--// Private Functions //--
--------------------------------------------------------------------------------

local function Greet(name: string)
print("Hello "..name)
end

Global Variable Declaration

Any variables in the global scope should be declared at the top of the file, typically in the following order:

  1. Services » Roblox services.
  2. Imports » Module requires and imports.
  3. Types » Type declarations.
  4. Constants » Constant variables.
  5. Volatiles » Volatile and other uncategorized variables.
  6. Private Functions » Module level functions.
  7. Module » The module declaration and logic

Below is an example of a potential module file setup.

--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")

--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import) -- Singular "require" usage exception.
local Constants = Import("Constants")
local Types = Import("Types")

--// Types //--
export type MyFileType = {
MyString: string,
MyValue: number
};
type Promise = Types.Promise

--// Constants //--
local DEFAULT_TIMEOUT = 60
local PLAYER_DATA_KEY = "InternalPlayerData"

--// Volatiles //--
local VisitorCount = 0

--------------------------------------------------------------------------------
--// Private Functions //--
--------------------------------------------------------------------------------

local function Sum(...number): number
local sum = 0
for _, num in {...} do
sum += num
end
return sum
end

--------------------------------------------------------------------------------
--// Module //--
--------------------------------------------------------------------------------

local MyModule = {}

return MyModule

Class Structures


Below are structure guides for the various common systems used within Orion. They detail en example usage along with guidelines for using them.

We also follow a specific structure for a declaration of a class and any associated methods within the file. Most of your classes should inherit from BaseObject at their lowest level.
  • Your class should be declared with a .ClassName property to enable BaseObject's type checker to function properly.
  • All fields should be declared in the constructor even if they start out as nil. Typecast them to what they will become.
  • Fields should usually be privated and accessed only through Getter and Setter methods.
  • If you override the :Destroy() method then make sure you call the superclass destroy method within it to ensure full cleanup.
-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class MyClass

New class which handles coding in general. Creates an entire game from scratch!
]=]

--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

--// References //--
local SharedAssets = ReplicatedStorage.SharedAssets

--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import)
local BaseObject = Import("BaseObject")

--------------------------------------------------------------------------------
--// Class //--
--------------------------------------------------------------------------------

local MyClass = setmetatable({}, BaseObject)
MyClass.ClassName = "MyClass"
MyClass.__index = MyClass

--------------------------------------------------------------------------------
--// Methods //--
--------------------------------------------------------------------------------

function MyClass:GetMyValue(): number
return self._MyValue
end

function MyClass:IncrementMyValue(amount: number)
self._MyValue += amount
end

function MyClass:GetOwner(): Player
return Players:GetPlayerByUserId(self._OwnerId)
end

--------------------------------------------------------------------------------
--// Core //--
--------------------------------------------------------------------------------

function MyClass.new(props: {
OwnerId: number
}): MyClass

local self = setmetatable(BaseObject.new(), MyClass)

self._OwnerId = props.OwnerId
self._MyPart = self:AddTask(Instance.new("Part"))
self._MyValue = 0

return self
end

-- @override
function MyClass:Destroy()
getmetatable(MyClass).Destroy(self) -- important for calling SuperClass destructors
end

--------------------------------------------------------------------------------
--// Finalization //--
--------------------------------------------------------------------------------

export type MyClass = typeof(MyClass.new())
return MyClass