Skip to content
🌊 You are viewing documentation for the Maelstrom pre-release channel. Pin a stable release for production.

Module Lifecycle

Riptide uses a three-phase lifecycle that gives every module a well-defined slot for initialization. Understanding these phases is the key to writing bug-free, race-condition-free code.


╔═══════════╗ ╔════════════╗ ╔═══════════╗
║ LOAD ║ ───▶ ║ INIT ║ ───▶ ║ START ║
╚═══════════╝ ╚════════════╝ ╚═══════════╝
All modules All :Init()s All :Start()s
are require()d run in sequence, run in parallel
first. synchronously. (task.spawn).

Before game modules load, Riptide loads and initializes configured plugins. Plugins are treated as framework extensions: they can expose PublicAPI, register sandbox-owned resources, and prepare infrastructure that services/controllers consume later.

Plugin Start runs as a bounded readiness barrier after game module Init, but before server player lifecycle replay and game module Start. A plugin can yield in Start, but it should only do bounded setup there; long-running loops should be spawned inside Start.

Riptide require()s every Lua module it finds in the folders you passed to Launch. No lifecycle methods are called yet — this is pure module loading. Every return value is stored internally by its module name.

Riptide calls :Init(Riptide) on every module that defines it, one at a time, in load order. The injected Riptide argument is already side-specific: server services receive Riptide.Server, and client controllers receive Riptide.Client. Because Init runs in sequence, you can safely call Riptide.GetService() or Riptide.GetController() to access other modules — they are already loaded, even if their own Init hasn’t run yet.

Once every module has finished Init, Riptide calls :Start(Riptide) on each module — wrapped in task.spawn, so they all run concurrently. You can yield freely here: start loops, wait for events, kick off async work, etc.


TaskPhase
Store a reference to another Service/ControllerInit
Register a Network event handlerInit
Set initial State valuesInit
Set up ComponentServiceInit
Start a while-true game loopStart
task.wait or WaitForChildStart
Connect to Players.PlayerAdded signalsStart (or use OnPlayerAdded)
Connect a UI event (GuiButton.Activated)Start

Two additional lifecycle methods are automatically called for you:

MethodWhen
:OnPlayerAdded(Riptide, player)A player joins. Also replays for existing players after plugin readiness and module initialization.
:OnPlayerRemoving(Riptide, player)A player leaves.

-- ServerScriptService/Services/DataService.lua
--!strict
local DataService = {}
-- Called once, in sequence, before Start.
function DataService:Init(Riptide)
-- ✅ Safe: grab a reference to another service.
-- Even if EconomyService's Init hasn't run yet, it IS loaded.
self.Economy = Riptide.GetService("EconomyService")
-- ✅ Safe: register network handlers.
Riptide.Network.Register("GetCoins", function(player)
return Riptide.State:Get("coins", player)
end)
-- ❌ WRONG: never yield in Init.
-- task.wait(1) ← this would block all other modules!
end
-- Called once everything has Init'd. Runs in its own task.spawn.
function DataService:Start(Riptide)
-- ✅ Safe to yield in Start.
while true do
task.wait(30)
self:_autosaveAll()
end
end
-- Fires for players already online AND new joiners.
function DataService:OnPlayerAdded(Riptide, player)
-- ✅ Safe to yield (runs in task.spawn).
local data = self:_loadFromDataStore(player)
Riptide.State:SetForPlayer(player, "coins", data.coins)
Riptide.State:SetForPlayer(player, "level", data.level)
end
function DataService:OnPlayerRemoving(Riptide, player)
self:_saveToDataStore(player)
end
-- Private helpers (prefixed with _ by convention)
function DataService:_loadFromDataStore(player)
-- task.wait() is fine here — called from Start/OnPlayerAdded
task.wait(0.1) -- simulated async load
return { coins = 0, level = 1 }
end
function DataService:_saveToDataStore(player)
print("Saving data for", player.Name)
end
function DataService:_autosaveAll()
print("Autosaving all player data...")
end
return DataService

function MyService:Init(Riptide)
-- Server: get a Service by name
local Economy = Riptide.GetService("EconomyService")
-- Client: get a Controller by name
local UI = Riptide.GetController("UIController")
-- Shared modules (loaded by SharedModulesFolder) are
-- accessed via require() directly, like normal Lua modules.
local Config = require(ReplicatedStorage.SharedModules.Config)
end