This codebase design born while I was organizing a bad codebase, most of them was result of my frustration whlie organizing the codebase. Its my first time taking it this serious, and I would like to improve it further with community’s feedback. My aim was to bring clarity and group related code while seperating unrelated code from it. After all if I know what this code accesses I don’t need to worry about what else it might be accessing.
Systems
System is a folder that contains all scripts and assets related to it.
For example gun system’s server side code, contained inside system folder that located in ServerScriptService.SystemName
, while assets contained in a system folder that located in ReplicatedStorage.Assets.SystemName
.
Here is how file structure for gun system would look, I will call “Guns” to this system.
As you can see everything inside folder called Guns, when we search in explorer “Guns.**” we will only see everything related to Guns system.
Thanks to this method no matter how bad code is written at least its limited to certain scope, and you will know that this badly written code isn’t touching anything else than what shows up in explorer after searching “Guns.**” in explorer
For assets that inside workspace, we could put those inside Workspace.Scripted.Guns
, I recommend using CollectionService instead of directly accessing them, if they’re accessed via CollectionService you can put them anywhere inside Workspace.
APIs
Lets say we have another system that handles kill rewards and logs, lets call this system KillTracker.
We want our Guns system to report kills to the KillTracker so, KillTracer system can display it on user’s UI and reward player for the kill.
We can add BindableEvent to KillTracer system and call when some gets a kill. At first this sounds good and to distinct it we definitly can put this bindableEvent into folder called “Public” to indicate its used by other systems. But what if want to get kill count from KillTracer? now we need to add BindableFunction, as you can see number of events increases, and to see what arguments those events accept we have to dig through KillTracer system’s internal code. To solve this problem, we use ModuleScript that handles all of that, thats what we call API, it allows us to do all of that easier.
API naming is important this allow us to distinct what available to us, if API has code that only will be called on client side we call it [systemName]ClientAPI, we put this API inside folder that dedicated for system’s modules inside ReplicatedStorage. If both client and server uses it we call, [systemName]SharedAPI. if its server side only we put it inside system’s folder that is inside ServerScriptService and we call it [systemName]API. Reason we don’t call [systemName]API when its used by both side is that, even scripter that clueless about codebase rules can distinct what side API supports, afterall everyone knows childrens of ServerScriptService only can be accessed by server side.
Internal code can use remotes and events to communicate with other internal code. APIs there to distinct public code from private code.
If we have leaderstats system, we shouldn’t change/read values directly by using player.leaderstats.Money.Value
as we might in future change how our leaderstats works,
because of this change/read these values using leaderstats mananging system’s API. This is just easier to explain example, I am sure it could get complicated and hard to manage. We could have ragdoll system that is based on boolean attribute, on character, but we might decide to change it and remove it, now we need to update all code that uses ragdoll attribute to make player ragdoll.
Its also harder to tell which system handles that value we are accessing, further more lowering clarity.
Sub-systems
Lets say we have system that handles so many parts that, we want to break it down, for example we could have system that handles abilities, like in combat games where player presses a key and character throws fireball. As you may realized every ability has its own assets, client and server side code, which is perfect candidate to be a system, but we want these abilities only managed by Ability system since those abilities won’t work standalone, they each need to communicate with some system to see if user has cooldown or can use moves, imagine we put those checks on every single ability’s code and then we wanted to change something in check, now we have to change code in every ability.
To prevent this we have Ability system, each ability will ask Ability system if they can be activated. When they activate Ability system logs their cooldown and prevents other abilities activating until this current ability finishes. So now we know we need Ability system and only that system will interact with each ability, we can make each ability sub-system of Ability system by creating new folder inside Ability system and contain all abilities inside it, lets call that folder Abilities.
Here how it would look in explorer.
As you can see, each ability is ModuleScript and there is no API for each ability, because this is clearly sub-system it wouldn’t be bad to use them like this. To clear confusions further more, we never allow other systems to access other systems sub-systems, because of this we don’t really need APIs for each sub-system to give parent system access to them. Be carefull tho, we can only do this because there is clearly a pattern that we can automate, like putting activation function inside each ability’s module. If there is no pattern its better to add API to sub-system, for sake of increasing clarity, as some systems could be massive and have tons of sub-systems. Sub-system APIs should be only accessed by direct parent of sub-system, and grandparent not allowed to access it. We need this rule so it becomes easier to remove/edit sub-system while not affecting parent system too much. This also gives us clarity what layer this sub-system’s APIs belongs to.
Code structure
To make it easier to track what APIs, events and assets our code uses, we should always put those things at the top.
- roblox services/APIs (game:GetService(“serviceName”))
- system APIs (system API variable names should be same as their module name)
- internal modules (modules inside current system we are coding)
- internal remotes/events
- asset variables (like screenui, model)
- constants
- variables
- functions
- module table (if this is module script)
- module functions (if module)
- connections like playerAdded
- return statement (if module)
When accessing something outside of this code/script starting directory should be always stored at 5th category which is asset variables. Avoid using script.AssetName or ReplicatedStorage.AssetName outside of this category as it will be hard to track and know.
Naming
Write code like someone one day would read it, and definitly you might read it in future when you want to add something to existing feature.
Don’t abbreviate variable names, plr is harder to read than player, hrp is harder than reading humanoidRootPart, while it might be easier to write abbrivated versions it creates confusion.
Use camelCase for function, method and variable names.
Use PascalCase for requires, APIs, roblox APIs and class names.
Add indicator at the end of remotes/events
like purchaseRE, eventBE, getDataBF. RE means remote event while BE means bindable event etc. We are doing this because it elimates extra steps of checking explorer or scrolling up in code, to see what type of event they’re.
When naming ModuleScripts use similar format we used for APIs
[moduleName][Client/Shared/Server] to give more clarity after all ModuleScripts doesn’t directly tell us which side they belong by looking at their name or file icon inside top bar.
Be consistent with naming, if event is about cooldown ending, don’t give it name of “CooldownBE”,
give it “CooldownEndedBE”.
Before creating new system make sure that system doesn’t exists by searching its name in explorer. Some names should be avoided for example “Sounds” can be pretty common as folder name that stores sounds related to a system.
Don’t put code that doesn’t reflect script’s name. For example if script deals with visuals and its named “FireballVisuals” or similar, don’t put there hitbox detection. Instead name it properly, if it deals with visuals and hitboxes you could just name it “Fireball”.
Misc
Using --!strict
would bring more clarity at cost of dealing with types.
Don’t write comments that explains the code, write comments that add context, for example there is code that grabs assets a specific way, a comment could say why we grabing assets this way, maybe other way would break it. Essentially just don’t write what is already written, scripters gonna read it, not non scripters.
Because StarterPack doesn’t allow system folders, parent tools to StarterPack in runtime for extra clarity. You also can store tool’s UI inside tool and then parent that ScreenGui to PlayerGui when equipped, and to make ScreenGui disappear when player dies, we can turn on ResetOnRespawn property of that ScreenGui. I also recommend naming tool’s ui in format like this [toolName]Tool.
For rojo users, parent LocalScript that should be inside tool in runtime.
Localscripts goes to StarterPlayerScripts, StarterCharacterScripts.
Scripts goes to ServerScriptService.
ModulesScripts can also be stored in ReplicatedStorage when two sides use same module, otherwise put those modules inside respective places that Scripts and Local Scripts has.
For utility modules they can be stored inside ReplicatedStorage.Libraries
for those who familiar with Rojo workflow, modules that managed by package managers can be stored inside ReplicatedStorage.Packages
.
Delete old unused assets or move them to folder called “Trash”. Don’t leave commented old code laying around. As they will create confusion.
Don’t leave or comment print statements after finishing debugging, as they bloat output.
Create a system if something used widely and you think there will be future extension, for example if your scripts frequently use TakeDamage() on humanoid, and your game is about combat, it might be wise to make API for it since you might want to track damage player taken or dealt in future.
Don’t rely on ResetOnRespawn to hide UI, hide the UI properly via code. Avoid using magic similar to this one.
Off topic coding tips
WaitForChild should only be used for instances that created/replicated on runtime, thats why you have to use WaitForChild when accessing player’s UI elements because they’re put in there runtime. We don’t need to use WaitForChild when accessing something like asset from ReplicatedStorage or ServerStorage or similar, only thing you need to watch out is workspace, StreamingEnabled might cause the thing you’re accessing not even exist so even WaitForChild can’t save you, if its server side code you will rarely need to use WaitForChild.