I was reading over an existing post about making your own “instances” (or really services) and I really liked the concept but realized that the implementation needed to be expanded quite a bit to actually be practical. Briefly, what the post is about is using existing service instances as a foundation and then expanding on them with metaprogramming; however, the implementation provided in the post doesn’t function as it’s laid out there anymore assuming it ever did. Here I’m going to provide what I believe to be, in my opinion, a succinct implementation of the design pattern laid out in that original post, as well as some worked examples, and hopefully I will have convinced you of the utility of this pattern by the end.
1. The Problem
As a game’s code base begins to grow it becomes more and more unwieldy because of lua’s relatively primitive module system in also working with a different organization method, Roblox’s ServiceProvider
, and the :WaitForChild(...)
problem that exists in dealing with folders as a organizational tool.
2. The Solution
We design modules our game will be using in a similar fashion to how the Roblox API designs it’s services so that from a engineering perspective, we are at least working in a uniform API. This solves the issue of heterogeneity but we’re still left require
and a increasing, possibly redundant list of :WaitForChild(...)
calls.
These last two issues are solved by wrapping the ServiceProvider with a metatable who’s indices includes the modules which we would like to behave similarly to services like so:
Service.lua
local ClientServices = ... -- Path to a top level folder which contains our module scripts as described below
local Services = {}
function Services:GetService(serviceName)
local modules = PlayerServices:FindFirstChild(serviceName)
local service = game:GetService(serviceName)
local subservices = {}
local metaService = {}
if modules then
for ix, subservice in pairs(modules:GetChildren()) do
subservices[subservice.Name] = require(subservice)
end
return setmetatable(subservices, {
__index = function(tbl, key)
if type(service[key]) == "function" then
return function(_, ...)
return service[key](service, ...)
end
else
return service[key] or subservices[key]
end
end
})
else
return service
end
end
return Services
Using this and working directory like the one below where the ModuleScript
named Service
contains the snippet above.
The Service Directory
We have now wrapped Service instances in a way which is indistinguishable to an existing pattern in the Roblox API. Just as one might do retrieve the HumanoidController
with
local ControllerService = game:GetService("ControllerService")
local HumanoidController = ControllerService:FindFirstChildOfClass("HumanoidController")
we are able to retrieve our own modules with
local ServiceProvider = require(script.Parent:WaitForChild("Services").Service)
local ControllerService = ServiceProvider:GetService("ControllerService")
local CameraService = ControllerService.CameraService
3. Why This is Useful
For large games, it tends to be the case that the first 100 lines can just be incorporating things we need into the script for later use whether it be with require
, :GetService(...)
, :FindService(...)
, local variables which hold a reference to the result of :WaitForChild(...)
or what have you. Compressing that all down to something which is not only compatible with :FindFirstChild(...)
and :WaitForChild(...)
but removes the need to strictly call require
keeps things uniformly organized. It furthermore encourages that you program in a way that is compliant to how Roblox’s API is already designed. I’d even go so far as to say that this is absolutely how you should structure things on the server side and to an extent the client as well although it tends to be a little more chaotic there. Lastly, this solution is extremely lightweight only demanding a single new module.
4. What can be Improved
I think the brevity of a single function with a single utility is appropriate for discussion here but a few extensions to the implementation I’ve provided could make it much more flexible.
- Add caching so that only one wrapper per service is in use.
- Add a Service macro or base class so that service design is structurally consistent.
2a. Add inheritable methods connecting toServiceAdded
andServiceRemoving
. - Wrap
FindService