Player Lifecycle
The PlayerLifecycle module provides centralized, server-side hooks that Riptide automatically calls on your modules when players join or leave.
Player Lifecycle is server-only. The hooks are only called on modules loaded on the server.
Instead of manually connecting to Players.PlayerAdded and Players.PlayerRemoving inside every service, expose these methods on your module table and Riptide calls them for you.
OnPlayerAdded
Section titled “OnPlayerAdded”function MyService:OnPlayerAdded(Riptide: Riptide, player: Player)Called when a player joins the server. Also called retroactively for all players already connected at the time the module is loaded.
function CoinsService:OnPlayerAdded(Riptide, player) Riptide.State:SetForPlayer(player, "coins", 100) print(player.Name .. " joined — gave 100 starting coins")endOnPlayerRemoving
Section titled “OnPlayerRemoving”function MyService:OnPlayerRemoving(Riptide: Riptide, player: Player)Called when a player is leaving the server (Players.PlayerRemoving). By this point, all modules have completed their Init and Start phases, so it is safe to read self fields here.
function DataService:OnPlayerRemoving(Riptide, player) local coins = Riptide.State:Get("coins", player) self:SaveToDataStore(player.UserId, coins) print(player.Name .. " leaving — saved " .. coins .. " coins")endExecution Order
Section titled “Execution Order”Server starts │ ▼PlayerLifecycle:Start() ← retroactive OnPlayerAdded for existing players │ ▼Init Phase (synchronous) ← self.* fields are set here │ ▼Start Phase (task.spawn) ← game logic begins │ ▼Players.PlayerAdded → OnPlayerAdded(...) ← new players (Init is done, self.* is safe) │ ▼Players.PlayerRemoving → OnPlayerRemoving(...) │ ▼StateReplication:_onPlayerRemoving(player) ← automatic cleanupFor retroactive calls (players already connected at launch), hooks fire before Init. For players joining after launch, hooks fire after Init and Start. Always prefer the Riptide argument to be safe in both cases.
Automatic Cleanup
Section titled “Automatic Cleanup”The PlayerLifecycle module automatically triggers StateReplication:_onPlayerRemoving(player) after your OnPlayerRemoving hooks run. This allows your modules to safely read player-scoped state during shutdown, and then clears it from the StateReplication registry, preventing stale data and memory leaks.
Error Handling
Section titled “Error Handling”If a hook throws an error, it is caught via xpcall and logged. Other modules’ hooks continue to execute — one failing module does not block the rest.
[PlayerLifecycle] Error in OnPlayerAdded for CoinsService:<stack trace>Full Example
Section titled “Full Example”--!strictlocal ReplicatedStorage = game:GetService("ReplicatedStorage")local RiptidePkg = require(ReplicatedStorage.Packages.Riptide)type Riptide = RiptidePkg.Riptide
local SessionService = {}
SessionService._sessions = {} :: { [number]: number }
function SessionService:Init(Riptide: Riptide) -- nothing to injectend
function SessionService:OnPlayerAdded(Riptide: Riptide, player: Player) -- Use Riptide argument directly (safe for retroactive calls) self._sessions[player.UserId] = os.time() Riptide.State:SetForPlayer(player, "sessionStart", os.time())end
function SessionService:OnPlayerRemoving(Riptide: Riptide, player: Player) local startTime = self._sessions[player.UserId] if startTime then local duration = os.time() - startTime print(player.Name .. " played for " .. duration .. "s") end self._sessions[player.UserId] = nilend
return SessionService