Services
Services are single-instanced modules (singletons) that represent game features that you really only need one of.
Controllers?
Unlike other frameworks, there is no concept of "client controllers." Instead, services can be placed on both the server and client.
Nevertheless, it would still be a good idea to enforce a convention to name clientside services as "Controllers" for clarity.
Creating a simple service
First, let's define a simple service.
--@service
local service = {}
function service._init()
print("Hello World!")
end
function service.greet(name: string)
print("Hello " .. name .. "!")
end
return service
Warning
Do not place blocking or heavy code inside of a service's _init() method as it yeilds before the next service can start. Instead, you should use the Roblox task.spawn or task.defer functions. Also note that this method along with any annotated methods should be defined with . instead of :.
Dependency injection
This project aims to reduce the need for manual requires as much as possible. Thus, the preferred way to utilize services is through dependency injection (DI). It is a technique where an object recieves dependencies externally rather than internally.
In this case, the runtime loader injects other services into the constructors of services that require them. Utilizing this would decouple your game features and thus allow for modular code, easier testing, and easier refactors.
Let's create a new service that utilizes the greet() method of the above GreetService.
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
--@service, depends=[GreetService]
local service = {}
function service._init(deps)
service.deps = deps
Players.PlayerAdded:Connect(function(player)
deps.GreetService.greet(player.Name)
end)
end
return service
In this example:
- The
dependsargument accepts a list of service names to inject. - The
depsparameter is a table which contains the injected services at runtime. - We store the
depsobject inside the module so that we could access it in other functions. - We import the autogenerated type file for the service which contains the injected dependency list type.
Note
The build tool will automatically determine the load order of services based on injected dependencies!
@initService
This annotation basically allows you to use a function as a service. It's great for any one-time loaders which require dependencies.
It's effectively the same as defining a service with only an _init() method.
local m = {}
--@initService, depends=[GreetService]
function m.greetPlayer(deps)
Players.PlayerAdded:Connect(function(player)
deps.GreetService.greet(player.Name)
end)
end
return m
@dependency
This is a simple annotation which @service inherits from. Modules annotated with it are not loaded automatically at runtime, ie they have no _init method. Use this for pure data modules which still need DI.