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.
Package | Description |
---|---|
Roam | Game and Service bootstrapper |
ModulesOnRails | Pathless module fetcher |
NetWire | Alternative networking solution |
BaseObject | Base class for objects |
Promise | Class for handling asynchronus code |
Signal | Class to create custom events |
Janitor | Cleanup helper object |
Input | Classes to provide easier input handling |
Fusion | State management library. Commonly used for UI |
TableManager | Class to make and listen to changes on tables |
TableReplicator | Enables replication of tables |
PlayerProfileManager | Orion PlayerData solution |
Component | Class to create objects associated with a tagged instance |
BaseComponent | Component extension to provide common useful functionality |
RemoteComponent | Component 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 usinggame:GetService()
. (workspace
is an exception to this rule) - Prefer using the ModulesOnRails
Import
function to fetch modules instead of usingrequire
, 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:
- Services » Roblox services.
- Imports » Module requires and imports.
- Types » Type declarations.
- Constants » Constant variables.
- Volatiles » Volatile and other uncategorized variables.
- Private Functions » Module level functions.
- 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.
- Generic Class
- Instance Component
- Roam Service
- Roam Controller
- Hoarcekat Story
- 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
- Generally, the
Instance
associated with the Tag should have itsStreamingBehaviorMode
set toAtomic
. - Use the RemoteComponent extension, to enable communication between Server / Client.
- When extending to use
RemoteComponent
, the Client version should have theClientComponent
suffix, and the Server version should have theServerComponent
suffix, and both versions should share the same Tag. - Any Instance or Connection created within a Component should be added as a task to ensure it is properly cleaned up.
-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class AreaZoneComponent
Creates a cool zone for cool players.
]=]
--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import)
local BaseComponent = Import("BaseComponent")
local Component = Import("Component")
local Zone = Import("Zone")
--------------------------------------------------------------------------------
--// Component Declaration //--
--------------------------------------------------------------------------------
local AreaZone = Component.new({
Tag = "AreaZone",
Ancestors = { workspace },
Extensions = { BaseComponent }, -- Make sure to always have `BaseExtension` here!
});
--------------------------------------------------------------------------------
--// Methods //--
--------------------------------------------------------------------------------
--[=[
Get the area into which this refers to.
]=]
function AreaZone:GetAreaLevel(): (number)
return (self:GetAttribute("AreaLevel") or 1)
end
--------------------------------------------------------------------------------
--// Component Core //--
--------------------------------------------------------------------------------
-- Used to declare all properties the component needs and will have.
function AreaZone:Construct()
assert(self.Instance.PrimaryPart,"[AreaZone] .Instance must have a .PrimaryPart defined. Source: " .. self.Instance:GetFullName());
self.InteractionZone = self:AddTask(Zone.new(self.Instance.PrimaryPart))
end
function AreaZone:Start()
self.InteractionZone.localPlayerEntered:Connect(function()
print("The local player has entered the zone!")
end)
end
function AreaZone:Stop()
-- Handle any additional cleanup here
end
return AreaZone
-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class CurrencyService
This file is an example template for something and does XYZ.
]=]
--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import)
local Types = Import("Types")
local Roam = Import("Roam")
--// Types //--
export type CurrencyType = ("Default" | "Premium")
--------------------------------------------------------------------------------
--// Private Functions //--
--------------------------------------------------------------------------------
-- Gets a formatted string representing the currency type key in the player's profile data.
local function GetCurrencyDataKey(currencyType: CurrencyType): (string)
return "CurrencyType_" .. currencyType
end
--------------------------------------------------------------------------------
--// Service //--
--------------------------------------------------------------------------------
local CurrencyService = Roam.createService {Name = "CurrencyService"};
CurrencyService.Client = {}
-- Provides a way for the Client to get their updated, current balance.
function CurrencyService.Client:GetBalance(player: Player, currencyType: CurrencyType)
return self.Server:GetBalance(player, currencyType)
end
--------------------------------------------------------------------------------
--// Public Methods //--
--------------------------------------------------------------------------------
--[=[
Gets the player's current balance of passed currency type.
@return Promise<number>
]=]
function CurrencyService:GetBalance(player: Player, currencyType: CurrencyType?): (Types.Promise)
currencyType = currencyType or "Default"
local DataKey = GetCurrencyDataKey(currencyType)
local PlayerDataService = Roam.getService("PlayerDataService")
return PlayerDataService:GetPlayerData(player, DataKey)
end
--[=[
Gives the player currency of passed type.
]=]
function CurrencyService:GiveCurrency(player: Player, amount: number, currencyType: CurrencyType?)
currencyType = currencyType or "Default"
local DataKey = GetCurrencyDataKey(currencyType)
local PlayerDataService = Roam.getService("PlayerDataService")
PlayerDataService:UpdatePlayerData(player, DataKey, function(currentValue: number)
return currentValue + amount
end)
end
--------------------------------------------------------------------------------
--// Service Core //--
--------------------------------------------------------------------------------
function CurrencyService:RoamStart(): ()
-- Runs after all Services' :RoamInit() have run.
-- At this point, it's safe to get other Roam Services.
end
function CurrencyService:RoamInit(): ()
self.Client = NetWire.Server.fromService(self) -- Provides an easy way to communicate between Client & Server.
end
-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class CurrencyController
This file is an example template for something and does XYZ
]=]
--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import)
local Roam = Import("Roam")
--------------------------------------------------------------------------------
--// Service //--
--------------------------------------------------------------------------------
local CurrencyController = Roam.createService {Name = "CurrencyController"};
--------------------------------------------------------------------------------
--// Public Methods //--
--------------------------------------------------------------------------------
--[=[
Gets the player's current balance of passed currency type.
]=]
function CurrencyController:GetBalance(currencyType: CurrencyType): (number)
-- Client exposed methods from the Server return as Promises!
return self.Server:GetBalance(currencyType):expect()
end
--------------------------------------------------------------------------------
--// Service Core //--
--------------------------------------------------------------------------------
function CurrencyController:RoamStart(): ()
-- Runs after all Services' :RoamInit() have run.
-- At this point, it's safe to get and interact with other Roam Services.
end
function CurrencyController:RoamInit(): ()
self.Server = NetWire.Client("CurrencyService") -- Provides an easy way to communicate between Client & Server.
end
-- Authors: Logan Hunt (Raildex)
-- Date: January 1st, 2000
--[=[
@class GenericButton.story
@ignore
]=]
--// Services //--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Imports //--
local Import = require(ReplicatedStorage.Orion.Import)
local Class = Import("GenericButton_fusion")
return function(target: ScreenGui)
-- Declare UI
local Object = Class {
OnPress = function()
print("Hello World!")
end,
};
-- Write test code here, if any
local thread = task.defer(function()
end)
Object.Parent = target -- Parent UI to hoarcekat
return function() -- Cleanup Handler
task.cancel(thread)
Object:Destroy()
end
end