Hello. I was responding recently under someone’s thread asking about Knit and if it’s useful and I thought that I’ve been working long enough with my custom framework that I might make a tutorial in case anyone finds it helpful. I was heavily inspired by this thread, but the way I structure my code has evolved a lot to suit my specific needs. If you’re looking for a way to structure your code that takes full advantage of Luau, works well in all workflows, and doesn’t have any excessive APIs or steep learning curves, this is probably for you.
Services
Services are definitely the bulk of where my code goes. These control the different systems of your game, and can be called upon to do actions. I also usually organize them to control a section of data, and that single service is responsible for handling that data. Services are module scripts that can be found in ReplicatedStorage > Services.
ObjectService
To give an example of one of the services in my game, I’m going to introduce you to ObjectService. ObjectService is responsible for handling all of the objects in my game. Objects are strictly typed and have very specific properties (See data section).
Let’s look at this ObjectService.CreateNew
function.
function ObjectService.CreateNew(name : string, player : Player?) : Object
if isServer then
local data : Object = table.clone(Objects.GetObject(name))
local model = ReplicatedStorage.Assets.Objects:FindFirstChild(data["assetName"], true)
data.model = ModelService.CreateOnServer(model)
if not _Data[player] then
_Data[player] = {}
end
_Data[player][data.name] = data
return data
else
return ObjectEvents.ExecuteOnServer:InvokeServer("CreateNew", name, LocalPlayer)
end
end
This is an actual block of code from one of my games, and essentially I clone an object template and return a new object of type Object. I write most of my functions so that they are usable on the server and the client.
This is accomplished by using a helpful piece of code in each script (or in your init function if applicable that says):
local isServer = game:GetService("RunService"):IsServer()
if isServer then
ObjectEvents.ExecuteOnServer.OnServerInvoke = function(_player, func, ...)
return ObjectService[func](...)
end
end
The self parameter
One thing to note is that not every function takes an object as its first parameter like you might see in an OOP approach. I use detailed names and Luau typechecking to only pass in the information that I need, not the entire object.
Data
The way I organize my data was taken largely from this video. While I didn’t agree with everything in the video, I did like his formatting so I’ll share some of it with you guys now.
I also store my Data in ReplicatedStorage>Data.
ObjectData
As a continuation of the data handled in ObjectService, I want to show you what the Object data looks like, and what kind of properties it might contain.
ObjectData.luau
export type ObjectCategory = "Decoration" | "Amenity"
export type Object = {
name: string,
currentLevel: number,
maxLevel: number,
cost: number,
timeRemaining: number,
assetName: string,
model: Model?,
category : ObjectCategory?
}
Once again, this is stored in a module script. Then, as a child of the script, I have an “Objects” folder where each folder is the title of an object.
drinkmachine.luau
local ObjectData = require(script.Parent.Parent)
type Object = ObjectData.Object
local drinkmachine : Object = {
name = "Drink Machine",
currentLevel = 1,
maxLevel = 3,
cost = 250,
cycleTime = 30,
timeRemaining = 0,
assetName = "vendingmachine_drinks",
model = nil,
}
return drinkmachine
This helps me ensure everything is very strictly typed and modularized.
UI
This section is a little bit more optional, but I personally manage all my UI via code and test it using UI-Labs (which I highly recommend checking out by the way).
Basically, I build all my UI and then use Codify to convert it to code. By having this in a module script with related functions I can also keep all panel-specific animations or functionality together and then reference it in my services.
For example, I might have a function called
function Messages.createMessage(message : string) : Frame
and another one called
function Messages.fade(frame : Frame, transparency : number) : ()
Using UI-Labs also helps me figure out how to clean up my GUI easily. I have a Service called UIService where I basically create a “target” gui that destroys itself when I destroy all the frames inside.
function UIService.createScope(object) : ScreenGui
local gui = Instance.new("ScreenGui")
gui.Parent = LocalPlayer.PlayerGui
object.Parent = gui
-- If no more children, clean up gui :)
gui.ChildRemoved:Connect(function()
if #gui:GetChildren() == 0 then
gui:Destroy()
end
end)
return gui
end
UIService also has some handy helper functions that can be applied across different UI scripts.
Conclusion
This is the way that I modularize my code and keep game functions as separate as possible. Please feel free to criticize or ask any questions. Hopefully this was helpful to at least one person.