๐ ๏ธ Services
Services in Shard are server-side modules responsible for handling:
- Data management
- Game state and round management
- NPCs, quests, and world systems
- Purchases, economy, and backend tasks
- Any shared state between players
Just like controllers, Shard uses single file architecture ideology, meaning services are defined inside the src/server/Services
folder/file.
They are automatically loaded and bootstrapped on the server before the client initializes. This ensures all core systems are running and any Replica objects (like PlayerData
) are ready to be replicated to players.
Each service module can implement the following lifecycle methods:
Method | Purpose |
---|---|
new() | Optional constructor (inject dependencies if needed) |
initialize() | Called after construction, used to set up state, listeners, etc |
start() | Called after all modules are initialized |
Services are automatically added to
GetModule
. You should only callGetModule("SomeService")
in thestart()
phase to avoid dependency ordering issues.
Service Bootstrapper Raw Code
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Packages = ReplicatedStorage:WaitForChild("Packages")
local Framework = require(ReplicatedStorage.Configs.Framework)
local GetModule = require(ReplicatedStorage.Packages.GetModule)
local Data = require(script.Parent.Data)
local Promise = require(Packages.Promise)
local start = tick()
GetModule.Debug = Framework.GET_MODULE_DEBUG
local totalModules = #script:GetChildren()
local loaded = 0
local initalized = {}
local function loadModule(module, Data)
if Framework.DEV_MODE and RunService:IsStudio() then
task.spawn(function()
local success, err = pcall(function()
local required = require(module)
if required.new then
required = required.new({
-- You can dependency inject here if needed
})
end
if required.initialize then
required:initialize()
end
initalized[module.Name] = required
GetModule:Add(required, module.Name)
end)
if not success then
warn(`๐ซ {err}`)
end
loaded += 1
end)
else
local required = require(module).new({
-- You can dependency inject here if needed
})
if required.initialize then
required:initialize()
end
initalized[module.Name] = required
GetModule:Add(required, module.Name)
end
end
Promise.new(function(resolve, reject)
Data:initialize()
for _, module in script:GetChildren() do
loadModule(module, Data)
end
if Framework.DEV_MODE and RunService:IsStudio() then
while loaded < totalModules do
task.wait()
end
end
for name, module in initalized do
if module.start then
module:start()
end
print(`๐ Loaded {name}`)
end
resolve()
end)
:andThen(function()
print(`โ
Server Loaded Successfully in {tick() - start}s`)
workspace:SetAttribute("ServerLoaded", true)
end)
:catch(function(err)
warn(err)
end)
๐งช Basic Service Exampleโ
local MyService = {}
MyService.__index = MyService
function MyService.new()
local self = setmetatable({}, MyService)
return self
end
function MyService:initialize()
print("Initialized MyService")
end
function MyService:start()
-- Call GetModule here and after to avoid dependency ordering issues
print("Started MyService")
end
return MyService
You can also write services in a stateless singleton format if needed, just like controllers.
๐ฌ Advanced Service Exampleโ
In this example, we will utilize the Data module and PlayerProfile
to create a simple cash service that manages player cash.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
-- `observePlayer` is a core reactive utility bundled with Shard.
local observePlayer = require(ReplicatedStorage.Packages.Observers.observePlayer)
local Data = require(ServerScriptService.Data)
local CashService = {}
CashService.__index = CashService
type self = {
new: () -> CashService,
initialize: () -> (),
start: () -> ()
}
export type CashService = typeof(setmetatable({} :: self, CashService))
function CashService.new(): CashService
local self = setmetatable({} :: self, CashService)
return self
end
function CashService:initialize()
-- Observe player lifecycle events
observePlayer(function(player)
local playerProfile = Data:GetProfileAsync(player)
if not playerProfile then return end -- Make sure to check if the player profile is valid
-- Income per second loop
local incomePerSecondThread = task.spawn(function()
while playerProfile do
task.wait(1)
playerProfile:Set("Cash", playerProfile:Get("Cash") + 1)
print("Cash: ", playerProfile:Get("Cash"))
end
end)
-- Cleanup on player removal
return function()
if incomePerSecondThread then
task.cancel(incomePerSecondThread)
end
end
end)
end
function CashService:start()
-- Reserved for post-initialization logic
end
return CashService